$ sudo teach IT
Модуль 8 · Функции

Урок 8.4 — Лямбда-функции

Анонимные функции в одну строку. Func, Action и замыкания.

Представь, что тебе нужно написать функцию, которая используется всего один раз. Лень создавать отдельный метод, давать ему имя, прописывать все формальности? Лямбда-функция — это анонимный метод, который ты пишешь прямо на месте использования. Как стикер, на котором ты набросал заметку, не заводя для неё отдельный блокнот.

📝

Синтаксис лямбда-выражений

Лямбда — это краткая запись анонимного метода. Базовый синтаксис:

(параметры) => выражение

Символ => читается как «стрелка» или «переходит в». Слева — параметры, справа — тело функции.

C# · Первые лямбды
// Самая простая лямбда: принимает int, возвращает int
Func<int, int> square = x => x * x;
Console.WriteLine(square(5));  // 25

// Лямбда с двумя параметрами
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 7));  // 10

// Лямбда без параметров
Func<int> getRandom = () => new Random().Next(1, 100);
Console.WriteLine(getRandom());

// Лямбда с одним параметром — скобки можно опустить
Func<int, int> triple = x => x * 3;
Console.WriteLine(triple(10));  // 30

// Типы параметров можно указывать явно:
Func<int, int> doubler = (int x) => x * 2;

Лямбда-выражение vs Лямбда-инструкция

Лямбда-выражение имеет только одну строку справа от =>, без фигурных скобок и без return. Лямбда-инструкция использует фигурные скобки и может содержать несколько строк:

C# · Выражение vs инструкция
// Лямбда-ВЫРАЖЕНИЕ — одна строка, без return
Func<int, int, int> multiply = (a, b) => a * b;

// Лямбда-ИНСТРУКЦИЯ — несколько строк, с return
Func<int, int, int> complex = (a, b) =>
{
    Console.WriteLine($"Считаем: {a} * {b}");
    int result = a * b;
    Console.WriteLine($"Результат: {result}");
    return result;
};

Console.WriteLine(multiply(5, 3));   // 15 (лямбда-выражение)
Console.WriteLine(complex(5, 3));    // с логами + 15 (лямбда-инструкция)
🔗

Делегаты Func и Action

Лямбды работают через делегаты — типы, которые описывают сигнатуру метода. В .NET есть готовые делегаты:

Делегат
Описание
Action
void, без параметров
Action<T1>
void, 1 параметр
Action<T1, T2>
void, 2 параметра
Func<TResult>
Возвращает TResult, без параметров
Func<T1, TResult>
Принимает T1, возвращает TResult
Func<T1, T2, TResult>
Принимает T1, T2, возвращает TResult
C# · Func и Action
// Action — ничего не возвращает
Action greet = () => Console.WriteLine("Привет!");
greet();  // Привет!

Action<string> greetUser = name => Console.WriteLine($"Привет, {name}!");
greetUser("Анна");  // Привет, Анна!

Action<string, int> repeat = (text, count) =>
{
    for (int i = 0; i < count; i++)
        Console.WriteLine(text);
};
repeat("C#", 3);

// Func — возвращает значение
Func<int, int, int> max = (a, b) => a > b ? a : b;
Console.WriteLine(max(10, 7));  // 10

// Можно передавать лямбду в метод:
static void ProcessNumbers(int[] numbers, Func<int, int> processor)
{
    for (int i = 0; i < numbers.Length; i++)
        numbers[i] = processor(numbers[i]);
}

int[] data = { 1, 2, 3, 4 };
ProcessNumbers(data, n => n * n);
Console.WriteLine(string.Join(", ", data));  // 1, 4, 9, 16
🔍

Лямбды с LINQ — самое частое применение

На практике лямбды чаще всего используются с LINQ — Language Integrated Query. LINQ-методы принимают лямбды для фильтрации, проекции, сортировки данных:

C# · LINQ + лямбды
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

        // Where — фильтрация (лямбда возвращает bool)
        var evens = numbers.Where(n => n % 2 == 0);
        Console.WriteLine("Чётные: " + string.Join(", ", evens));

        // Select — преобразование (проекция)
        var squares = numbers.Select(x => x * x);
        Console.WriteLine("Квадраты: " + string.Join(", ", squares));

        // OrderBy — сортировка по ключу
        var sorted = numbers.OrderByDescending(x => x);
        Console.WriteLine("По убыванию: " + string.Join(", ", sorted));

        // Все вместе:
        var result = numbers
            .Where(n => n > 3)          // оставляем > 3
            .Select(n => n * 10)        // умножаем на 10
            .OrderBy(n => n);           // сортируем
        Console.WriteLine("Результат: " + string.Join(", ", result));

        // Any, All, Count — проверки
        bool hasBig = numbers.Any(n => n > 8);
        bool allPositive = numbers.All(n => n > 0);
        int countEven = numbers.Count(n => n % 2 == 0);

        Console.WriteLine($"Есть > 8: {hasBig}");       // True
        Console.WriteLine($"Все > 0: {allPositive}");    // True
        Console.WriteLine($"Чётных: {countEven}");       // 5

        // First, FirstOrDefault
        int firstBig = numbers.First(n => n > 5);
        int firstOr = numbers.FirstOrDefault(n => n > 20);
        Console.WriteLine($"Первое > 5: {firstBig}");   // 6
        Console.WriteLine($"Первое > 20: {firstOr}");   // 0 (default)
    }
}
💡 LINQ-методы находятся в пространстве имён System.Linq. Добавь using System.Linq; в начале файла. Лямбды в LINQ — это стандарт де-факто.
🔒

Замыкания (Closures)

Лямбда может «захватывать» переменные из внешнего контекста. Это называется замыкание (closure). Лямбда «запоминает» окружение, в котором была создана, и может использовать его переменные:

C# · Замыкание
int multiplier = 3;  // внешняя переменная

Func<int, int> multiplyBy = x => x * multiplier;
//                      лямбда захватила multiplier ↑

Console.WriteLine(multiplyBy(5));  // 15
Console.WriteLine(multiplyBy(10)); // 30

// Если изменить внешнюю переменную:
multiplier = 10;
Console.WriteLine(multiplyBy(5));  // 50 — лямбда видит новое значение!

// Замыкание с изменяемой коллекцией:
static List<Func<int>> CreateCounters()
{
    var counters = new List<Func<int>>();
    for (int i = 0; i < 5; i++)
    {
        // ⚠️ Внимание: захватывается одна и та же переменная i!
        counters.Add(() => i);
    }
    return counters;
}

var counters = CreateCounters();
foreach (var c in counters)
    Console.Write(c() + " ");  // 5 5 5 5 5 (а не 0 1 2 3 4!)
Console.WriteLine();

// Как исправить — копировать в локальную переменную:
static List<Func<int>> CreateCountersFixed()
{
    var counters = new List<Func<int>>();
    for (int i = 0; i < 5; i++)
    {
        int copy = i;  // копируем в локальную переменную цикла
        counters.Add(() => copy);
    }
    return counters;
}

var fixedCounters = CreateCountersFixed();
foreach (var c in fixedCounters)
    Console.Write(c() + " ");  // 0 1 2 3 4
Console.WriteLine();

⚠️ Важно: в C# 5+ замыкание на переменную цикла for работает корректно (каждая итерация создаёт новую переменную). Но в старых версиях (до C# 5) — была та же проблема, что показана выше. Если пишешь код, совместимый со старыми версиями — всегда копируй в локальную переменную.

Ещё пример замыкания — «фабрика» функций:

C# · Фабрика функций
static Func<int, int> CreateAdder(int increment)
{
    return x => x + increment;  // increment захвачен
}

var add5 = CreateAdder(5);
var add10 = CreateAdder(10);

Console.WriteLine(add5(3));   // 8
Console.WriteLine(add5(20));  // 25
Console.WriteLine(add10(3));  // 13

// Каждая лямбда хранит свой increment:
// add5 помнит increment = 5
// add10 помнит increment = 10
⚖️

Когда использовать лямбды?

✅ Хорошие случаи:

  • Короткая логика на месте — Where(x => x > 5)
  • Обратные вызовы (callbacks) — обработчики событий, OnClick
  • Передача поведения в метод — «как обработать каждый элемент»
  • LINQ-запросы — фильтрация, сортировка, группировка
  • Фабрики функций — создание функций с настроенным поведением

❌ Плохие случаи:

  • Сложная многострочная логика — лучше создать отдельный метод с именем
  • Повторяющееся выражение — не копируй одну лямбду 5 раз, вынеси в метод
  • Рекурсия — лямбды сложно делать рекурсивными
  • Читаемость страдает — если от一行 лямбды глаза разбегаются, пора сделать метод
💡 Золотое правило: если лямбда занимает больше 2-3 строк или содержит сложную логику — вынеси её в отдельный метод с понятным именем.
🧩

Практический пример: работа с лямбдами

C# · Лямбды в действии
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        var students = new List<(string Name, int Age, double Grade)>
        {
            ("Анна", 20, 4.5),
            ("Борис", 22, 3.8),
            ("Вика", 19, 5.0),
            ("Глеб", 21, 4.0),
            ("Дарья", 23, 4.8),
        };

        // Фильтрация: отличники (оценка >= 4.5)
        var excellent = students.Where(s => s.Grade >= 4.5);
        Console.WriteLine("Отличники:");
        foreach (var s in excellent)
            Console.WriteLine($"  {s.Name} — {s.Grade}");

        // Проекция: только имена
        var names = students.Select(s => s.Name);
        Console.WriteLine("Имена: " + string.Join(", ", names));

        // Сортировка по возрасту
        var byAge = students.OrderBy(s => s.Age);
        Console.WriteLine("\nПо возрасту:");
        foreach (var s in byAge)
            Console.WriteLine($"  {s.Name} — {s.Age} лет");

        // Агрегация: средняя оценка
        double avgGrade = students.Average(s => s.Grade);
        Console.WriteLine($"\nСредняя оценка: {avgGrade:F2}");

        // Комбинация: имена отличников, отсортированные по возрасту
        var topStudents = students
            .Where(s => s.Grade >= 4.5)
            .OrderByDescending(s => s.Grade)
            .Select(s => $"{s.Name} ({s.Grade})");

        Console.WriteLine("\nЛучшие студенты:");
        foreach (var s in topStudents)
            Console.WriteLine($"  {s}");

        // Создание кастомной функции через замыкание
        double minGrade = 4.0;
        var filtered = students.Where(s => s.Grade >= minGrade);
        Console.WriteLine($"\nСтуденты с оценкой >= {minGrade}:");
        foreach (var s in filtered)
            Console.WriteLine($"  {s.Name} — {s.Grade}");
    }
}
🧪

Мини-тест

Вопрос 1

Что выведет код: Func<int, int> f = x => x + 1; Console.WriteLine(f(5));?

Ответ: 6. Лямбда просто прибавляет 1 к аргументу.

Вопрос 2

В чём разница между лямбда-выражением и лямбда-инструкцией?

Ответ: Выражение — одна строка без return и {}. Инструкция — несколько строк с return и { }.

Вопрос 3

Что такое замыкание (closure) в контексте лямбд?

Ответ: Когда лямбда «захватывает» переменные из внешнего контекста и может их использовать (и даже изменять).

Вопрос 4

Какой делегат использовать, если метод возвращает int и принимает два double?

Ответ: Func<double, double, int>. Последний тип в Func — возвращаемый, остальные — параметры.

🧩

Задание для тренировки

Напиши программу «Анализ продаж», которая:

  1. Создаёт список кортежей (string Product, double Price, int Qty) — 6 продаж
  2. Использует LINQ с лямбдами для:
    • Выбора товаров дороже 1000
    • Сортировки по сумме (Price * Qty)
    • Группировки по цене (дешёвые < 500, средние 500-2000, дорогие > 2000)
    • Вычисления общей выручки
  3. Создаёт функцию через замыкание, которая фильтрует товары с прибылью выше заданного порога
  4. Выводит результаты
C# · Решение
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        var sales = new List<(string Product, double Price, int Qty)>
        {
            ("Ноутбук", 45000, 2),
            ("Мышь", 1500, 10),
            ("Клавиатура", 2500, 5),
            ("Наушники", 3500, 3),
            ("Кабель USB", 300, 20),
            ("Монитор", 22000, 1),
        };

        // Товары дороже 1000
        var expensive = sales.Where(s => s.Price > 1000)
            .Select(s => $"{s.Product} — {s.Price} руб.");
        Console.WriteLine("Дороже 1000:");
        foreach (var s in expensive)
            Console.WriteLine($"  {s}");

        // Сортировка по сумме (цена * количество)
        var byTotal = sales
            .OrderByDescending(s => s.Price * s.Qty)
            .Select(s => $"{s.Product}: {s.Price * s.Qty} руб.");
        Console.WriteLine("\nПо сумме продаж:");
        foreach (var s in byTotal)
            Console.WriteLine($"  {s}");

        // Группировка по ценовой категории
        var groups = sales.GroupBy(s => s.Price switch
        {
            < 500 => "Дешёвые",
            <= 2000 => "Средние",
            _ => "Дорогие"
        });

        Console.WriteLine("\nПо категориям:");
        foreach (var group in groups)
        {
            Console.WriteLine($"  {group.Key}: {group.Count()} товаров");
            foreach (var s in group)
                Console.WriteLine($"    - {s.Product} ({s.Price} руб.)");
        }

        // Общая выручка
        double totalRevenue = sales.Sum(s => s.Price * s.Qty);
        Console.WriteLine($"\nОбщая выручка: {totalRevenue:N0} руб.");

        // Замыкание — фильтр по порогу прибыли
        double profitThreshold = 5000;
        Func<(string Product, double Price, int Qty), bool>
            isProfitable = s => s.Price * s.Qty >= profitThreshold;

        var profitable = sales.Where(isProfitable);
        Console.WriteLine($"\nТовары с выручкой >= {profitThreshold}:");
        foreach (var s in profitable)
            Console.WriteLine($"  {s.Product} — {s.Price * s.Qty:N0} руб.");
    }
}
📌

Что важно запомнить

1️⃣
Лямбда — анонимная функция. Синтаксис: (параметры) => выражение или (параметры) => { инструкции; return; }.
2️⃣
Func — возвращает значение, Action — void. Последний тип в Func — всегда возвращаемый. Func<T1, T2, TResult> — два параметра, возвращает TResult.
3️⃣
LINQ + лямбды — стандартный паттерн. Where(x => условие), Select(x => преобразование), OrderBy(x => ключ).
4️⃣
Замыкание (closure) — лямбда захватывает переменные из внешнего контекста. Будь осторожен с захватом переменных цикла.
5️⃣
Лямбды — для краткости. Если логика сложная — вынеси в отдельный метод. Лямбда не должна быть длиннее 2-3 строк.

Тест: 8.4: Лямбда-функции

3 вопроса

Лямбды

Premium