🔷 C#

Лямбда-функции и функциональный стиль в C# (.NET): подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы — шеф-повар в ресторане. У вас есть стандартный набор инструментов: ножи, сковородки, кастрюли. Но каждый день приходят разные посетители с разными пожеланиями. Один просит «нарежьте овощи кубиками», другой — «нарежьте соломкой», третий — «нарежьте кружочками». Вместо того чтобы для каждого способа нарезки покупать отдельный станок, вы просто берёте универсальный нож и говорите: «Вот нож, а как именно резать — я решу на месте, в зависимости от заказа».

В программировании лямбда-функция — это тот самый «универсальный нож». Это маленькая функция, которую вы создаёте прямо на месте, когда она нужна, и тут же её используете. Ей не нужно давать имя, её не нужно объявлять заранее в другом месте кода. Вы просто говорите: «Вот что нужно сделать с каждым элементом этого списка» — и описываете действие прямо в той строке, где оно выполняется.

Зачем это нужно? Представьте, что у вас есть список чисел, и вы хотите:

  • оставить только чётные;
  • каждое число умножить на 2;
  • отсортировать по убыванию.

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

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

Функциональный стиль — это подход, при котором мы описываем что нужно сделать с данными, а не как это сделать шаг за шагом. Вместо цикла с переменной-счётчиком мы говорим: «Возьми каждый элемент и примени к нему это действие». Лямбды — главный инструмент такого стиля в C#.

Теория: как это работает изнутри

Чтобы понять лямбды, нужно разобраться с двумя вещами: делегаты и деревья выражений. Не пугайтесь — объясню простыми словами.

Делегаты — это «коробки для функций»

Делегат — это тип данных, который может хранить ссылку на метод. Представьте пульт дистанционного управления. На пульте есть кнопка. Вы можете «назначить» на эту кнопку любое действие: включить свет, открыть дверь, запустить музыку. Сама кнопка — это делегат. А то, что произойдёт при нажатии — это метод, который вы на неё «повесили».

В C# есть встроенные делегаты, самые популярные:

Делегат Что принимает Что возвращает Пример использования
Func<T, TResult> Один или несколько параметров Значение Преобразование числа в строку
Action<T> Один или несколько параметров Ничего (void) Вывод элемента на экран
Predicate<T> Один параметр Всегда bool Проверка условия (фильтрация)

Лямбда-функция — это просто удобный способ создать метод и сразу «упаковать» его в такой делегат.

Деревья выражений — это «чертежи» лямбд

Иногда лямбду нужно не выполнить, а проанализировать. Например, чтобы перевести её в SQL-запрос для базы данных (это делают Entity Framework и другие ORM). В таких случаях лямбда превращается не в делегат, а в дерево выражений — структуру данных, которая описывает, что делает лямбда, но не выполняет её.

Простой пример: лямбда x => x > 5 как дерево выражений будет хранить информацию: «есть параметр x, есть операция сравнения "больше", есть константа 5». Это позволяет библиотеке прочитать этот «чертёж» и, например, написать SQL: WHERE x > 5.

Запомните: Если лямбда работает с коллекциями в памяти (List, Array) — это делегат. Если с IQueryable (база данных) — это дерево выражений. Разница важна, потому что в дереве выражений нельзя использовать некоторые конструкции (например, присваивание).

Замыкания — «память» лямбды

Лямбда может использовать переменные, которые объявлены вне её тела. Это называется замыкание. Лямбда «запоминает» значение этих переменных на момент своего создания.

Пример: вы объявили переменную int множитель = 10;, а потом создали лямбду x => x * множитель. Даже если вы потом измените множитель, лямбда будет использовать то значение, которое было «захвачено» в момент создания (если это не ссылочный тип).

Синтаксис в C# (.NET): все формы и варианты

Лямбда-функция в C# записывается с помощью оператора => (стрелка). Слева — входные параметры, справа — тело функции. Есть несколько форм записи.

1. Полная форма с блоком кода

// Самая полная форма: параметры в скобках, тело в фигурных скобках
Func<int, int, int> сложение = (int a, int b) =>
{
    int результат = a + b;
    return результат;
};

int сумма = сложение(5, 3); // результат: 8

2. Сокращённая форма (одно выражение)

// Если тело состоит из одного выражения, фигурные скобки и return не нужны
Func<int, int, int> сложение = (a, b) => a + b;

int сумма = сложение(5, 3); // результат: 8

3. Один параметр — без скобок

// Если параметр один, скобки можно опустить
Func<int, int> удвоить = x => x * 2;

int результат = удвоить(5); // результат: 10

4. Без параметров

// Если параметров нет, ставятся пустые скобки
Func<int> получитьСлучайное = () => new Random().Next(1, 100);

int число = получитьСлучайное(); // случайное число от 1 до 99

5. Action — лямбда без возврата

// Action не возвращает значение, только выполняет действие
Action<string> показатьСообщение = сообщение => Console.WriteLine(сообщение);

показатьСообщение("Привет, мир!"); // выведет: Привет, мир!

6. Лямбда с явным указанием типа

// Иногда нужно явно указать тип параметра (например, при неоднозначности)
Func<int, double> половина = (int x) => x / 2.0;

double результат = половина(5); // результат: 2.5

7. Лямбда в LINQ-запросах (самый частый случай)

List<int> числа = new List<int> { 1, 2, 3, 4, 5, 6 };

// Лямбда прямо внутри метода Where
List<int> чётные = числа.Where(n => n % 2 == 0).ToList();

// Результат: [2, 4, 6]

8. Асинхронная лямбда

// Лямбда может быть асинхронной (с await)
Func<Task<int>> получитьДанные = async () =>
{
    await Task.Delay(1000); // имитация задержки
    return 42;
};

int данные = await получитьДанные(); // результат: 42 через 1 секунду

Примеры кода — от простого к сложному

Пример 1: Фильтрация списка чисел

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        // Создаём список чисел
        List<int> числа = new List<int> { 10, 25, 3, 47, 8, 19, 30 };

        // Фильтруем: оставляем только числа больше 15
        // n => n > 15 — это лямбда, которая проверяет условие
        List<int> большие = числа.Where(n => n > 15).ToList();

        // Выводим результат
        Console.WriteLine("Числа больше 15:");
        foreach (int число in большие)
        {
            Console.WriteLine(число);
        }
        // Вывод: 25, 47, 19, 30
    }
}

Пример 2: Преобразование списка (проекция)

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<string> имена = new List<string> { "анна", "пётр", "мария", "иван" };

        // Преобразуем каждое имя в верхний регистр
        // s => s.ToUpper() — лямбда, которая принимает строку и возвращает строку в верхнем регистре
        List<string> заглавные = имена.Select(s => s.ToUpper()).ToList();

        Console.WriteLine("Имена заглавными:");
        foreach (string имя in заглавные)
        {
            Console.WriteLine(имя);
        }
        // Вывод: АННА, ПЁТР, МАРИЯ, ИВАН
    }
}

Пример 3: Сортировка с собственным правилом

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<string> слова = new List<string> { "яблоко", "груша", "слива", "ананас" };

        // Сортируем по длине слова (от короткого к длинному)
        // a => a.Length — лямбда, которая извлекает длину строки для сравнения
        List<string> отсортированные = слова.OrderBy(a => a.Length).ToList();

        Console.WriteLine("Слова, отсортированные по длине:");
        foreach (string слово in отсортированные)
        {
            Console.WriteLine(слово);
        }
        // Вывод: слива, груша, яблоко, ананас
    }
}

Пример 4: Поиск первого подходящего элемента

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> числа = new List<int> { 1, 3, 5, 7, 9, 10, 12 };

        // Находим первое число, которое делится на 3
        // n => n % 3 == 0 — проверка на делимость
        int первоеПодходящее = числа.FirstOrDefault(n => n % 3 == 0);

        Console.WriteLine($"Первое число, кратное 3: {первоеПодходящее}");
        // Вывод: Первое число, кратное 3: 9
    }
}

Пример 5: Проверка, все ли элементы удовлетворяют условию

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> оценки = new List<int> { 4, 5, 3, 5, 4, 5 };

        // Проверяем, все ли оценки больше или равны 3
        // o => o >= 3 — условие для каждой оценки
        bool всеХорошо = оценки.All(o => o >= 3);

        Console.WriteLine($"Все оценки удовлетворительные? {всеХорошо}");
        // Вывод: Все оценки удовлетворительные? True

        // Проверяем, есть ли хотя бы одна пятёрка
        bool естьОтличники = оценки.Any(o => o == 5);
        Console.WriteLine($"Есть отличники? {естьОтличники}");
        // Вывод: Есть отличники? True
    }
}

Пример 6: Группировка данных

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<string> фрукты = new List<string> 
        { 
            "яблоко", "груша", "апельсин", "абрикос", "арбуз", "вишня" 
        };

        // Группируем фрукты по первой букве
        // f => f[0] — лямбда, которая извлекает первый символ
        var группы = фрукты.GroupBy(f => f[0]);

        Console.WriteLine("Фрукты, сгруппированные по первой букве:");
        foreach (var группа in группы)
        {
            Console.Write($"Буква '{группа.Key}': ");
            foreach (string фрукт in группа)
            {
                Console.Write($"{фрукт} ");
            }
            Console.WriteLine();
        }
        // Вывод:
        // Буква 'я': яблоко
        // Буква 'г': груша
        // Буква 'а': апельсин абрикос арбуз
        // Буква 'в': вишня
    }
}

Пример 7: Сложная обработка с несколькими лямбдами

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> числа = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

        // Цепочка операций:
        // 1. Фильтруем чётные числа (n => n % 2 == 0)
        // 2. Умножаем каждое на 10 (n => n * 10)
        // 3. Сортируем по убыванию (n => -n)
        var результат = числа
            .Where(n => n % 2 == 0)    // оставляем чётные: 2, 4, 6, 8, 10
            .Select(n => n * 10)       // умножаем: 20, 40, 60, 80, 100
            .OrderByDescending(n => n) // сортируем по убыванию
            .ToList();

        Console.WriteLine("Результат сложной обработки:");
        foreach (int число in результат)
        {
            Console.WriteLine(число);
        }
        // Вывод: 100, 80, 60, 40, 20
    }
}

Пример 8: Лямбда с захватом внешней переменной (замыкание)

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        int порог = 5; // внешняя переменная

        List<int> числа = new List<int> { 1, 4, 6, 3, 8, 2 };

        // Лямбда захватывает переменную порог
        // n => n > порог — использует значение порога на момент вызова
        List<int> большеПорога = числа.Where(n => n > порог).ToList();

        Console.WriteLine($"Числа больше {порог}:");
        foreach (int число in большеПорога)
        {
            Console.WriteLine(число);
        }
        // Вывод: 6, 8

        // Внимание: если изменить порог после создания лямбды, 
        // лямбда всё равно использует старое значение (для значимых типов)
        порог = 10;
        // большеПорога уже вычислен, он не изменится
    }
}

Сравнение с Python: чем похоже и чем отличается

Python — ещё один язык, где лямбды очень популярны. Если вы знаете Python, вам будет проще понять C#. Но есть важные различия.

Характеристика C# Python
Ключевое слово => (стрелка) lambda
Синтаксис (x, y) => x + y lambda x, y: x + y
Многострочность Да (через фигурные скобки) Нет (только одно выражение)
Типизация Строгая (типы важны) Динамическая (типы не указываются)
Замыкания Да, с особенностями (захват по значению для значимых типов) Да, с особенностями (позднее связывание)
Использование в коллекциях LINQ (Where, Select, OrderBy) Встроенные функции (filter, map, sorted)
Производительность Выше (компилируется в IL) Ниже (интерпретируется)
Делегаты vs функции Лямбда — это делегат (тип) Лямбда — это функция (объект)

Пример на Python для сравнения

# Python: фильтрация списка с лямбдой
числа = [1, 2, 3, 4, 5, 6]
чётные = list(filter(lambda n: n % 2 == 0, числа))
print(чётные)  # [2, 4, 6]

# То же самое на C#:
// List<int> чётные = числа.Where(n => n % 2 == 0).ToList();
Главное отличие: В C# лямбда — это строго типизированный делегат. Компилятор проверяет, что типы входных и выходных данных совпадают. В Python типы не проверяются до выполнения, что может привести к ошибкам во время работы программы.

Типичные ошибки и подводные камни

Ошибка 1: Захват переменной цикла

// ОШИБКА: Все лямбды будут использовать последнее значение i
List<Action> действия = new List<Action>();
for (int i = 0; i < 5; i++)
{
    действия.Add(() => Console.WriteLine(i));
}

foreach (var действие in действия)
{
    действие(); // Выведет: 5 5 5 5 5 (а не 0 1 2 3 4)
}

// ИСПРАВЛЕНИЕ: Создаём копию переменной внутри цикла
List<Action> действия2 = new List<Action>();
for (int i = 0; i < 5; i++)
{
    int копия = i; // копируем значение
    действия2.Add(() => Console.WriteLine(копия));
}

foreach (var действие in действия2)
{
    действие(); // Выведет: 0 1 2 3 4
}

Ошибка 2: Изменение захваченной переменной

int счетчик = 0;
Func<int> инкремент = () => счетчик++; // захватываем счетчик

Console.WriteLine(инкремент()); // 0
Console.WriteLine(инкремент()); // 1
Console.WriteLine(счетчик);     // 2 (счетчик изменился!)

// Будьте осторожны: лямбда может изменять внешние переменные,
// что приводит к неожиданным побочным эффектам

Ошибка 3: Использование лямбды с ref/out параметрами

// ОШИБКА: Лямбда не может захватывать ref или out параметры
void Пример(ref int x)
{
    // Ошибка компиляции!
    // Action действие = () => x = 10;
}

// ИСПРАВЛЕНИЕ: Используйте локальную переменную
void Пример2(ref int x)
{
    int копия = x;
    Action действие = () => копия = 10;
    действие();
    x = копия;
}

Ошибка 4: Путаница между делегатом и деревом выражений

// Работает: лямбда как делегат
Func<int, bool> делегат = n => n > 5;

// Работает: лямбда как дерево выражений
System.Linq.Expressions.Expression<Func<int, bool>> дерево = n => n > 5;

// ОШИБКА: Нельзя использовать блок кода в дереве выражений
// System.Linq.Expressions.Expression<Func<int, bool>> ошибка = n => 
// {
//     return n > 5; // Ошибка компиляции!
// };

Ошибка 5: Забыли ToList() или ToArray()

List<int> числа = new List<int> { 1, 2, 3, 4, 5 };

// ОШИБКА: Результат Where — это IEnumerable, а не список
var результат = числа.Where(n => n > 2);
// результат — это запрос, который выполнится только при переборе

// ИСПРАВЛЕНИЕ: Материализуем результат
List<int> результат2 = числа.Where(n => n > 2).ToList();

Лучшие практики и советы профессионалов

1. Используйте лямбды для коротких операций

Лямбда хороша, когда тело функции умещается в одну-две строки. Если логика сложнее — вынесите её в отдельный метод с понятным именем.

// Хорошо: короткая лямбда
var чётные = числа.Where(n => n % 2 == 0);

// Плохо: лямбда на 10 строк
var обработанные = числа.Select(n =>
{
    // ... много сложной логики ...
    // ... лучше вынести в отдельный метод ...
    return результат;
});

2. Давайте переменным осмысленные имена

// Плохо: непонятные имена
var result = data.Where(x => x.Age > 18).Select(x => x.Name);

// Хорошо: понятные имена
var совершеннолетние = пользователи.Where(пользователь => пользователь.Возраст > 18)
                                     .Select(пользователь => пользователь.Имя);

3. Избегайте побочных эффектов в лямбдах

Лямбда должна только преобразовывать данные, а не изменять внешнее состояние (например, писать в файл или менять глобальные переменные).

// Плохо: лямбда с побочным эффектом
int счетчик = 0;
var результат = числа.Select(n => 
{
    счетчик++; // побочный эффект!
    return n * 2;
});

// Хорошо: лямбда без побочных эффектов
var результат2 = числа.Select(n => n * 2);

4. Используйте лямбды с кортежами для сложных данных

// Создаём список кортежей
var люди = new List<(string Имя, int Возраст)>
{
    ("Анна", 25),
    ("Пётр", 30),
    ("Мария", 22)
};

// Лямбда с кортежем
var имена = люди.Select(человек => человек.Имя).ToList();

5. Помните об отложенном выполнении

LINQ-запросы с лямбдами выполняются не сразу, а при первом переборе результата. Это может привести к неожиданностям, если исходная коллекция изменится.

List<int> числа = new List<int> { 1, 2, 3 };
var запрос = числа.Where(n => n > 1); // запрос ещё не выполнен

числа.Add(4); // добавляем элемент после создания запроса

foreach (int n in запрос)
{
    Console.WriteLine(n); // Выведет: 2, 3, 4 (4 тоже попала!)
}

6. Используйте лямбды для обработки событий

Button кнопка = new Button();
кнопка.Click += (sender, args) => 
{
    Console.WriteLine("Кнопка нажата!");
};

Вопросы для самопроверки

  1. Что такое лямбда-функция в C#?

    Ответ: Это анонимная функция, которая создаётся с помощью оператора =>. Она может быть преобразована в делегат или дерево выражений.

  2. В чём разница между Func и Action?

    Ответ: Func возвращает значение (последний тип в угловых скобках — это тип возвращаемого значения). Action не возвращает значение (void).

  3. Что такое замыкание в контексте лямбд?

    Ответ: Это способность лямбды использовать переменные, объявленные вне её тела. Лямбда «захватывает» эти переменные и может их читать или изменять.

  4. Почему в цикле for с лямбдой может возникнуть ошибка?

    Ответ: Потому что все лямбды захватывают одну и ту же переменную цикла, и после завершения цикла её значение равно последнему. Нужно создавать копию переменной внутри цикла.

  5. Что такое дерево выражений и чем оно отличается от делегата?

    Ответ: Дерево выражений — это структура данных, которая описывает код лямбды, но не выполняет его. Делегат — это ссылка на выполняемый код. Деревья выражений используются для анализа и трансформации кода (например, в SQL).

  6. Какой метод LINQ используется для фильтрации коллекции?

    Ответ: Where(). Он принимает лямбду-предикат (возвращает bool) и возвращает элементы, удовлетворяющие условию.

  7. Что такое отложенное выполнение LINQ?

    Ответ: Это когда запрос не выполняется сразу, а только при первом переборе результата (например, в foreach или после вызова ToList()).

  8. Можно ли использовать лямбду с async/await?

    Ответ: Да, можно. Для этого перед лямбдой нужно добавить ключевое слово async: async () => await Task.Delay(1000).

  9. В чём разница между лямбдой в C# и Python?

    Ответ: В C# лямбда строго типизирована и может быть многострочной. В Python лямбда — это всегда одно выражение, и типы не проверяются до выполнения.

  10. Как избежать побочных эффектов в лямбдах?

    Ответ: Не изменять внешние переменные, не выполнять ввод-вывод (консоль, файлы) внутри лямбды. Лямбда должна только преобразовывать входные данные в выходные.

Итог и что изучить