【问题标题】:Access to Modified Closure (2)访问修改后的闭包 (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 的值可能已经迭代到字符串集合中的最后一个值。

标签: c# .net resharper closures


【解决方案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 中没有改变
猜你喜欢
  • 2011-09-29
  • 2011-07-28
  • 2015-11-08
  • 2012-09-14
  • 2010-12-13
  • 2016-05-31
  • 2011-12-18
  • 1970-01-01
相关资源
最近更新 更多