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但是,当我实际运行程序时,每个 Button 都会显示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循环。该循环只为整个循环创建一个变量“i”。它不会为每次迭代创建一个新变量“i”。 注意:据报道,这已针对 C# 5 进行了更改。

这意味着当您的匿名委托捕获或关闭该“i”变量时,它正在关闭一个由所有按钮共享的变量。当您实际单击这些按钮中的任何一个时,循环已经完成将该变量增加到 7。

我可能做的与尼克的代码不同的一件事是使用字符串作为内部变量并在前面而不是在按钮按下时构建所有这些字符串,如下所示:

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 时间......这取决于您的应用程序,更重要的是。

另一种选择是根本不手动编码循环:

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函数仅在单击按钮时调用。单击按钮时,循环已完成。所以,此时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。

要捕获一个值,请将其分配给循环体中声明的变量。在循环的每次迭代中,将为其中声明的每个变量创建一个新实例。

Jon Skeet关于闭包的文章有更深入的解释和更多的例子。

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 回答