Методы как значения
Чему вы научитесь:
- хранить метод в переменной и передавать его в другой метод через
Func,ActionиPredicate; - записывать лямбда-выражения и объяснять, что захватывает замыкание;
- отбирать, преобразовывать и сортировать элементы методами LINQ и считать по ним итоги;
- объяснять, когда выполняется запрос LINQ и зачем нужен
ToList().

Представьте карточку с рецептом. Готовить по ней можно самому, а можно отдать карточку другу: он приготовит блюдо, когда понадобится. Карточку можно положить в ящик, заменить другой или передать дальше, и рецепт при этом переписывать не нужно.
До сих пор метод в программе только вызывали, как копировальный аппарат из темы о методах: написали Max(3, 8), и он сразу выполнился. Но аппарат другу не отдашь, а карточку с рецептом отдать можно. В C# метод можно передать, как такую карточку: положить в переменную или отдать другому методу, а вызвать позже.
Зачем это нужно? Допустим, в одном месте программы нужно удвоить каждое число списка, а в другом — возвести каждое в квадрат. Два метода получатся одинаковыми, кроме одной строки с вычислением. Удобнее написать один метод, который обходит список, а вычисление передавать ему, как карточку с рецептом.
Начнём с переменной, в которой лежит метод:
Func<int, int> f = Twice;
Console.WriteLine(f(5)); // 10
f = Square;
Console.WriteLine(f(5)); // 25
int Twice(int x) => x * 2;
int Square(int x) => x * x;Давайте разберём программу по строкам:
Func<int, int> f = Twice;кладёт в переменнуюfсам методTwice. Скобок послеTwiceнет: метод здесь не вызывается, а передаётся.f(5)вызывает метод, который сейчас лежит вf. ЭтоTwice, и он возвращает10.f = Square;кладёт в ту же переменную другой метод.- Второй вызов
f(5)выполняет ужеSquareи печатает25.
Переменная с методом
Тип Func<int, int> описывает метод, который принимает int и возвращает int. Тип, переменная которого хранит ссылку на метод, называется делегатом. Переменную-делегат вызывают так же, как сам метод: имя и аргументы в скобках.
Имя метода без скобок, как Twice в первой строке, называют группой методов. Слово «группа» здесь потому, что под одним именем может быть несколько перегрузок, и компилятор выберет ту, что подходит делегату.
Здесь карточка с рецептом расходится с делегатом. На карточку можно записать любой рецепт, а в переменную Func<int, int> — только метод, который принимает int и возвращает int. Метод Greet(string name) туда не положить: компилятор сверит параметры и тип результата.
Готовые делегаты
В .NET есть готовые делегаты трёх видов. Их хватает почти для всех задач, поэтому свои типы делегатов, объявленные словом delegate, пишут редко. Этот вопрос часто задают на собеседовании: чем отличаются Func, Action и Predicate?
Func — с результатом
В угловых скобках перечисляют типы параметров, а последним — тип результата:
Func<int>— без параметров, возвращаетint;Func<int, int>— принимаетint, возвращаетint;Func<int, int, int>— принимает дваint, возвращаетint.
Action — без результата
Action описывает метод void. В угловых скобках только типы параметров: Action<string> принимает строку и ничего не возвращает. Action без угловых скобок не принимает ничего.
Predicate — проверка
Predicate<T> принимает одно значение типа T и возвращает bool: подходит значение или нет. Такую проверку и называют предикатом.
Action<string> greet = Greet;
greet("Anna"); // Hello, Anna!
Predicate<int> isEven = IsEven;
Console.WriteLine(isEven(4)); // True
Console.WriteLine(isEven(7)); // False
void Greet(string name) => Console.WriteLine($"Hello, {name}!");
bool IsEven(int n) => n % 2 == 0;greet("Anna") выполняет Greet и печатает приветствие. isEven(4) вызывает IsEven, и тот отвечает true.
Метод как аргумент
Вернёмся к задаче из начала урока. Параметр метода тоже может быть делегатом, и тогда метод получает вычисление снаружи:
List<int> nums = [1, 2, 3];
Console.WriteLine(string.Join(" ", Transform(nums, Twice))); // 2 4 6
Console.WriteLine(string.Join(" ", Transform(nums, Square))); // 1 4 9
List<int> Transform(List<int> items, Func<int, int> f)
{
List<int> result = [];
foreach (int n in items)
{
result.Add(f(n));
}
return result;
}Разберём, что делает Transform:
- Параметр
fтипаFunc<int, int>принимает любой метод изintвint. - Цикл обходит список и для каждого элемента вызывает
f(n). - Результаты складываются в новый список
result, который метод и возвращает. - Вызов
Transform(nums, Twice)передаётTwiceкак карточку, аTransform(nums, Square)— другую карточку. Цикл написан один раз.
Готовые методы .NET тоже принимают делегаты. У списка есть метод FindAll: ему передают Predicate<T>, и он возвращает новый список из подходящих элементов. Пример вызова: nums.FindAll(IsEven).
Частая ошибка
Метод передали со скобками. Здесь Apply получает метод и число и вызывает этот метод с числом x:
Console.WriteLine(Apply(Twice(), 5)); // ✗
int Apply(Func<int, int> f, int x) => f(x);
int Twice(int x) => x * 2;error CS7036: There is no argument given that corresponds to the required parameter 'x' of 'Twice(int)'Программа не компилируется. Скобки после имени означают вызов, поэтому компилятор пытается вызвать Twice без аргументов, а методу Twice нужен аргумент x. Чтобы передать сам метод, напишите имя без скобок: Apply(Twice, 5).
Главное
- Делегат — тип, переменная которого хранит ссылку на метод; её вызывают, как сам метод:
f(5). Funcвозвращает результат, и его тип стоит в угловых скобках последним;Actionничего не возвращает;Predicate<T>отвечаетbool.- Чтобы передать метод, пишут его имя без скобок:
Transform(nums, Twice). Со скобками это уже вызов.