我遇到了一个关于 C# 的有趣问题。我有如下代码。列表
我遇到了一个有关 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。但是,它实际上输出了五个 10。
看起来这是因为所有操作都引用一个捕获的变量。因此,当它们被调用时,它们都有相同的输出。
有没有办法解决这个限制,让每个动作实例都有自己的捕获变量?
触发此行为是因为您使用了 lambda 表达式, () => variable * 2
但其外部作用域 variable
并未在 lambda 的内部作用域中实际定义。
Lambda 表达式(在 C#3+ 中,以及在 C#2 中为匿名方法)仍会创建实际方法。将变量传递给这些方法会遇到一些难题(按值传递?按引用传递?C# 采用按引用传递 - 但这又带来了另一个问题,即引用可能比实际变量存在的时间更长)。C# 解决所有这些难题的方法是创建一个新的辅助类(“闭包”),其中字段对应于 lambda 表达式中使用的局部变量,方法对应于实际的 lambda 方法。 variable
代码中的任何更改实际上都会转化为该代码中的更改 ClosureClass.variable
因此,您的 while 循环会不断更新, ClosureClass.variable
直到达到 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;
}
或者使用实际的辅助类:
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 表达式使用局部范围变量有关 - 尽管一些不谨慎的循环使用会展示闭包陷阱。