我遇到了一个关于C#的有趣问题.我有如下代码.
List> actions = new List >(); 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节,关于闭包的文章也有更多示例.
我相信你所经历的就是Closure http://en.wikipedia.org/wiki/Closure_(computer_science).您的lamba引用了一个变量,该变量在函数本身之外.在您调用lamba之前,不会对其进行解释.一旦它被调用,它将获得变量在执行时具有的值.
在幕后,编译器生成一个表示方法调用闭包的类.它为循环的每次迭代使用闭包类的单个实例.代码看起来像这样,这使得更容易看到错误发生的原因:
void Main() { List> actions = new List >(); int variable = 0; var closure = new CompilerGeneratedClosure(); Func anonymousMethodAction = null; while (closure.variable < 5) { if(anonymousMethodAction == null) anonymousMethodAction = new Func (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; } }
这实际上不是您的示例中的已编译代码,但我已经检查了自己的代码,这看起来非常类似于编译器实际生成的内容.
解决这个问题的方法是在代理变量中存储您需要的值,并捕获该变量.
IE
while( variable < 5 ) { int copy = variable; actions.Add( () => copy * 2 ); ++variable; }
是的,您需要variable
确定循环范围,并以这种方式将其传递给lambda:
List> actions = new List >(); int variable = 0; while (variable < 5) { int variable1 = variable; actions.Add(() => variable1 * 2); ++variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } Console.ReadLine();
在多线程(C#、. NET 4.0)中也发生了相同的情况。
请参见以下代码:
目的是依次打印1,2,3,4,5。
for (int counter = 1; counter <= 5; counter++) { new Thread (() => Console.Write (counter)).Start(); }
输出很有趣!(可能像21334 ...)
唯一的解决方案是使用局部变量。
for (int counter = 1; counter <= 5; counter++) { int localVar= counter; new Thread (() => Console.Write (localVar)).Start(); }
触发此行为的原因是,您使用的Lambda表达式() => variable * 2
的外部作用域variable
实际上未在Lambda的内部作用域中定义。
Lambda表达式(在C#3 +中以及在C#2中为匿名方法)仍会创建实际方法。将变量传递给这些方法会遇到一些难题(按值传递吗?按引用传递?C#随引用一起使用-但这带来了另一个问题,即引用可能会超过实际变量的寿命)。C#解决所有这些难题的方法是创建一个新的帮助器类(“ closure”),该类具有与lambda表达式中使用的局部变量相对应的字段,以及与实际lambda方法相对应的方法。variable
您对代码的任何更改实际上都会翻译为更改ClosureClass.variable
因此,您的while循环会不断更新ClosureClass.variable
直到达到10,然后您才能让for循环执行操作,所有操作都在同一上进行ClosureClass.variable
。
为了获得预期的结果,您需要在循环变量和要关闭的变量之间创建一个分隔。您可以通过引入另一个变量来做到这一点,即:
List> actions = new List >(); 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> actions = new List >(); int variable = 0; while (variable < 5) { actions.Add(Mult(variable)); ++variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); }
您可以将Mult实现为lambda表达式(隐式关闭)
static FuncMult(int i) { return () => i * 2; }
或使用实际的辅助课程:
public class Helper { public int _i; public Helper(int i) { _i = i; } public int Method() { return _i * 2; } } static FuncMult(int i) { Helper help = new Helper(i); return help.Method; }
在任何情况下,“闭包”都不是与循环相关的概念,而是与局部范围变量使用的匿名方法/ lambda表达式有关,尽管对循环的某些谨慎使用显示了闭包陷阱。