当前位置:  开发笔记 > 编程语言 > 正文

C#中循环中的捕获变量

如何解决《C#中循环中的捕获变量》经验,为你挑选了7个好方法。

我遇到了一个关于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.

似乎是由于所有操作都涉及一个捕获的变量.结果,当它们被调用时,它们都具有相同的输出.

有没有办法解决这个限制,让每个动作实例都有自己的捕获变量?



1> Jon Skeet..:

是 - 在循环中获取变量的副本:

while (variable < 5)
{
    int copy = variable;
    actions.Add(() => copy * 2);
    ++ variable;
}

您可以将其视为C#编译器每次命中变量声明时都创建一个"新"局部变量.实际上它会创建适当的新闭包对象,如果你在多个范围内引用变量,它会变得复杂(在实现方面),但是它可以工作:)

请注意,此问题更常见的是使用forforeach:

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.0,行为是不同的(更合理),请参阅Jon Skeet的更新答案 - http://stackoverflow.com/questions/16264289/captured-closure-loop-variable-in-c-sharp-5-0
与以往一样,对skeet@pobox.com的反馈将不胜感激:)

2> TheCodeJunki..:

我相信你所经历的就是Closure http://en.wikipedia.org/wiki/Closure_(computer_science).您的lamba引用了一个变量,该变量在函数本身之外.在您调用lamba之前,不会对其进行解释.一旦它被调用,它将获得变量在执行时具有的值.



3> gerrard00..:

在幕后,编译器生成一个表示方法调用闭包的类.它为循环的每次迭代使用闭包类的单个实例.代码看起来像这样,这使得更容易看到错误发生的原因:

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;
    }
}

这实际上不是您的示例中的已编译代码,但我已经检查了自己的代码,这看起来非常类似于编译器实际生成的内容.



4> tjlevine..:

解决这个问题的方法是在代理变量中存储您需要的值,并捕获该变量.

IE

while( variable < 5 )
{
    int copy = variable;
    actions.Add( () => copy * 2 );
    ++variable;
}



5> cfeduke..:

是的,您需要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();



6> Sunil..:

在多线程(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();
}



7> David Refael..:
这与循环无关。

触发此行为的原因是,您使用的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 Func 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 Mult(int i)
{
    Helper help = new Helper(i);
    return help.Method;
}

在任何情况下,“闭包”都不是与循环相关的概念,而是与局部范围变量使用的匿名方法/ lambda表达式有关,尽管对循环的某些谨慎使用显示了闭包陷阱。

推荐阅读
低调pasta_730
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有