[C#] クロージャー怖い (その2)

2012年9月16日

 前回、「クロージャーに渡した変数がスコープ外になった時の値が表示される」とか書いたわけですが、それは本当なのか?

 たとえば以下。

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 されていきます。






タグ:
カテゴリー: C#, Program

Follow comments via the RSS Feed | Leave a comment | Trackback URL

コメントを投稿する

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


«   »
 
Powered by Wordpress and MySQL. Theme by Shlomi Noach, openark.org