1. Введение
Знаете, что интересно? Лямбда-выражение само по себе — это как рецепт без кастрюли. Оно описывает, что делать, но ему нужен «контейнер», чтобы жить в коде. В C# для этого есть готовые универсальные типы делегатов: Func, Action и Predicate.
Представьте их как готовые формочки для ваших лямбд — берите нужную и заливайте свою логику. Никаких самодельных велосипедов и собственных типов делегатов, когда под рукой уже есть всё необходимое.
Немного истории
С самого начала, когда возникала необходимость передать функцию как параметр, требовалось объявлять собственный тип делегата. Это было долго, муторно и выглядело так:
delegate int Calculate(int x, int y);
Calculate adder = (a, b) => a + b;
Когда в C# появились Func, Action и Predicate, можно было забыть о ручном объявлении делегатов в 90% случаев. Выглядит теперь в разы проще и универсальнее:
Func<int, int, int> adder = (a, b) => a + b;
2. Func<T, TResult> — функция с возвратом
Синтаксис и назначение
Func — это шаблонный делегат (generic delegate), который принимает от 0 до 16 параметров и возвращает значение.
Func<int, int, int> sum = (x, y) => x + y;
Func<тип1, тип2, ..., типN, TResult> — все параметры до последнего — это аргументы, последний — возвращаемый тип.
Примеры
1. Сумма двух чисел
Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(3, 5)); // 8
2. Возведение числа в квадрат
Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // 16
3. Без параметров
Func<string> greet = () => "Привет, лямбда!";
Console.WriteLine(greet());
Визуальная схема
| Сигнатура | Пример | Описание |
|---|---|---|
|
|
Принимает int, возвращает int |
|
|
Два int, возвращает int |
|
|
Не принимает ничего, возвращает строку |
Как это выглядит в вашем приложении
Допустим, в нашем мини-приложении (условный "Справочник пользователей"), у нас есть список чисел — хотим применить к нему разные обработки. Например, возвести в квадрат или посчитать сумму с фиксированным числом:
List<int> numbers = new() { 1, 2, 3, 4, 5 };
Func<int, int> square = x => x * x;
var squares = numbers.Select(square);
Console.WriteLine(string.Join(", ", squares)); // 1, 4, 9, 16, 25
3. Action<T> — действие без возврата
Action — универсальный делегат для методов, которые что-то делают (например, печатают на экран), но ничего не возвращают.
Может принимать от 0 до 16 параметров, но всегда возвращает void.
Примеры
1. Печать на экран
Action<string> print = text => Console.WriteLine("Данные: " + text);
print("Привет, мир!");
2. Действие без параметров
Action greet = () => Console.WriteLine("Добро пожаловать!");
greet();
3. Действие с несколькими параметрами
Action<int, int> showSum = (a, b) => Console.WriteLine($"Сумма: {a + b}");
showSum(2, 3); // Сумма: 5
Визуальная схема
| Сигнатура | Пример | Описание |
|---|---|---|
|
|
Без параметров, без возврата |
|
|
Один параметр |
|
|
Несколько параметров |
В нашем приложении
Добавим в справочник пользователей метод для печати всех имён:
List<string> names = new() { "Анна", "Борис", "Вика" };
Action<string> printName = name => Console.WriteLine("Пользователь: " + name);
names.ForEach(printName);
// или так: names.ForEach(name => Console.WriteLine(name));
4. Predicate<T> — да или нет?
Когда нужна функция, которая вернет только true или false для одного параметра, используйте Predicate<T>. Это просто делегат, который принимает один параметр и возвращает bool.
Predicate — официальная "нам нужна булева проверка" обёртка для лямбды.
Примеры
1. Проверить, больше ли число 5
Predicate<int> isGreaterThanFive = x => x > 5;
Console.WriteLine(isGreaterThanFive(3)); // false
Console.WriteLine(isGreaterThanFive(7)); // true
2. Использование с методом List<T>.Find
List<int> values = new() { 2, 4, 7, 10 };
int found = values.Find(isGreaterThanFive); // используется Predicate<int>
Console.WriteLine(found); // 7
3. Все ли взрослые?
List<int> ages = new() { 12, 19, 34 };
bool allAdults = ages.TrueForAll(age => age >= 18);
// TrueForAll принимает Predicate<int>
В чем отличие от Func<T, bool>?
Фактически, они полностью взаимозаменяемы. Даже документация Microsoft говорит: "Predicate<T> — это просто Func<T, bool> для специальных API". Но методы стандартной библиотеки иногда просят именно Predicate.
5. Как лямбды "вписываются" в Func, Action, Predicate
Когда вы пишете лямбду, C# анализирует: "О, её форма соответствует требуемому делегату — можно подставить!"
Func<int, int> f1 = x => x * 2;
Action<string> a1 = text => Console.WriteLine(text);
Predicate<int> p1 = x => x < 10;
Везде лямбда! Но под капотом — три разных делегата с разной сигнатурой.
Применение на примере "реального" кода
List<User> users = new() {
new User("Анна", 24),
new User("Борис", 17),
new User("Вика", 31),
};
// Функция, возвращающая только взрослых пользователей (Predicate<User>)
List<User> adults = users.FindAll(user => user.Age >= 18);
Console.WriteLine("Список взрослых: " + string.Join(", ", adults.Select(u => u.Name)));
А если мы хотим вывести имена всех пользователей через Action<User>:
users.ForEach(user => Console.WriteLine(user.Name));
Получить их имена (Func<User, string>):
IEnumerable<string> names = users.Select(user => user.Name);
Таблица для наглядности
| Делегат | Сигнатура | Пример лямбды | Где используется |
|---|---|---|---|
|
T → U | |
Select, любые преобразования |
|
T → void | |
ForEach, методы-действия |
|
T → bool | |
Find, Exists, фильтры |
6. Примеры с внутренним приложением: шаг за шагом
Давайте расширим наше маленькое приложение "Справочник пользователей". Пусть у нас есть класс User:
public class User
{
public string Name { get; }
public int Age { get; }
public bool IsActive { get; set; }
public User(string name, int age)
{
Name = name;
Age = age;
IsActive = true;
}
}
1. Func<User, bool> — проверяем, совершеннолетний ли пользователь
Func<User, bool> isAdult = user => user.Age >= 18;
Используем в LINQ:
var adults = users.Where(isAdult);
2. Predicate<User> — ищем активного пользователя
Predicate<User> isActive = user => user.IsActive;
User found = users.Find(isActive);
3. Action<User> — деактивируем пользователя
Action<User> deactivate = user => user.IsActive = false;
users.ForEach(deactivate);
4. Func<User, string> — получаем короткое описание
Func<User, string> describe = user => $"{user.Name} ({user.Age})";
var descriptions = users.Select(describe);
Все эти лямбды — это вполне себе полноценный "код в виде данных", который можно передавать в методы, хранить в переменных, комбинировать.
7. Неочевидные нюансы и типичные ошибки
1. Лямбда должна соответствовать сигнатуре делегата. Если сигнатура не совпадает, будет ошибка компиляции.
Func<int, string> wrong = x => x * 2; // ошибка: ожидается string, получен int
// Правильно:
Func<int, string> right = x => (x * 2).ToString();
2. Не забывать про void vs return. Action не возвращает значения — попытка записать что-то вроде Action<int> a = x => x * x; не сработает, потому что возвращается значение, хотя не должно.
3. Predicate<T> и Func<T, bool> часто взаимозаменяемы, но не всегда. Иногда методы коллекций ожидают именно Predicate<T>, иногда — Func<T, bool>. Прямое присваивание не работает без явного обёртывания.
Predicate<int> pred = x => x > 0;
Func<int, bool> func = pred; // ошибка
// Но:
Func<int, bool> func2 = x => x > 0;
Predicate<int> pred2 = new Predicate<int>(func2); // можно преобразовать через конструктор
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ