这是来自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在某些情况下提到的反直觉结果一样。那么我需要注意什么呢?如果列表不止一次运行会没问题吗?
答案 0 :(得分:158)
在C#5之前,你需要在foreach中重新声明一个变量 - 否则它是共享的,所有处理程序都将使用最后一个字符串:
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