我遇到了一个关于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秒。
似乎是由于所有操作都涉及一个捕获的变量。因此,当它们被调用时,它们都具有相同的输出。
有没有办法解决这个限制,让每个动作实例都有自己的捕获变量?
答案 0 :(得分:167)
是 - 在循环中获取变量的副本:
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节,我的article on closures也有更多示例。
答案 1 :(得分:19)
我相信你所经历的就是Closure http://en.wikipedia.org/wiki/Closure_(computer_science)。您的lamba引用了一个变量,该变量在函数本身之外。在您调用lamba之前,它不会被解释,一旦它被获取,它将获得变量在执行时具有的值。
答案 2 :(得分:9)
在幕后,编译器正在生成一个表示方法调用闭包的类。它为循环的每次迭代使用闭包类的单个实例。代码看起来像这样,这使得更容易理解错误发生的原因:
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;
}
}
这实际上不是您的示例中的已编译代码,但我已经检查了自己的代码,这看起来非常类似于编译器实际生成的代码。
答案 3 :(得分:7)
解决这个问题的方法是在代理变量中存储所需的值,并捕获该变量。
即
while( variable < 5 )
{
int copy = variable;
actions.Add( () => copy * 2 );
++variable;
}
答案 4 :(得分:4)
是的,您需要在循环中范围variable
并将其传递给lambda:
List<Func<int>> actions = new List<Func<int>>();
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();
答案 5 :(得分:4)
多线程也发生了同样的情况(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();
}
答案 6 :(得分:3)
因为您使用lambda表达式() => variable * 2
而触发此行为,其中外部作用域variable
实际上未在lambda的内部作用域中定义。
Lambda表达式(在C#3 +中,以及C#2中的匿名方法)仍然创建实际方法。将变量传递给这些方法涉及一些困境(通过值传递?通过引用传递?C#通过引用传递 - 但这会打开另一个问题,其中引用可以比实际变量更长)。 C#解决所有这些困境的方法是创建一个新的辅助类(&#34; closure&#34;),其中包含与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;
}
无论如何,&#34; Closures&#34;不是与循环相关的概念,而是使用本地作用域变量的匿名方法/ lambda表达式 - 尽管一些不谨慎的循环使用了闭包陷阱。
答案 7 :(得分:0)
这称为关闭问题, 只需使用一个复制变量,就可以完成。
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
int i = variable;
actions.Add(() => i * 2);
++ variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
答案 8 :(得分:0)
for (int n=0; n < 10; n++) //forloop syntax
foreach (string item in foo) foreach syntax
答案 9 :(得分:-1)
由于这里没有人直接引用ECMA-334:
10.4.4.10对于语句
明确赋值检查形式的for语句:
for (for-initializer; for-condition; for-iterator) embedded-statement
就像编写该语句一样完成:
{
for-initializer;
while (for-condition) {
embedded-statement;
LLoop: for-iterator;
}
}
进一步,
12.16.6.3局部变量的实例
当执行进入变量的范围时,将认为局部变量已实例化。
[示例:例如,当调用以下方法时,局部变量
x
实例化并初始化三次,每次循环迭代一次。
static void F() {
for (int i = 0; i < 3; i++) {
int x = i * 2 + 1;
...
}
}
但是,将
x
的声明移到循环外会导致x
的单个实例化:
static void F() {
int x;
for (int i = 0; i < 3; i++) {
x = i * 2 + 1;
...
}
}
示例]
如果未捕获,则无法准确观察局部变量被实例化的频率-由于实例化的生命周期是不相交的,因此每个实例化都可以简单地使用相同的存储位置。但是,当匿名函数捕获局部变量时,实例化的效果变得明显。
[示例:示例
using System;
delegate void D();
class Test{
static D[] F() {
D[] result = new D[3];
for (int i = 0; i < 3; i++) {
int x = i * 2 + 1;
result[i] = () => { Console.WriteLine(x); };
}
return result;
}
static void Main() {
foreach (D d in F()) d();
}
}
产生输出:
1
3
5
但是,当
x
的声明移到循环之外时:
static D[] F() {
D[] result = new D[3];
int x;
for (int i = 0; i < 3; i++) {
x = i * 2 + 1;
result[i] = () => { Console.WriteLine(x); };
}
return result;
}
输出为:
5
5
5
请注意,允许(但不是必需)编译器将三个实例优化为单个委托实例(第11.7.2节)。
如果for循环声明了一个迭代变量,则该变量本身被视为在循环外部声明。 [示例:因此,如果更改示例以捕获迭代变量本身:
static D[] F() {
D[] result = new D[3];
for (int i = 0; i < 3; i++) {
result[i] = () => { Console.WriteLine(i); };
}
return result;
}
仅捕获迭代变量的一个实例,该实例产生输出:
3
3
3
示例]
是的,我想应该提到的是,在C ++中不会发生此问题,因为您可以选择是通过值还是通过引用捕获变量(请参见:Lambda capture)。