8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png

在 C# 中的循环中捕获变量

Evariste Galois 2月前

217 0

我遇到了一个关于 C# 的有趣问题。我有如下代码。列表 >actions=新列表 >();int 变量 = 0;while (变量 < 5){actions.Add((...

我遇到了一个有关 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。

看起来这是因为所有操作都引用一个捕获的变量。因此,当它们被调用时,它们都有相同的输出。

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

帖子版权声明 1、本帖标题:在 C# 中的循环中捕获变量
    本站网址:http://xjnalaquan.com/
2、本网站的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
3、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
4、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
5、站长邮箱:yeweds@126.com 除非注明,本帖由Evariste Galois在本站《multithreading》版块原创发布, 转载请注明出处!
最新回复 (0)
  • 由于这里没有人直接引用 ECMA-334 :

    10.4.4.10 For 语句

    对以下形式的 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 捕获 )。

  • 这被称为闭包问题,只需使用复制变量即可完成。

    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());
    }
    
  • for (int n=0; n < 10; n++) //forloop syntax
    foreach (string item in foo) foreach syntax
    
  • 正如其他人所说,它与循环无关。它是 C# 中匿名函数体中的变量捕获机制的效果。当您定义 lambda 作为示例时;

    actions.Add(() => variable * 2);
    

    为 lambda 函数 <>c__DisplayClass0_0 <>c__DisplayClass0_0 () => () => variable * 2 .

    在生成的类(容器)内部,它会生成一个名为 变量 ,该字段具有一个同名的捕获变量和包含 lambda 主体的方法 b__0()。

    [CompilerGenerated]
    private sealed class <>c__DisplayClass0_0
    {
    public int variable;
    
    internal int <Main>b__0()
    {
        return variable * 2;
    }
    }
    

    然后名为变量的局部变量 成为 容器类 (<>c__DisplayClass0_0) 的一个字段

    <>c__DisplayClass0_.variable = 0;
    while (<>c__DisplayClass0_.variable < 5)
    {
        list.Add(new Func<int>(<>c__DisplayClass0_.<Main>b__0));
        <>c__DisplayClass0_.variable++;
    }
    

    因此,增加变量会导致容器类的字段依次增加,并且因为我们在 while 循环的所有迭代中都获得容器类的一个实例,所以我们得到相同的输出,即 10。

    enter image description here

    您可以通过将循环体内捕获的变量重新分配给新的局部变量来防止

    while (variable < 5)
    {
        var index = variable; // <= this line
        actions.Add(() => index * 2);
        ++ variable;
    }
    

    顺便说一句,这种行为在 .Net 8 Preview 中仍然有效,并且我发现这种行为存在很多缺陷且具有欺骗性。

  • 这与为什么您需要“重新声明”要捕获的变量无关。这仅与第二个线程可能在操作系统级别上更快地“准备好工作”或执行代码更早被安排有关。您的第二个示例也不会每次都输出 1-5。在 Debug 中可能会这样,因为速度要慢很多,但在 Release 版本中绝对不会这样。

  • 同样的情况也发生在多线程(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();
    }
    
  • 是的,您需要 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();
    
  • 另外,Jon,我很想了解一下你对各种 Java 7 闭包提案的看法。我曾看到你提到过想要写一个,但我还没有看到。

  • @tjlevine:非常感谢。我会在我的回答中添加对此的引用。我忘了这件事了!

  • 哈哈乔恩,事实上我刚刚读了你的文章:csharpindepth.com/Articles/Chapter5/Closures.aspx 你做得很好我的朋友。

  • 解决这个问题的方法是将您需要的值存储在代理变量中,然后捕获该变量。

    IE

    while( variable < 5 )
    {
        int copy = variable;
        actions.Add( () => copy * 2 );
        ++variable;
    }
    
  • 这与循环无关。

    触发此行为是因为您使用了 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 表达式使用局部范围变量有关 - 尽管一些不谨慎的循环使用会展示闭包陷阱。

  • 在幕后,编译器会生成一个代表方法调用闭包的类。它会在循环的每次迭代中使用该闭包类的单个实例。代码如下所示,这样可以更轻松地了解错误发生的原因:

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

    这实际上不是您的示例中编译的代码,但我检查了我自己的代码,它看起来非常像编译器实际生成的内容。

  • 我相信您遇到的是一种称为 Closure 的东西 http://en.wikipedia.org/wiki/Closure_(computer_science) 。您的 lamba 引用了一个作用域在函数本身之外的变量。您的 lamba 只有在您调用它时才会被解释,一旦被解释,它就会在执行时获得变量的值。

  • @Florimond:C# 中的闭包并不是这样工作的。它们捕获变量,而不是值。(无论循环如何,这都是正确的,并且可以通过捕获变量的 lambda 轻松演示,并且每次执行时都只打印当前值。)

  • 引用 17

    对于 C# 5.0 的行为不同(更合理),请参阅 Jon Skeet 的较新答案 - .com/questions/16264289/…

  • 如果我让其他人插入它,它看起来会更好;)(我承认我确实倾向于投票支持推荐它的答案。)

  • 是的——复制循环内的变量:

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

    您可以认为 C# 编译器每次遇到变量声明时都会创建一个“新”局部变量。实际上,它会创建适当的新闭包对象,如果您在多个范围内引用变量,它会变得复杂(就实现而言),但它确实有效 :)

    请注意,此问题更常见的情况是使用 f或 or 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 变化,因此您不再需要进行本地复制。 有关更多详细信息, 此答案

  • 经验证,截至目前,它在 C# 6.0 (VS 2015) 上输出了 5 个 10。我怀疑闭包变量的这种行为是否值得改变。

返回
作者最近主题: