前回、「クロージャーに渡した変数がスコープ外になった時の値が表示される」とか書いたわけですが、それは本当なのか?
たとえば以下。
private void button1_Click(...) { int i=1; Action f = () => { MessageBox.Show(i.ToString()); }; // button1_Click内で実行 f.Invoke(); // button1_Click内の後で実行 Dispatcher.BeginInvoke(f); // i を変更 i = 10; }
これは、f.Invoke(); では 1 がちゃんと表示されます。Dispatcher.BeginInvoke(f); のほうは予定通り 10 で表示されます。
ではこれでは?
private Action f; private void button1_Click(...) { int i=0; f = () => { i += 1; MessageBox.Show(i.ToString()); }; } private void button2_Click(...) { f.Invoke(); } private void button3_Click(...) { Dispatcher.BeginInvoke(f); }
最初の1回のみ button1_Click() を実行し、button2_Click() , button3_Click() を適当に実行します。するとどうなるか?インクリメントしていってますよね?
要するに、キャプチャされた変数 (今回の場合は i ) は別に親のスコープから外れたときに固定化されるのではなく、
・変数の実体 (値) を参照化 (ボクシング処理) し、
・その参照を無名関数 (上記の場合、f) が所持する。
ということをしていそうです。
だからもっとひどいことをすればこんなこともできる。
private Action f1; private Action f2; private void button1_Click(...) { int i=0; f1 = () => { i += 1; MessageBox.Show(i.ToString()); }; f2= () => { i += 100; MessageBox.Show(i.ToString()); }; } private void button2_Click(...) { f1.Invoke(); } private void button3_Click(...) { Dispatcher.BeginInvoke(f2); }
関数を変えても、参照した変数 i は同じなので、f1 と f2 で i は共有されています。だから、同じ実体 (=i) に対して、f1 を実行すれば +1 , f2 を実行すれば +100 されていきます。