Перейти к содержимому
Теория · Лямбды и LINQ · урок 1 из 10

Методы как значения

Чему вы научитесь

Чему вы научитесь:

  • хранить метод в переменной и передавать его в другой метод через Func, Action и Predicate;
  • записывать лямбда-выражения и объяснять, что захватывает замыкание;
  • отбирать, преобразовывать и сортировать элементы методами LINQ и считать по ним итоги;
  • объяснять, когда выполняется запрос LINQ и зачем нужен ToList().
В машину Transform вставлена карточка Twice, плитки с 1, 2 и 3 точками выходят с 2, 4 и 6, рядом ждёт карточка Square

Представьте карточку с рецептом. Готовить по ней можно самому, а можно отдать карточку другу: он приготовит блюдо, когда понадобится. Карточку можно положить в ящик, заменить другой или передать дальше, и рецепт при этом переписывать не нужно.

До сих пор метод в программе только вызывали, как копировальный аппарат из темы о методах: написали Max(3, 8), и он сразу выполнился. Но аппарат другу не отдашь, а карточку с рецептом отдать можно. В C# метод можно передать, как такую карточку: положить в переменную или отдать другому методу, а вызвать позже.

Зачем это нужно? Допустим, в одном месте программы нужно удвоить каждое число списка, а в другом — возвести каждое в квадрат. Два метода получатся одинаковыми, кроме одной строки с вычислением. Удобнее написать один метод, который обходит список, а вычисление передавать ему, как карточку с рецептом.

Начнём с переменной, в которой лежит метод:

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;

Давайте разберём программу по строкам:

  1. Func<int, int> f = Twice; кладёт в переменную f сам метод Twice. Скобок после Twice нет: метод здесь не вызывается, а передаётся.
  2. f(5) вызывает метод, который сейчас лежит в f. Это Twice, и он возвращает 10.
  3. f = Square; кладёт в ту же переменную другой метод.
  4. Второй вызов 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: подходит значение или нет. Такую проверку и называют предикатом.

C#
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.

Метод как аргумент

Вернёмся к задаче из начала урока. Параметр метода тоже может быть делегатом, и тогда метод получает вычисление снаружи:

C#
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:

  1. Параметр f типа Func<int, int> принимает любой метод из int в int.
  2. Цикл обходит список и для каждого элемента вызывает f(n).
  3. Результаты складываются в новый список result, который метод и возвращает.
  4. Вызов Transform(nums, Twice) передаёт Twice как карточку, а Transform(nums, Square) — другую карточку. Цикл написан один раз.

Готовые методы .NET тоже принимают делегаты. У списка есть метод FindAll: ему передают Predicate<T>, и он возвращает новый список из подходящих элементов. Пример вызова: nums.FindAll(IsEven).

Частая ошибка

Метод передали со скобками. Здесь Apply получает метод и число и вызывает этот метод с числом x:

C#
Console.WriteLine(Apply(Twice(), 5)); // ✗

int Apply(Func<int, int> f, int x) => f(x);
int Twice(int x) => x * 2;
TEXT
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). Со скобками это уже вызов.