以下のコードは、WPF系のフレームワーク(WPF/Windows Phone/Win8) を想定しています。Winforms や Consoleでは動作しないので注意。
C# や VB.net で 無名関数(ラムダ式?) を作るとき、無名関数ではなく、無名関数を定義した側の変数を参照することができます。クロージャーと呼ぶらしいです。例えば以下。
private void button1_Click(...) { int i = 1; Dispatcher.BeginInvoke ( () => { MessageBox.Show(i.ToString()); } ); }
結果は 1 が表示されます。無名関数の部分は緑マーカーの部分ですが、赤マーカー部分の設定が表示されています。要するに(無名)関数の本来のスコープから外れたところの値を、引数で渡すとかすることなく、参照できてしまうわけです。
ところでこのクロージャー。便利ですが、いろいろ罠があります。
private void button1_Click(...) { for (int i=0; i<5; i+=1) { Dispatcher.BeginInvoke ( () => { MessageBox.Show(i.ToString()); } ); } }
上記のコードの場合、for で 5回ループさせて、そのループ変数をさせようとしたつもりなので、結果は 0→1→2→3→4 と表示されそうですが、そうはなりません。 5回表示されますが、表示される数字は全て 5 です。
なんでさ。
また、上記のコードの for が抜けてから i = 10; を入れると、5 回の表示すべてが 10 となってしまいます。
private void button1_Click(...) { int i; for (i = 0; i < 5; i += 1) { Dispatcher.BeginInvoke( () => { MessageBox.Show(i.ToString()); } ); } i = 10; }
for の外で代入した数値がなぜ!!
そもそも、当初のもくろみとしては 0→1→2→3→4 と表示したかったはずです。クロージャーを使うと実現できないのでしょうか。
できます。
以下のように書けばいいです。
private void button1_Click(...) { for (int i = 0; i < 5; i += 1) { int a = i; Dispatcher.BeginInvoke( () => { MessageBox.Show(a.ToString()); } ); } }
この結果からみると、要するに、クロージャーに渡した変数がスコープ外になった時の値が表示される。ということがいえそうです。(*1)
(*1)
最後のコードで、for の ループ変数 i と、for ループ内の int a のスコープは違います。
ループ変数 i は次回ループでもそのインスタンスが継続して使用されますが、a のほうはループごとに新しいインスタンスが生成されている点に注意です。