C# 5 での互換性のない変更

C#5 では、ループ変数とラムダ式の嫌な問題を一つ直すようです。

var values = new List<int>() { 0, 1, 2 };
var funcs = new List<Func<int>>();

foreach ( var v in values )
    funcs.Add( () => v );
foreach ( var f in funcs )
    Console.WriteLine( f() );

このコードを実行すると C#4 までは予想に反して「2 2 2」ですが、C#5 からは「0 1 2」となります。Windows 8 CP 上の Visual Studio 11 で試した結果です。C# の「破壊的」仕様変更であり、後方互換性がなくなります。
これまでも、この問題には簡単な回避策があり、ループ中で v をいったん var v2 = v; と別の変数に入れてラムダ式から v2 を参照すれば、まともに動きます。しかし、これまでの動作は誰も得しないので仕様を変更してまで修正するようです。

foreach はコンパイラによって while を使ったコードに展開されますが、このときループの外で変数を定義していたのを、ループの中で定義することで修正します。Eric Lippert さんの blog から引用します。

{
    IEnumerator<int> e = ((IEnumerable<int>)values).GetEnumerator();
    try
    {
      int m; // OUTSIDE THE ACTUAL LOOP
      while(e.MoveNext())
      {
        m = (int)(int)e.Current;
        funcs.Add(()=>m);
      }
    }
    finally
    {
      if (e != null) ((IDisposable)e).Dispose();
    }
}

この int m; を while の中へ。

    try
    {
      while(e.MoveNext())
      {
        int m; // INSIDE
        m = (int)(int)e.Current;
        funcs.Add(()=>m);
      }

従来は一つだけの変数の寿命が伸びていたので最後の値が見えていました。これからは、ループのたびに変数が作られ、それぞれの寿命が伸びることで解決します。

しかし! for は変更なしです。

var funcs2 = new List<Func<int>>();

for ( int i = 0; i < 3; i++ )
    funcs2.Add( () => i );
foreach ( var f in funcs2 )
    Console.WriteLine( f() );

この結果は「3 3 3」で変わりません(「2 2 2」でもありませんw)。


Eric Lippert さんが挙げる foreach の変更の悪い点。

  1. 互換性がなくなる破壊的変更であること。
  2. foreach の構文に矛盾しているように見えること。foreach ( int x in M() ) の右側にある M() より、左にある int x が内側にあるような感じ。左から右という一般的な規則に反する。
  3. foreach と for で一貫性がなくなる。
  4. 簡単な回避策があるのに破壊的変更は必要?

それでも変更するようです。


参考、および引用元:


おまけ。Eric さんの blog の最後の方にある pros or cons (pros and cons) とは、アメリカの子供が学校で習う考える訓練方法で、紙の真ん中に線をひき、左と右に良い点悪い点を書き出して考える方法だそうです。真ん中辺りの画像が参考になります。