匿名メソッドの仕組み
C#2.0の匿名メソッドは直感的でない動作をするので、あちこちで議論になります。原因の1つにコンパイラがソースコードからは想像がつかないコードを生成していることが挙げられると思います。コンパイラがかなりのコードを生成していますが、大きく分けて3つのパターンあって、それらが分かると匿名メソッドを理解しやすくなります。
まずは、ソースコードとしては一番単純な(一番直感的では無い)例から。
using System; public class App { delegate void D (); public static void Main () { D[] ds = new D [10]; for (int i = 0; i < ds.Length; ++i) { ds [i] = delegate { Console.Write ("{0} ", i); }; } foreach (D d in ds) d (); } }
C#2.0の匿名メソッドを知らない人に見せて結果を聞くと、まず、
0 1 2 3 4 5 6 7 8 9
と答えが返るでしょう。ところが結果は、
10 10 10 10 10 10 10 10 10 10
です。
この問題を解く鍵は匿名メソッド内で使用している変数iにあります。
iが生成されるタイミングはループの最初の1回だけです。匿名メソッドが(ソースコード上では)ループの度に生成されるのとは対照的です。
ところで、匿名メソッドとは何でしょうか? 簡単に言うと名前の無い関数です。アレ?、ちょっと待ってください。C#はクラスに属さない関数なんて定義出来ませんよね。匿名メソッドも例外に漏れず、ちゃんとクラスに属しています。実は、匿名メソッドはメソッドだけでなく匿名なクラスも生成していたのです。
つまり、先ほどの匿名メソッドは、
private sealed class Foo { public int i; public void Bar () { Console.WriteLine ("{0} ", i); } }
このようなクラスに変換され、ループはこのように書き換えられます。
Foo foo = new Foo (); for (foo.i = 0; foo.i < ds.Length; ++foo.i) ds[foo.i] = new D (foo.Bar); }
匿名クラスFooがループの外で1回だけ生成されていることに注意してください。
匿名メソッドが使っていた変数iがスコープ内で1度だけした生成されないため、コンパイラは匿名クラスのインスタンスは1つあれば良いという判断を下すようです。なので、
foreach (D d in ds) d ();
とかしても、呼び出されるFoo.Barのインスタンスは1つだけであり、メンバiの値は10です。よって、
10 10 10 10 10 10 10 10 10 10
このような結果になるのでした。
次のパターンは匿名メソッドで使用している変数が、匿名メソッドと同じスコープを持つ場合です。
for (int i = 0; i < ds.Length; ++i) { int j = i; ds [i] = delegate { Console.Write ("{0} ", j); }; }
匿名クラス/メソッド内で使っている変数がjになった以外はほとんど同じですね。
private sealed class Foo { public int j; public void Bar () { Console.WriteLine ("{0} ", j); } }
さて、ここからが問題です。変数jはループの度に生成されています。つまり、匿名クラスFooのインスタンスもループの度に生成する必要があります。
for (int i = 0; i < ds.Length; ++i) { Foo foo = new Foo (); foo.j = i; ds[i] = new D (foo.j); }
この場合、Fooのインスタンスは10生成され、各々のjの値は異なります。なので結果は当然、
0 1 2 3 4 5 6 7 8 9
となります。
3つ目は、両者の混在です。
for (int i = 0; i < ds.Length; ++i) { int j = i; ds[i] = delegate { Console.WriteLine ("{0} ", i + j); }; }
変数iについてはインスタンスは1つですが、変数jについてはインスタンスを10生成しなければなりません。
class Foo { public int i; public int j; public void Bar () { ... } }
では上手く行かなさそうです。実は、この場合、匿名クラスが2つ生成されるのです。
class Baz { public int i; } class Bar { public int j; public Baz baz; public void Bar () { Console.Write ("{0} ", baz.i + j); } }
で、この2つのクラスで置き換えると、
Baz baz = new Baz (); for (baz.i = 0; baz.i < ds.Length; ++baz.i) { Foo foo = new Foo (); foo.baz = baz; foo.j = baz.i; ds[baz.i] = new D (foo.Bar); }
となります。
分かってしまえは単純ですが、ソースコードからは想像がつかないし、ちょっと癖がありますよね。