C# 事件处理程序委托中的闭包? [复制]

Posted

技术标签:

【中文标题】C# 事件处理程序委托中的闭包? [复制]【英文标题】:Closures in C# event handler delegates? [duplicate] 【发布时间】:2010-02-09 03:13:12 【问题描述】:

目前我来自函数式编程背景,如果我不了解 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);

我希望文本 "I am button number " + i 在 for 循环的迭代中以 i 的值关闭。但是,当我实际运行程序时,每个 Button 都会显示I am button number 7。我错过了什么?我用的是VS2005。

编辑:所以我想我的下一个问题是,我如何捕获价值?

【问题讨论】:

你没有捕捉到价值。您永远不会捕获值,只会捕获变量。有关此问题的更多信息,请参阅 blogs.msdn.com/ericlippert/archive/2009/11/12/… 和 blogs.msdn.com/ericlippert/archive/2009/11/16/… 【参考方案1】:

要获得这种行为,您需要在本地复制变量,而不是使用迭代器:

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

更详细地讨论了推理in this question。

【讨论】:

【参考方案2】:

尼克说得对,但我想在这个问题的正文中更好地解释一下为什么

问题不在于关闭;这是for循环。该循环只为整个循环创建一个变量“i”。它不会为每次迭代创建一个新变量“i”。 注意:据报道,这在 C# 5 中有所改变。

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

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

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

我不太喜欢最后一个选项,因为它消除了循环,而是因为它让您开始思考如何从数据源构建此控件。

【讨论】:

这不是错误,但他们正在更改它。就像 Silverlight 是一个可行的框架一样(但可能永远不会获得任何新功能并且支持减少/停用)。【参考方案3】:

您创建了七个委托,但每个委托都持有对同一 i 实例的引用。

MessageBox.Show 函数仅在单击按钮时调用。单击按钮时,循环已完成。因此,此时 i 将等于 7。

试试这个:

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

【讨论】:

【参考方案4】:

闭包捕获的是变量而不是值。这意味着到执行委托时,即循环结束后的某个时间,i 的值是 6。

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

Jon Skeet 的articles on closures 有更深入的解释和更多示例。

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

【讨论】:

-1:一个更丰富的答案,深入解释了一个例子发生的事情应该被评为更高。【参考方案5】:

到你点击任意按钮的时候,它们都是从1到7生成的,所以它们都会表示i的最终状态,即7。

【讨论】:

以上是关于C# 事件处理程序委托中的闭包? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

C#里事件和委托有啥区别啊??

C#中的委托和事件(续)

C#中的委托

C# 事件(Event)

事件委托

浅谈JavaScript的事件(事件委托)