Урок по треку C# и .NET 6 мин

Делегаты, лямбды и события

Func и Action, мультикаст-делегаты, лямбды и замыкания, event и утечки подписок.

Делегаты лежат в основе LINQ, событий, колбэков и Minimal API. На собесе проверяют замыкания, мультикаст и утечки через события.

Делегат

Делегат это типобезопасная ссылка на метод. Объект делегата хранит метод и, для методов экземпляра, целевой объект (Target).

Func<int, int, int> add = (a, b) => a + b;   // возвращает значение
Action<string> log = s => Console.WriteLine(s); // ничего не возвращает
Predicate<int> isEven = x => x % 2 == 0;

Свои типы делегатов (delegate int Op(int a, int b);) нужны редко, обычно хватает Func и Action.

Мультикаст

Делегаты неизменяемы, += создаёт новый делегат со списком вызовов. При вызове методы выполняются по очереди, а результат возвращается только от последнего. Исключение в одном прерывает остальных. Все методы списка доступны через GetInvocationList(). -= удаляет последнее вхождение.

Лямбды и замыкания

Лямбда захватывает переменную, а не её значение. Компилятор переносит захваченные переменные в поля скрытого класса:

var actions = new List<Action>();
for (int i = 0; i < 3; i++)
    actions.Add(() => Console.Write(i));   // все увидят i == 3
foreach (var item in new[] { 0, 1, 2 })
    actions.Add(() => Console.Write(item)); // в foreach переменная своя на итерацию

Следствия: аллокация объекта-замыкания при каждом вызове внешнего метода и «продлённая жизнь» захваченных объектов. Лямбда без захвата кешируется компилятором. static-лямбда (C# 9) запрещает захват.

События

event это делегат с ограниченным доступом: снаружи можно только += и -=, вызвать событие или затереть подписчиков через = нельзя.

public class Order
{
    public event EventHandler<PaidEventArgs>? Paid;
    protected virtual void OnPaid(PaidEventArgs e) => Paid?.Invoke(this, e);
}

?.Invoke защищает от null (подписчиков нет) и от гонки с отпиской: поле читается один раз.

Утечки через события

Источник события хранит делегаты подписчиков, а делегат держит ссылку на объект-подписчик. Пока жив источник, живы и подписчики. Типичный сценарий: окно или scoped-сервис подписался на синглтон и не отписался. Лечение:

  • отписка в Dispose;
  • подписка, возвращающая IDisposable-токен;
  • слабые события в UI.

Ещё одна ловушка: отписка лямбдой. x.Changed -= (s, e) => Handle(); ничего не удалит, потому что это новый делегат. Сохраните делегат в поле или подписывайте метод.

Async и события

Обработчик события может быть только async void. Источник не ждёт его, а исключение уходит в контекст синхронизации и может завершить процесс. Внутри такого обработчика обязательно try/catch. Для асинхронных доменных событий лучше свой диспетчер с Func<T, Task>.

Частые ошибки

  • Замыкание на переменную цикла for.
  • Ожидать результаты всех методов от мультикаст-делегата.
  • Забытая отписка и утечка памяти.
  • Отписка новой лямбдой.

Что спросят на собесе

  • Что такое делегат, чем Func отличается от Action?
  • Как компилятор реализует замыкание и какие у него последствия?
  • Чем event отличается от публичного поля-делегата?
  • Как события приводят к утечкам памяти и как их избежать?
  • Что вернёт вызов мультикаст-делегата с возвращаемым значением?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.