Как | Где хранятся закрытые переменные? - PullRequest
8 голосов
/ 17 ноября 2009

Это вопрос, основанный на статье Эрика Липперта "Закрытие по переменной цикла, которая считается вредной" . Это хорошее чтение, Эрик объясняет, почему после этого фрагмента кода все функции возвращают значение last в v:

 var funcs = new List<Func<int>>();
 foreach (var v in values)
 {
    funcs.Add(() => v);
 }

И правильная версия выглядит так:

 foreach (var v in values)
 {
    int v2 = v;
    funcs.Add(() => v2);
 }

Теперь мой вопрос: как и где хранятся эти захваченные переменные 'v2'? В моем понимании стека все эти переменные v2 занимают один и тот же кусок памяти.

Моей первой мыслью был бокс, каждый член func имел ссылку на коробочную версию v2. Но это не объясняет первый случай.

Ответы [ 2 ]

6 голосов
/ 17 ноября 2009

Обычно переменной v2 выделяется некоторое пространство в стеке в начале кодового блока, в котором она найдена. В конце кодового блока (т. Е. В конце итерации) стек возвращается назад (I Описываю логический сценарий, а не оптимизированное реальное поведение). Следовательно, каждый v2 в действительности отличается от v2 от предыдущей итерации, хотя верно то, что он в конечном итоге займет одну и ту же ячейку памяти.

Однако компилятор обнаруживает, что v2 используется анонимной функцией, созданной лямбда-выражением. Компилятор делает hoist переменная v2. Компилятор создает новый класс с полем Int32 для хранения значения v2, ему не выделяется место в стеке. Это также делает анонимную функцию методом этого нового типа. (Для простоты я назову этот безымянный класс именем, назовем его «Вещь»).

Теперь в каждой итерации создается новый экземпляр "Thing", и когда v2 назначается его поле Int32, которое фактически назначается не просто точке в памяти стека , Выражение анонимной функции (лямбда) теперь будет возвращать делегата, который имеет ненулевую ссылку на объект экземпляра, эта ссылка будет на текущий экземпляр из «Вещи».

Когда вызывается делегат для анонимной функции, он будет выполняться как метод экземпляра экземпляра Thing. Следовательно, v2 доступно в качестве поля-члена и будет иметь значение, которое оно будет давать во время итерации, когда был создан этот экземпляр «Вещи».

4 голосов
/ 17 ноября 2009

В дополнение к ответам Нила и Энтони, вот пример кода, который может быть сгенерирован автоматически в обоих случаях.

(Обратите внимание, что это только для демонстрации принципа, фактический код, сгенерированный компилятором, не будет выглядеть точно так же. Если вы хотите увидеть реальный код, вы можете посмотреть с помощью Reflector.)

// first loop
var captures = new Captures();
foreach (var v in values)
{
    captures.Value = v;
    funcs.Add(captures.Function);
}

// second loop
foreach (var v in values)
{
    var captures = new Captures();
    captures.Value = v;
    funcs.Add(captures.Function);
}

// ...

private class Captures
{
    public int Value;

    public int Function()
    {
        return Value;
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...