23

私は現在関数型プログラミングのバックグラウンドを持っているので、C#のクロージャを理解していない場合はご容赦ください。

匿名のイベントハンドラーを取得するボタンを動的に生成するための次のコードがあります。

for (int i = 0; i < 7; i++)
{
    Button newButton = new Button();

    newButton.Text = "Click me!";

    newButton.Click += delegate(Object sender, EventArgs e)
    {
        MessageBox.Show("I am button number " + i);
    };

    this.Controls.Add(newButton);
}

forループのその反復での値でテキスト"I am button number " + iが閉じられることを期待していました。iただし、実際にプログラムを実行すると、すべてのボタンに「」と表示されI am button number 7ます。私は何が欠けていますか?VS2005を使用しています。

編集:それで、私の次の質問は、どうすれば価値を捉えることができるかということだと思います。

4

5 に答える 5

27

この動作を得るには、イテレータを使用せずに、変数をローカルにコピーする必要があります。

for (int i = 0; i < 7; i++)
{
    var inneri = i;
    Button newButton = new Button();
    newButton.Text = "Click me!";
    newButton.Click += delegate(Object sender, EventArgs e)
    {
        MessageBox.Show("I am button number " + inneri);
    };
    this.Controls.Add(newButton);
}

この質問では、その理由についてさらに詳しく説明します。

于 2010-02-09T03:17:18.410 に答える
23

ニックはそれを正しく理解していますが、この質問のテキストで、その理由をもう少し詳しく説明したいと思います。

問題は閉鎖ではありません。これはforループです。ループは、ループ全体に対して1つの変数「i」のみを作成します。反復ごとに新しい変数「i」は作成されません。 注:これはC#5で変更されたと報告されています。

これは、匿名デリゲートがその「i」変数をキャプチャまたはクローズすると、すべてのボタンで共有されている1つの変数をクローズすることを意味します。これらのボタンのいずれかを実際にクリックするまでに、ループはその変数を7までインクリメントし終えています。

Nickのコードとは異なる方法の1つは、内部変数に文字列を使用し、ボタンを押したときではなく、すべての文字列を前もって作成することです。

for (int i = 0; i < 7; i++)
{
    var message = $"I am button number {i}.";

    Button newButton = new Button();
    newButton.Text = "Click me!";
    newButton.Click += delegate(Object sender, EventArgs e)
    {
        MessageBox.Show(message);
    };
    this.Controls.Add(newButton);
}

これは、後で少しのcpu時間と少しのメモリ(整数ではなく大きな文字列変数を保持する)を交換するだけです...それはアプリケーションによって異なります。

もう1つのオプションは、ループを手動でコーディングしないことです。

this.Controls.AddRange(Enumerable.Range(0,7).Select(i => 
{ 
    var b = new Button() {Text = "Click me!", Top = i * 20};
    b.Click += (s,e) => MessageBox.Show($"I am button number {i}.");
    return b;
}).ToArray());

この最後のオプションは、ループを削除するのでそれほど好きではありませんが、データソースからこのコントロールを構築するという観点から考え始めるからです。

于 2010-02-09T04:09:57.923 に答える
5

7つのデリゲートを作成しましたが、各デリゲートはiの同じインスタンスへの参照を保持しています。

この関数は、ボタンがクリックされたときにMessageBox.Showのみ呼び出されます。ボタンがクリックされるまでに、ループは完了しています。したがって、この時点で7になります。i

これを試して:

for (int i = 0; i < 7; i++) 
{ 

    Button newButton = new Button(); 

    newButton.Text = "Click me!"; 

    int iCopy = i; // There will be a new instance of this created each iteration
    newButton.Click += delegate(Object sender, EventArgs e) 
    { 
        MessageBox.Show("I am button number " + iCopy); 
    }; 

    this.Controls.Add(newButton); 
}
于 2010-02-09T03:19:18.767 に答える
4

クロージャは、値ではなく変数をキャプチャします。これは、デリゲートが実行されるまで、つまりループの終了後のある時点で、iの値が6であることを意味します。

値をキャプチャするには、ループ本体で宣言された変数に値を割り当てます。ループが繰り返されるたびに、その中で宣言された変数ごとに新しいインスタンスが作成されます。

クロージャーに関するJonSkeetの記事には、より深い説明とより多くの例があります。

for (int i = 0; i < 7; i++)
{
    var copy = i;

    Button newButton = new Button();

    newButton.Text = "Click me!";

    newButton.Click += delegate(Object sender, EventArgs e)
    {
        MessageBox.Show("I am button number " + copy);
    };

    this.Controls.Add(newButton);
}
于 2010-02-09T03:16:24.560 に答える
1

いずれかのボタンをクリックするまでに、それらはすべて1から7まで生成されているため、iの最終状態である7をすべて表します。

于 2010-02-09T03:17:32.950 に答える