Перед прочтением статьи, ответьте на следующий вопрос — что будет напечатано, после исполнения следующего кода?
P p = Console.WriteLine; // P объявлен как delegate void P();
foreach (var i in new [] { 1, 2, 3, 4 }) {
p += () => Console.Write(i);
}
p();
(К сожалению, не хватает кармы для нормального оформления)
Я провел опрос среди своих коллег, и только три человека из десяти смогли ответить правильно, причем только двое точно знали, что происходит и почему. Я, к своему стыду, правильного ответа не знал.
Так вот, переведенный выше код покажет
P p = Console.WriteLine;
foreach (var i in new int[] { 1, 2, 3, 4 }) {
int j = i;
p += () => Console.Write(j);
}
p();
… то результат будет
Наш анонимный метод (лямбда-выражение — это всего лишь «синтаксический сахар» для анонимных методов) использует в теле внешнюю переменную. Эта переменная становится захваченной (captured), и ее время жизни увеличивается до времени жизни делегата, которые ее использует. Это позволяет методу в принципе использовать значения захваченных переменных.
Теперь в дело вступают инстанциация (instantiation) переменных и их область видимости (scope). В первом случае, переменная
foreach (var i in new[] { 1, 2, 3, 4 }) {
//…
}
эквивалентен
{
int i;
foreach (i in new[] { 1, 2, 3, 4 }) {
//…
}
}
Во втором случае переменная
Все это становится вполне очевидно, если мы заглянем внутрь сгенерированного компилятором кода, например, при помощи Reflector. Скомпилированный код первого примера (после некоторого причесываения) выглядит вот так:
class DecodedFoo {
private delegate void P();
class Anonim {
public int i;
public void p()
{
Console.Write(i);
}
}
public void Print() {
P p = Console.WriteLine;
var a = new Anonim();
var array = new[] { 1, 2, 3, 4 };
for (var i = 0; i < array.Length; i++) {
a.i = array[i];
p += a.p;
}
p();
}
}
Весьма интересно, что компилятор развернул цикл
Второй пример будет выглядеть вот так:
class DecodedBar {
private delegate void P();
class Anonim {
public int j;
public void p() {
Console.Write(j);
}
}
public void Print() {
P p = Console.WriteLine;
foreach (var i in new [] { 1, 2, 3, 4 }) {
var a = new Anonim();
a.j = i;
p += a.p;
}
p();
}
}
Ссылки по теме для заинтересовавшихся:
http://blogs.msdn.com/abhinaba/archive/2005/10/18/482180.aspx
http://blogs.msdn.com/oldnewthing/archive/2006/08/02/686456.aspx
http://blogs.msdn.com/oldnewthing/archive/2006/08/03/687529.aspx
http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx
P.S. Resharper 4.0 умеет определять такие вот случаи, и для первого примера он выдает предупреждение «Access to modified closure» и предлагает переделать первый пример во второй. Но, однако, он не умеет отделять случаи, когда делегат вызывается внутри цикла, от слчуаев, когда делегат вызывается вне цикла.
P p = Console.WriteLine; // P объявлен как delegate void P();
foreach (var i in new [] { 1, 2, 3, 4 }) {
p += () => Console.Write(i);
}
p();
(К сожалению, не хватает кармы для нормального оформления)
Я провел опрос среди своих коллег, и только три человека из десяти смогли ответить правильно, причем только двое точно знали, что происходит и почему. Я, к своему стыду, правильного ответа не знал.
Так вот, переведенный выше код покажет
4444
. Однако, если этот код слегка изменить:P p = Console.WriteLine;
foreach (var i in new int[] { 1, 2, 3, 4 }) {
int j = i;
p += () => Console.Write(j);
}
p();
… то результат будет
1234
. Давайте разберемся, почему так происходит.Наш анонимный метод (лямбда-выражение — это всего лишь «синтаксический сахар» для анонимных методов) использует в теле внешнюю переменную. Эта переменная становится захваченной (captured), и ее время жизни увеличивается до времени жизни делегата, которые ее использует. Это позволяет методу в принципе использовать значения захваченных переменных.
Теперь в дело вступают инстанциация (instantiation) переменных и их область видимости (scope). В первом случае, переменная
i
инстанциируется один раз перед foreach
. Фактически кодforeach (var i in new[] { 1, 2, 3, 4 }) {
//…
}
эквивалентен
{
int i;
foreach (i in new[] { 1, 2, 3, 4 }) {
//…
}
}
Во втором случае переменная
j
создается и инстанциируется внутри цикла на каждой итерации. Переменные замыкаются в своей области видимости. Таким образом, в первом случае замкнутая переменная i
будет изменятся при каждой итерации и к концу цикла будет равна четырем. Именно поэтому делегат выведет четыре четверки. Во втором случае, j
будет замкнута внутри области видимости цикла и будет неизменна (фактически, будет созданно четыре экземпляра переменной j
, каждая из которых получит свое значение), и делегат выведет 1234
.Все это становится вполне очевидно, если мы заглянем внутрь сгенерированного компилятором кода, например, при помощи Reflector. Скомпилированный код первого примера (после некоторого причесываения) выглядит вот так:
class DecodedFoo {
private delegate void P();
class Anonim {
public int i;
public void p()
{
Console.Write(i);
}
}
public void Print() {
P p = Console.WriteLine;
var a = new Anonim();
var array = new[] { 1, 2, 3, 4 };
for (var i = 0; i < array.Length; i++) {
a.i = array[i];
p += a.p;
}
p();
}
}
Весьма интересно, что компилятор развернул цикл
foreach
в for
.Второй пример будет выглядеть вот так:
class DecodedBar {
private delegate void P();
class Anonim {
public int j;
public void p() {
Console.Write(j);
}
}
public void Print() {
P p = Console.WriteLine;
foreach (var i in new [] { 1, 2, 3, 4 }) {
var a = new Anonim();
a.j = i;
p += a.p;
}
p();
}
}
Ссылки по теме для заинтересовавшихся:
http://blogs.msdn.com/abhinaba/archive/2005/10/18/482180.aspx
http://blogs.msdn.com/oldnewthing/archive/2006/08/02/686456.aspx
http://blogs.msdn.com/oldnewthing/archive/2006/08/03/687529.aspx
http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx
P.S. Resharper 4.0 умеет определять такие вот случаи, и для первого примера он выдает предупреждение «Access to modified closure» и предлагает переделать первый пример во второй. Но, однако, он не умеет отделять случаи, когда делегат вызывается внутри цикла, от слчуаев, когда делегат вызывается вне цикла.