Как поток может получить доступ к локальной переменной даже после завершения метода?

Скажем, у меня есть метод С#, подобный этому

public void MyMethod()
    {
        int i = 0;

        var thread = new Thread(() =>
        {
            Thread.Sleep(100);

            if (i == 0)
            {
                Console.WriteLine("Value not changed and is {0}", i);
            }
            else
            {
                Console.WriteLine(" Value changed to {0}.", i);
            }
        });

        thread.Start();


        i = 1;
    }

Здесь метод создает поток, который обращается к локальной переменной, созданной в методе. К моменту обращения к этой переменной метод завершился, и поэтому локальная переменная я не должна существовать. Но код работает без проблем. По моему пониманию, локальная переменная не существует после завершения блока метода. Я не могу это получить.

Ответ 1

В соответствии с моим пониманием локальные переменные не существуют после завершения блока метода. Я не могу это получить.

Ваше понимание просто неверно. Определяющей характеристикой локальной переменной является то, что ее имя находится только в области видимости внутри блока объявления. Вот почему он называется "локальной" переменной - потому что его имя видно только локально.

Вы верите в ложь, что "локальный" означает "недолговечный". Это неверно; локальная переменная недолговечна, когда она может быть, но есть три ситуации, в которых она не может быть: когда это закрытая внешняя переменная анонимной функции, когда она находится в блоке итератора или когда она находится в асинхронный блок.

Кстати, ваш вопрос был предметом моего блога на прошлой неделе; см. подробнее:

http://blogs.msdn.com/b/ericlippert/archive/2012/01/16/what-is-the-defining-characteristic-of-a-local-variable.aspx

Ответ 2

Это работает, потому что компилятор перезаписывает ваш код, чтобы использовать closure.

Поскольку вы используете переменную в лямбда, переменная заканчивается тем, что она становится членом класса. Скомпилированный код не содержит локальную переменную для я - даже если вы написали ее таким образом. Вместо этого он перезаписывает ваш код, чтобы использовать класс, сгенерированный компилятором, который содержит Int32 в качестве переменной-члена, а вместо этого локальный код, а также лямбда, относятся к этому классу.

Подробнее см. в этом сообщении в блоге о закрытии, которое дает вам грубое представление о том, что здесь делает компилятор.

Ответ 3

Это называется closure.
Он расширяет локальные переменные за время жизни метода.

Ответ 4

Выражение лямбда, которое вы запустили как новый поток, создает closure на i; поскольку закрытие закрывается по переменной, а не по значению, поток ссылается на тот же объект, что и внешняя область, и сможет получить доступ к своим переменным даже после того, как закончилась внешняя область.

Ответ 5

Компилятор С# обычно преобразует код С# в промежуточный "язык", называемый MSIL, который, как и С#, имеет локальные переменные. Локальные переменные в MSIL ведут себя так, как вы ожидаете, что переменные С# ведут себя: они перестают существовать, когда процедура, в которой они определены, завершается. Кроме того, когда код С#, который использует локальные переменные, преобразуется в код MSIL, который использует локальные переменные, эти переменные С# ведут себя как MSIL: они перестают существовать, когда функция определения завершается. Однако не все С# -код, который использует локальные переменные, использует локальные переменные MSIL для их хранения.

В различных ситуациях компилятор принимает код, который написан для использования локальных переменных и перед его переносом в MSIL переписывает его так, чтобы он определял новый объект класса, который содержит поля для рассматриваемых переменных, а затем переписывает обращения к этим переменным, чтобы они вместо этого обращались к новым полям. Если "переменные" используются делегатом, делегату будет дана ссылка на этот новый объект класса. Даже если функция, в которой объект был определен, выходит, сам объект будет продолжать существовать до тех пор, пока что-либо, включая любую копию делегата, содержит ссылку.

Правила, определяющие, когда компилятор использует локальные переменные MSIL и когда они переписывают вещи для использования полей классов, могут быть сложными, и это позволяет небольшим изменениям части подпрограммы изменить семантику других, казалось бы, несвязанных частей.