JavaRush /Курсы /C# SELF /Лямбда-выражения с Func

Лямбда-выражения с Func, Action, Predicate

C# SELF
50 уровень , 1 лекция
Открыта

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());

Визуальная схема

Сигнатура Пример Описание
Func<int, int>
x => x * 2
Принимает int, возвращает int
Func<int, int, int>
(a, b) => a + b
Два int, возвращает int
Func<string>
() => "hi"
Не принимает ничего, возвращает строку

Как это выглядит в вашем приложении

Допустим, в нашем мини-приложении (условный "Справочник пользователей"), у нас есть список чисел — хотим применить к нему разные обработки. Например, возвести в квадрат или посчитать сумму с фиксированным числом:

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

Визуальная схема

Сигнатура Пример Описание
Action
() => ...
Без параметров, без возврата
Action<int>
x => ...
Один параметр
Action<int, string>
(x, s) => ...
Несколько параметров

В нашем приложении

Добавим в справочник пользователей метод для печати всех имён:

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);

Таблица для наглядности

Делегат Сигнатура Пример лямбды Где используется
Func<T, U>
T → U
user => user.Name
Select, любые преобразования
Action<T>
T → void
user => Console.WriteLine(...)
ForEach, методы-действия
Predicate<T>
T → bool
user => user.Age > 18
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); // можно преобразовать через конструктор
2
Задача
C# SELF, 50 уровень, 1 лекция
Недоступна
Применение Action для обработки списка строк
Применение Action для обработки списка строк
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ