И начнем мы с определения. Что такое замыкание?
Замыкание (closure) в C# — это ситуация, когда лямбда-выражение или анонимный метод использует переменные из внешней области видимости, и компилятор обеспечивает, чтобы эти переменные продолжали существовать (и оставались общими для всех таких лямбд), даже если внешний метод уже завершил работу.
И сразу же многие думают про боксинг в этом случае. Но классический пример замыкания самый простой будет выглядеть как-то так:
static Func<int> MakeCounter()
{
int x = 0; // захваченная локальная переменная
return () => ++x; // замыкание: лямбда использует x из внешней области
}
static void Main()
{
var counter = MakeCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Console.WriteLine(counter()); // 3
}Почему это так работает? Потому что при таком использовании казалось бы странном, создается объект закмыкания, и разворачивается как-то так (грубо говоря):
// Скрытый класс замыкания (display class)
private sealed class DisplayClass
{
public int x; // сюда переезжает захваченная локальная переменная
public int Lambda()
{
return ++x; // тело лямбды
}
}
static Func<int> MakeCounter()
{
var closure = new DisplayClass();
closure.x = 0;
// Делегат хранит ссылку на closure (Target) и на метод closure.Lambda
return new Func<int>(closure.Lambda);
}
static void Main()
{
var counter = MakeCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
}и вот тут у нас value-type, local scope и всем всё привычно. Да, замыкание, знаем проходили. А для понимания примера из прошлого поста нужно внимательно прочитать определение и осознать фразу "использует переменные из внешней области видимости, и компилятор обеспечивает, чтобы эти переменные продолжали существовать". Потому что в том примере это по сути превращается в this.name. Но мы же не можем гарантировать в лямбде, что объект родитель этого name продолжает существовать? Можем, замыканием. Потому что это создаст скрытый объект замыкания со ссылкой на объект родитель этого поля. О чем и говорится в видео. И я обратил внимание на это, так как в этом плавают прям очень многие.
Замыкание - это не про боксинг. Замкание это гарантия того, что то, что находится в лямбде или анонимной функции на момент обращения к нему будет существовать (в рамках механизмов .Net). Ибо это вам не плюсы, которые позволят вам выстрелить себе в ногу, сделают Undefained Behavior и без логов ищите что происходит пару недель. .Net позволяет разработчику "ошибаться", но берет плату обычно памятью. А то что чаще всего во всех мануалах говорится про стек и о том, что там с value-type и локальными переменнами, это из-за непонимания сути работы этого механизма. Там даже не боксинг происходит в общем смысле на самом деле. Так как боксинг это обычно запаковка в класс object. А тут создается именно объект замыкания. Поэтому выделяется память, но это по своей сути не боксинг.
UPD: И да, не понимать это опасно именно из-за потенциальных утечек памяти со всякими удержаниями ссылочных типов. В сложной системе раскопать почему GC что-то не собирает, когда оно выделилось, потому что существует лямбда которая ещё жива, бывает достаточно трудоемко, даже когда ты знаешь про эту фишку. А когда ты об этой фишке не знаешь, то и вряд ли догадаешься.
UPD2: В случае с this не создается в современном .Net объект замыкания. Аллоцируется только лямбда. А оно реализуется через Generic Action. Как подсказали в комментах. Замыкание всё равно есть, просто реализовано по-другому. То ли я запутался, то ли в старых версиях .Net или Mono это работало иначе. Вот такая тема, и даже я не всё знаю. Люблю признавать какие-то ошибки. Но важно помнить, что с точки зрения главной проблемы (захвата ссылок) даже без доп объекта замыкание возникает :)
#интересное