Делегаты лежат в основе 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отличается от публичного поля-делегата? - Как события приводят к утечкам памяти и как их избежать?
- Что вернёт вызов мультикаст-делегата с возвращаемым значением?