我遇到了一个关于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。
似乎是由于所有的动作都引用了一个捕获的变量。因此,当它们被调用时,它们都具有相同的输出。
有没有办法绕过这个限制,让每个动作实例都有自己的捕获变量?
是-获取循环内变量的副本:
while (variable < 5)
{
int copy = variable;
actions.Add(() => copy * 2);
++ variable;
}
您可以把它想像成C#编译器每次命中变量声明时都会创建一个“新的”局部变量。实际上,它将创建适当的新闭包对象,如果引用多个作用域中的变量,则会变得复杂(就实现而言),但它可以工作:)
请注意,更常见的此问题是使用foreach
或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
的行为发生了变化,因此您不再需要进行本地复制。请参阅此答案以了解更多详细信息。
我相信您正在经历的是所谓的关闭http://en.wikipedia.org/wiki/closal_(computer_science)。lamba有一个对作用域在函数本身之外的变量的引用。在调用lamba之前,它不会被解释,一旦被解释,它将获得变量在执行时所具有的值。
在幕后,编译器正在生成一个表示方法调用的闭包的类。它将closure类的单个实例用于循环的每次迭代。代码如下所示,这样更容易看出bug发生的原因:
void Main()
{
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
var closure = new CompilerGeneratedClosure();
Func<int> anonymousMethodAction = null;
while (closure.variable < 5)
{
if(anonymousMethodAction == null)
anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);
//we're re-adding the same function
actions.Add(anonymousMethodAction);
++closure.variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
}
class CompilerGeneratedClosure
{
public int variable;
public int YourAnonymousMethod()
{
return this.variable * 2;
}
}
这并不是示例中的编译代码,但我检查了我自己的代码,它看起来非常像编译器实际生成的代码。