我遇到了一个关于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。
这似乎是由于所有的操作都指向一个捕获的变量。因此,当调用它们时,它们都有相同的输出。
有没有办法绕过这个限制,让每个动作实例都有自己的捕获变量?
这和循环没有关系。
触发此行为是因为您使用了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表达式有关——尽管一些不小心使用循环会显示闭包陷阱。