В каком случае возникает проблема с областью видимости при использовании замыканий?
Что проверяют
Понимаете ли вы, что замыкание захватывает переменную, а не её значение в момент создания лямбды. Следом обычно дают код с циклом и просят сказать, что он выведет, или спрашивают, что при этом генерирует компилятор.
Как отвечать
Компилятор переносит захваченную локальную переменную в поле скрытого класса (display class), и лямбда вместе с внешним кодом работает с одним и тем же полем. Поэтому проблема видна там, где переменная меняется после создания делегата, а сам делегат выполняется позже:
- цикл
for: переменнаяiодна на весь цикл. Пять лямбд, созданных вfor (int i = 0; i < 5; i++), после цикла выведут5пять раз. Лечится копией в теле цикла:int current = i;; foreach: с C# 5 переменная цикла своя на каждой итерации, там проблемы больше нет. Об этом стоит сказать, иначе ответ выглядит устаревшим;- отложенное выполнение LINQ:
var q = items.Where(x => x > limit); limit = 10;при перечислении отфильтрует уже по новому значениюlimit; - асинхронный код:
Task.Run(() => Process(i))в циклеforполучит то значениеi, которое будет к моменту старта задачи, а не к моменту вызова.
Вторая сторона той же темы: захват продлевает жизнь объектов. Лямбда в подписке на событие долгоживущего объекта удерживает this и всё захваченное, отсюда утечки памяти в WPF и WinForms. С C# 9 лямбду можно пометить static, и компилятор запретит ей захват. Ещё ограничение: параметры ref, out, in и переменные типа Span<T> захватить нельзя, это ошибка компиляции.
Где спотыкаются
Рассказывают про foreach как про действующую проблему. Её исправили больше десяти лет назад, а ловушка с for осталась.
Говорят, что лямбда копирует значение. Будь это так, изменение переменной внутри лямбды не было бы видно снаружи, а оно видно.
Не замечают аллокаций. Каждое замыкание это объект display class плюс делегат, на горячем пути это заметная нагрузка на GC, поэтому там состояние передают параметром.