Страницы

Поиск по вопросам

пятница, 31 января 2020 г.

Как увидеть захваченную переменую?

#c_sharp


Читая Герберта Шилдта, дошел до раздела "Применение внешних переменных в анонимных
методах" где в частности сказано:


  Захваченная переменная существует до тех пор, пока захвативший ее делегат не будет
собран в "мусор".


А как мне в этом можно убедится? В том, что переменная действительно существует после
выхода из кодового блока. Отладчик в Visual Studio говорит:


  error CS0103: The name 'variable' does not exist in the current context.


Вот пример кода:

class Program
{
    delegate void MyDelegate();

    static void Main(string[] args)
    {
        Test();
    }

    static void Test()
    {
        int i;
        MyDelegate myDelegate = delegate { i = 10; Console.WriteLine(i.ToString()); };
        myDelegate();
    }
}


Тут переменная i будет жить до сборки мусора.
    


Ответы

Ответ 1



Переменная и область видимости - это понятия уровня языка, а не уровня рантайма. После IL-компиляции переменная превращается в одну из двух вещей: - значение в стеке - поле класса-замыкания После JIT-компиляции и значения в стеке, и значения полей замыкания могут местами кешироваться еще и в регистрах процессора. То, что вы хотите проверить - это существование класса-замыкания в куче после того, управление покинуло область видимости локальной переменной. Можно проверить студией, или любым другим отладчиком, позволяющим смотреть содержимое кучи: static void Main(string[] args) { Debugger.Break(); { string var1 = "somevalue"; Action a = () => { var1 = "newvalue"; }; } Debugger.Break(); } после остановки на первом брейке открыть Debug -> Show Diagnostic Tools, и нажать Memory Usage / Take Snapshot. продолжить выполнение, на втором брейке нажать Take Snapshot еще раз. И сравнить снапшоты: Видно что и объект замыкания и локальная переменная еще живы, кроме того, в path to root для обоих показывается что они являются локальными переменными метода. Картина чуть поменяется при запуске в Release не под отладчиком. Следующий код покажет нулевой дифф - оптимизатор просто выбросит все неиспользуемое: Debugger.Launch(); { string var1 = "somevalue"; Action a = () => { var1 = "newvalue"; }; } Debugger.Break(); А вот если вы продлите время жизни Action, то сможете увидеть что объект-замыкание жив, хотя переменная уже недоступна: static void Main(string[] args) { Action a; Debugger.Launch(); { string var1 = "somevalue"; a = () => { var1 = "newvalue"; }; } Debugger.Break(); a(); }

Ответ 2



"Захваченная переменная существует до тех пор, пока захвативший ее делегат не будет собран в "мусор"" всего лишь означает, что, как и в абсолютно всех других случаях, если объект может быть хоть каким-то образом достигнут (в данном случае через замыкание), то он не будет собран сборщиком мусора. Это работает абсолютно всегда, и замыкания (которые представляют собой классы, где поля — все захваченные переменные) — не исключение. Проверить это не так просто, потому что, если ссылка на экземпляр замыкания — это единственный "маршрут" к объекту, то извлечь его можно только с помощью отражений. Но не будем шаманствовать, вместо этого проверим внешне наблюдаемое поведение на деле. Для начала заведём класс, который будет сообщать нам, что сборка мусора и последующая финализация до него добрались: using System; using static System.Console; class Named { string _name; public Named (string name) { _name = name; WriteLine($"{_name} created"); } ~Named () { WriteLine($"{_name} finalized"); } } А дальше проверяем: class Program { static void Main () => new Program().Run(); Func _act; void Run () { LocalVariable(); CollectGarbage(); LocalVariableUsedByAnonymousMethod(); CollectGarbage(); CallAnonymousDelegateAndForgetIt(); CollectGarbage(); WriteLine("Done!"); } void LocalVariable () { WriteLine("Local variable:"); var foo = new Named("Foo"); } void LocalVariableUsedByAnonymousMethod () { WriteLine("Local variable used by anonymous method:"); var bar = new Named("Bar"); _act = () => bar.ToString(); } void CallAnonymousDelegateAndForgetIt () { WriteLine($"Result of ToString: {_act()}"); _act = null; } void CollectGarbage () { WriteLine("Collecting garbage."); GC.Collect(); GC.WaitForPendingFinalizers(); WriteLine("Collected garbage."); } } Вывод: Local variable: Foo created Collecting garbage. Foo finalized Collected garbage. Local variable used by anonymous method: Bar created Collecting garbage. Collected garbage. Result of ToString: Named Collecting garbage. Bar finalized Collected garbage. Done! Как можно видеть, объект "Foo" был собран после выхода из метода, в котором был создан, а объект "Bar" просуществовал до момента, когда на него пропали все ссылки, в том числе из замыкания.

Ответ 3



Самая надежная проверка - декомпилятор. Например, ILSpy. Любое замыкание преобразуется компилятором во внутренний класс, а захваченные переменные становятся полями этого класса. В декомпиляторе этот класс можно увидеть, со всеми своими полями. PS при использовании ILSpy для того, чтобы увидеть сгенерированные классы-замыкания, надо выключить декомпиляцию замыканий в настройках. В противном случае декомпилятор их разпознает и скроет.

Комментариев нет:

Отправить комментарий