Добавить в корзинуПозвонить
Найти в Дзене

Давайте подробнее поговорим про замыкание

И начнем мы с определения. Что такое замыкание? Замыкание (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) p

Давайте подробнее поговорим про замыкание

И начнем мы с определения. Что такое замыкание?

Замыкание (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 это работало иначе. Вот такая тема, и даже я не всё знаю. Люблю признавать какие-то ошибки. Но важно помнить, что с точки зрения главной проблемы (захвата ссылок) даже без доп объекта замыкание возникает :)

#интересное