访问修改后的闭包 (2)

Posted

技术标签:

【中文标题】访问修改后的闭包 (2)【英文标题】:Access to Modified Closure (2) 【发布时间】:2010-09-23 04:28:44 【问题描述】:

这是来自Access to Modified Closure 的问题的延伸。我只是想验证以下内容对于生产使用是否足够安全。

List<string> lists = new List<string>();
//Code to retrieve lists from DB    
foreach (string list in lists)

    Button btn = new Button();
    btn.Click += new EventHandler(delegate  MessageBox.Show(list); );

我每次启动时只运行一次上述操作。现在它似乎工作正常。正如 Jon 在某些情况下提到的违反直觉的结果。那么我在这里需要注意什么?如果列表运行不止一次可以吗?

【问题讨论】:

恭喜,您现在是 Resharper 文档的一部分。 confluence.jetbrains.net/display/ReSharper/… 这个很棘手,但上面的解释让我明白了:这看起来是正确的,但实际上,只要有任何按钮,都会使用 str 变量的最后一个值被点击。这样做的原因是 foreach 展开到一个 while 循环中,但是迭代变量是在这个循环之外定义的。这意味着当您显示消息框时,str 的值可能已经迭代到字符串集合中的最后一个值。 【参考方案1】:

在 C# 5 之前,您需要在 foreach 中重新声明一个变量 inside - 否则它是共享的,并且您的所有处理程序都将使用最后一个字符串:

foreach (string list in lists)

    string tmp = list;
    Button btn = new Button();
    btn.Click += new EventHandler(delegate  MessageBox.Show(tmp); );

值得注意的是,请注意,从 C# 5 开始,这种情况发生了变化,特别是在 foreach 的情况下,您不再需要这样做:问题中的代码可以工作正如预期的那样。

要表明如果不进行此更改就无法正常工作,请考虑以下事项:

string[] names =  "Fred", "Barney", "Betty", "Wilma" ;
using (Form form = new Form())

    foreach (string name in names)
    
        Button btn = new Button();
        btn.Text = name;
        btn.Click += delegate
        
            MessageBox.Show(form, name);
        ;
        btn.Dock = DockStyle.Top;
        form.Controls.Add(btn);
    
    Application.Run(form);

运行上述C# 5 之前的,虽然每个按钮显示不同的名称,但单击按钮会显示四次“Wilma”。

这是因为语言规范 (ECMA 334 v4, 15.8.4)(在 C# 5 之前)定义:

foreach (V v in x) embedded-statement 然后扩展为:


    E e = ((C)(x)).GetEnumerator();
    try 
        V v;
         while (e.MoveNext()) 
            v = (V)(T)e.Current;
             embedded-statement
        
    
    finally 
        … // Dispose e
    

请注意,变量v(即您的list)是在循环外部声明的。所以根据捕获变量的规则,列表的所有迭代都将共享捕获的变量持有者。

从 C# 5 开始,这发生了变化:迭代变量 (v) 的作用域是在循环内。我没有规范参考,但基本上变成了:


    E e = ((C)(x)).GetEnumerator();
    try 
        while (e.MoveNext()) 
            V v = (V)(T)e.Current;
            embedded-statement
        
    
    finally 
        … // Dispose e
    


重新退订;如果您主动想取消订阅匿名处理程序,诀窍是捕获处理程序本身:

EventHandler foo = delegate ...code...;
obj.SomeEvent += foo;
...
obj.SomeEvent -= foo;

同样,如果您想要一个一次性事件处理程序(例如 Load 等):

EventHandler bar = null; // necessary for "definite assignment"
bar = delegate 
  // ... code
  obj.SomeEvent -= bar;
;
obj.SomeEvent += bar;

现在是自行退订;-p

【讨论】:

如果是这种情况,临时变量将保留在内存中,直到应用程序关闭,以便为委托提供服务,如果变量占用了非常大的循环,则不建议这样做很多内存。我说的对吗? 它将在内存中保留有事件(按钮)的时间长度。有一种方法可以取消订阅一次代表,我将在帖子中添加。 但要说明您的观点:是的,捕获的变量确实可以增加变量的范围。你需要小心不要捕捉到你没有预料到的东西...... 您能否就 C# 5.0 规范中的更改更新您的答案?只是为了使它成为有关 C# 中 foreach 循环的出色 wiki 文档。关于 C# 5.0 编译器处理 foreach 循环 bit.ly/WzBV3L 的变化,已经有一些很好的答案,但它们不是类似 wiki 的资源。 @Kos 是的,for 在 5.0 中没有改变

以上是关于访问修改后的闭包 (2)的主要内容,如果未能解决你的问题,请参考以下文章

访问修改后的闭包 - 为啥这是一个建议的修复?

访问修改后的闭包:ReSharper

如何升级到 C# 5.0?访问修改后的闭包

Do.. While.. 使用 Exists 谓词。访问修改后的闭包?

Python 2.x闭包(enclosure)中的变量访问&修改

在直接调用委托的情况下如何减轻“访问修改的闭包”