我遇到了一个关于c#的有趣问题。我有如下代码。
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
actions.Add(() => variable * 2);
++ variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
我期望它输出0 2 4 6 8。然而,它实际上输出了5个10。
这似乎是由于所有的操作都指向一个捕获的变量。因此,当调用它们时,它们都有相同的输出。
有没有办法绕过这个限制,让每个动作实例都有自己的捕获变量?
是的-在循环中获取变量的副本:
while (variable < 5)
{
int copy = variable;
actions.Add(() => copy * 2);
++ variable;
}
你可以把它想象成c#编译器每次碰到变量声明时都会创建一个“新的”局部变量。事实上,它会创建适当的新闭包对象,如果你在多个作用域中引用变量,它会变得复杂(在实现方面),但它是有效的:)
注意,这个问题更常见的用法是for或foreach:
for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud
有关这方面的更多细节,请参阅c# 3.0规范的7.14.4.2节,我关于闭包的文章也有更多示例。
请注意,从c# 5编译器开始(甚至在指定较早版本的c#时),foreach的行为发生了变化,因此您不再需要进行本地复制。更多细节请看这个答案。
这和循环没有关系。
触发此行为是因为您使用了lambda expression() =>变量* 2,其中外部作用域变量实际上没有在lambda的内部作用域中定义。
Lambda expressions (in C#3+, as well as anonymous methods in C#2) still create actual methods. Passing variables to these methods involve some dilemmas (pass by value? pass by reference? C# goes with by reference - but this opens another problem where the reference can outlive the actual variable). What C# does to resolve all these dilemmas is to create a new helper class ("closure") with fields corresponding to the local variables used in the lambda expressions, and methods corresponding to the actual lambda methods. Any changes to variable in your code is actually translated to change in that ClosureClass.variable
所以你的while循环会不断更新ClosureClass。变量,直到它达到10,然后for循环执行操作,这些操作都操作在同一个ClosureClass.variable上。
为了得到预期的结果,您需要在循环变量和被封闭的变量之间创建一个分离。你可以通过引入另一个变量来做到这一点,即:
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
var t = variable; // now t will be closured (i.e. replaced by a field in the new class)
actions.Add(() => t * 2);
++variable; // changing variable won't affect the closured variable t
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
你也可以将闭包移动到另一个方法来创建这种分离:
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
actions.Add(Mult(variable));
++variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
您可以将Mult实现为lambda表达式(隐式闭包)
static Func<int> Mult(int i)
{
return () => i * 2;
}
或者使用一个实际的helper类:
public class Helper
{
public int _i;
public Helper(int i)
{
_i = i;
}
public int Method()
{
return _i * 2;
}
}
static Func<int> Mult(int i)
{
Helper help = new Helper(i);
return help.Method;
}
在任何情况下,“闭包”都不是一个与循环相关的概念,而是与使用局部作用域变量的匿名方法/ lambda表达式有关——尽管一些不小心使用循环会显示闭包陷阱。