C# 5.0 epic breaking change
Да, это случилось. В последней версии компилятора C# из состава Visual Studio “11” Beta изменилось правило разворачивания компилятором цикла foreach
:
foreach (T x in xs) {
f(x);
}
Цикл foreach
- достаточно нетривиальная конструкция и имеет множество нюансов, я привожу лишь самый обычный случай, когда xs
является переменной типа IEnumerable<T>
. Раньше такой цикл компилировался прибилизительно следующим образом:
{
IEnumerator<T> e = ((IEnumerable<T>) xs).GetEnumerator();
try {
T t;
while (e.MoveNext()) {
t = e.Current;
f(t);
}
}
finally {
((IDisposable)e).Dispose();
}
}
Теперь компилируется вот так:
{
IEnumerator<T> e = ((IEnumerable<T>) xs).GetEnumerator();
try {
while (e.MoveNext()) {
T t = e.Current;
f(t);
}
}
finally {
((IDisposable)e).Dispose();
}
}
То есть декларация переменной итерации уехала внутрь цикла while
и это оказывает влияние на то, как теперь происходит замыкание на переменную итерации foreach
. Так как замыкание в C# происходит по ссылке (замыкание на сами переменные, а не на их значения), то все анонимные методы и лямбда-выражения замыкаются на одну и ту же переменную и если их исполнение будет отложено за пределы цикла, то они “увидят” в переменной значение на момент последней итерации цикла, а не на момент создания анонимного метода/лямбда-выражения. Не смотря на то, что поведение чётко описано в спецификации и в некотором роде имеет право на существование, среднестатический юзер C# всё-же упёрто ожидает, что на каждой итерации цикла будет замыкаться на “fresh variable”. Ходят шутки, что изменение этого поведение убрало бы на StackOverflow добрую треть вопросов по C#.
Видимо, юзеры настолько задолбали C# team, что те решились на такой достаточно серьёзный breaking change. Однако факт остаётся фактом, теперь этот код:
using System;
using System.Collections.Generic;
using System.Linq;
static class Program {
static void Main() {
var xs = new List<Action>();
foreach (var i in Enumerable.Range(1, 3)) {
xs.Add(() => Console.WriteLine(i));
}
foreach (var action in xs) {
action();
}
}
}
Выводит на экран ожидаемые:
1
2
3
Думаю, что решающим был тот факт, что сложно придумать пример, в котором предыдущее поведение было бы осмысленным в случае отложенного исполнения замыкания - в 99% случаях такие замыкания просто являются ошибками. Будем надеяться, что с таким изменением язык C# станет более логичным для пользователя, а скрытые ошибки, связанные с замыканием на переменную итерации foreach
, сами пофиксятся, когда проекты будут собирать компилятором C# версии 5.0.
p.s. Эти изменения никак не затрагивают цикл for
, в котором присутствует подобная проблема, так как цикл for
на самом деле совсем не связан с переменными, которые могут определять (а могут и не определять) в части инициализации цикла.