访问修改后的闭包 (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)的主要内容,如果未能解决你的问题,请参考以下文章
Do.. While.. 使用 Exists 谓词。访问修改后的闭包?