Closures in C# event handler delegates? [duplicate]

十年热恋 提交于 2019-11-30 03:08:00
Nick Craver

To get this behavior, you need to copy the variable locally, not use the iterator:

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);
}

The reasoning is discussed in much greater detail in this question.

Nick has it right, but I wanted to explain a little better in the text of this question exactly why.

The problem isn't the closure; it's the for-loop. The loop only creates one variable "i" for the entire loop. It does not create a new variable "i" for each iteration. Note: This has reportedly changed for C# 5.

This means when your anonymous delegate captures or closes over that "i" variable it's closing over one variable that is shared by all the buttons. By the time you actually get to click any of those buttons the loop has already finished incrementing that variable up to 7.

The one thing I might do differently from Nick's code is use a string for the inner variable and build all those strings up front rather than at button-press time, like so:

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

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

That just trades a little bit of memory (holding on to larger string variables instead of integers) for a little bit of cpu time later on... it depends on your application what matters more.

Another option is to not manually code the loop at all:

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(string.Format("I am button number {0}.", i));
    return b;
}).ToArray());

I like this last option not so much because it removes the loop but because it starts you thinking in terms of building this controls from a data source.

The closure captures the variable not the value. This means that by the time the delegate is executed, ie sometime after the end of the loop, the value of i is 6.

To capture a value, assign it to a variable declared in the loop body. On each iteration of the loop, a new instance will be created for each variable declared within it.

Jon Skeet's articles on closures has a deeper explanation and more examples.

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);
}

You have created seven delegates, but each delegate holds a reference to the same instance of i.

The MessageBox.Show function is only called when the button is clicked. By the time the button has clicked, the loop has completed. So, at this point i will be equaling seven.

Try this:

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); 
}

By the time you click any button, they have all been generated from 1 thru 7, so they will all express the final state of i which is 7.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!