Лямбда-выражения, делегаты и события

Tip

Вы новичок в разработке программного обеспечения? Сначала начните с учебников для начинающих. Освойте основные навыки работы с типами и методами перед использованием лямбда-выражений.

Иногда требуется передать небольшую часть поведения, функцию непосредственно другому методу. Например, может потребоваться отфильтровать список, но условие фильтрации изменяется в зависимости от ситуации. Вместо написания отдельного именованного метода для каждого возможного условия вы передаете само условие в качестве аргумента.

Лямбда-выражения — это функция C#, которая делает это возможным. Лямбда-выражение — это компактная встроенная функция, которую вы пишете без указания имени. Оператор => со стрелкой используется для разделения списка параметров из текста:

x => x * 2

Чтение слева направо: x является входным параметром, => означает "ведет к", и x * 2 является телом. Он вычисляет возвращаемое значение. Если нет параметров или нескольких параметров, заключите их в скобки: () => 42 или (left, right) => left + right.

Делегаты поддерживают лямбда-выражения

Чтобы использовать лямбда-выражение, компилятор C# должен знать две вещи: типы параметров и возвращаемого типа. Это описание, типы параметров и возвращаемый тип, называется типом делегата.

Тип делегата — это тип, представляющий сигнатуру метода. Переменная типа делегата может содержать любой подходящий метод, например, лямбда-выражение или именованный метод, если типы его параметров и возвращаемого значения совпадают.

Вы объявляете тип делегата с ключевым словом delegate :

delegate int Transform(int value);

Это объявление говорит: "Transform является типом делегата для методов, которые принимают один int и возвращают объект int". Затем можно назначить лямбда-выражение или именованный метод переменной этого типа:

Transform doubler = x => x * 2;    // assign a lambda expression
Transform squarer = Square;         // assign a named method

Console.WriteLine(doubler(5));      // 10
Console.WriteLine(squarer(5));      // 25

static int Square(int value) => value * value;

И doubler, и squarer имеют значение типа Transform. Вы вызываете их точно так же, как обычные методы. Компилятор проверяет, совпадает ли то, что вы присваиваете, с объявленной сигнатурой.

Встроенные типы делегатов: FuncAction

Объявление пользовательского типа делегата для каждой ситуации может быть повторяющимся. .NET предоставляет два семейства универсальных типов делегатов, Func и Action, которые охватывают большинство сценариев, поэтому редко требуется использовать ключевое слово delegate самостоятельно.

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

  • System.Func<T,TResult> (и Func<T1, T2, TResult>т. д.) представляет метод, возвращающий значение. Последний параметр типа всегда является возвращаемым типом; все предыдущие — это типы входных данных.
  • System.Action<T> (и Action<T1, T2>т. д.) представляет метод, возвращающий ничего (void). Все параметры типа — входные типы. System.Action без параметров типа представляет метод без входных данных и не возвращает возвращаемое значение.

Например, Func<int, int, int> описывает метод с двумя int входными данными и одним int результатом. Action<string> описывает метод с одним string входным и не возвращаемым значением.

Func<int, int, int> add = (left, right) => left + right;
Action<string> report = message => Console.WriteLine($"Report: {message}");

int total = add(5, 9);
report($"5 + 9 = {total}");

Используйте описательные имена параметров в лямбда-файлах, чтобы читатели могли понять намерение без сканирования полного текста метода.

Передача лямбда-выражения методу

Когда метод объявляет параметр Func или Action, вызывающие передают лямбда-выражение, соответствующее типу делегата. Компилятор проверяет, соответствуют ли лямбда-типы параметров и тип возвращаемого значения объявленному типу делегата. Если они не соответствуют, код не компилируется.

int[] numbers = [1, 2, 3, 4, 5, 6];
int[] evenNumbers = Filter(numbers, value => value % 2 == 0).ToArray();

Console.WriteLine(string.Join(", ", evenNumbers));

Метод Filter объявляет Func<int, bool> параметр с именем predicate. Тип Func<int, bool> сообщает вызывающим сторонам ожидаемую структуру: один int вход, один bool результат. Вызывающий передает value => value % 2 == 0 в качестве этого аргумента. Этот шаблон используется во множестве API LINQ и .NET.

Делайте лямбда-выражения самодостаточными

Лямбда-выражение может ссылаться на переменные из окружающего кода. Захват означает, что лямбда содержит ссылку на переменную, объявленную за пределами собственного тела. Сочетание лямбда и переменных, которые она захватывает, называется замыканием.

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

Func<int, bool> isEven = static value => value % 2 == 0;

Console.WriteLine(isEven(14));
Console.WriteLine(isEven(15));

Статические лямбды делают намерение понятным и предотвращают случайный захват.

Используйте параметры игнорирования, когда входные данные не актуальны

Иногда подпись делегата включает параметры, которые вам не нужны. Используйте параметр "отменить" _, чтобы явно указать на этот выбор.

Распространенные примеры включают обработчики событий, в которых вы не используете sender или EventArgs, обратные вызовы, в которых требуется только несколько входных данных, и перегрузки LINQ, которые предоставляют индекс, который не используется.

Action<int, int, string> statusUpdate = (_, _, message) => Console.WriteLine(message);
statusUpdate(200, 42, "Operation completed");

Исключения улучшают удобочитаемость, так как они показывают, какие параметры имеют значение.

События предоставляют необязательные уведомления

Событие — это механизм, который один объект (издатель) использует для уведомления других объектов (подписчиков) при возникновении чего-то. Издателю не нужно знать, кто слушает или сколько подписчиков есть. Подписчики решили принять участие.

События основаны на делегатах. Событие является полем делегата с дополнительными ограничениями, применяемыми ключевым словом event: внешний код может только подписываться (+=) или отменять подписку (-=) от события; только класс, объявляющий событие, может инициировать его (raise).

Соглашение .NET для типов делегатов событий — System.EventHandler<TEventArgs>, где T — это тип данных, включенных в уведомление. Его подпись всегда имеет два параметра: sender (объект, который вызвал событие) и данные события типа T.

MessagePublisher publisher = new();
publisher.MessagePublished += (_, message) => Console.WriteLine($"Received: {message}");

publisher.Publish("Records updated");

Пошаговое руководство по коду:

  • MessagePublisher event EventHandler<string>? MessagePublished объявляет . Ключевое слово event означает, что подписчики могут только подписаться или отменить подписку — вызывать его напрямую нельзя.
  • publisher.MessagePublished += (_, message) => ... подписывается с использованием лямбда-выражения. Обработчик игнорирует параметр _, поскольку он ему не нужен.
  • publisher.Publish("Records updated") вызывает событие и запускает все подписанные обработчики.

Подписка является необязательной. ?.Invoke(...) Метод Publish означает, что событие возникает только при подключении по крайней мере одного подписчика. Издатель поднимает событие, не зная или заботясь о том, слушает ли кто-либо.

См. также