0

可能重复:
C#:在 lambda 表达式中使用 foreach 循环的迭代器变量 - 为什么会失败?

我在 MSDN 上阅读 c# 参考资料,我发现了这个..

http://msdn.microsoft.com/en-us/library/0yw3tz5k.aspx

在评论的最后有一条评论是albionmike 这样的..

When you "catpure" a variable from an outer scope, some counter-intuitive things happen.
If you run this, you will get an IndexOutOfRange exception during the call f().
If you uncomment the two commented out lines of code, it will work as expected.
Hint: Captured Outer Variables have reference rather than value semantics

// Console Project
using System;
using System.Collections.Generic;
using System.Text;


namespace EvilDelegation
{
    delegate void PrintIt();

    class Program
    {

        static void Main(string[] args)
        {
            string[] strings = { "zero", "one", "two", "three", "four" };
            PrintIt f = null;
            for (int i = 0; i < strings.Length; ++i) {
                if (i == 2 || i == 3) {
                    // Can you see why this would not work?
                    f = delegate() { Console.WriteLine(strings[i]); };

                    // But this does...
                    //int k = i;
                    //f = delegate() { Console.WriteLine(strings[k]); };

                }
            }
            f();
        }
    }
}

我不明白,为什么第一个不行,第二个不行?在第 4 行中,他说:Captured Outer Variables have reference rather than value semantics.
好的。但是在for循环中,我们定义iintwhich当然是值类型,那么类型怎么能int持有引用呢?如果i不能保持引用,这意味着它正在存储价值,如果它正在存储价值,那么我不明白为什么第一个不起作用而第二个会起作用?
我在这里错过了什么吗?

编辑:我认为原作者有一个错字,对 f() 的调用应该在 if 循环内。请在回答时考虑这一点。

编辑 2:好的,如果有人可能会说,这不是错字,让我们考虑一下。我想知道在f()子句if中调用的情况。在那种情况下都会运行,还是只运行一个没有评论的?

4

3 回答 3

4

这是因为闭包的语义。当闭包在其外部范围内引用局部变量时,它会捕获对变量的引用,而不是变量中包含的值。

在这种情况下,匿名委托delegate() { Console.WriteLine(strings[i]); }正在捕获对变量的引用;i也就是说,该变量在匿名函数和i声明的范围之间共享。当i一种情况发生变化时,另一种情况也会发生变化。

例如(看到它运行):

using System;

class Foo {
    static void Main() {
        int i = 0;
        Action increment = delegate { ++i; };

        Console.WriteLine(i);

        ++i;
        Console.WriteLine(i);

        increment();
        Console.WriteLine(i);

        ++i;
        Console.WriteLine(i);

        increment();
        Console.WriteLine(i);
    }
}

这将输出:

0
1
2
3
4

在 C# 中,local 的生命周期被扩展为包括引用它们的任何闭包的生命周期。这启用了一些非常有趣的技巧,可能会冒犯 C/C++ 开发人员的敏感性:

static Func<int> Counter() {
    int i = 0;
    return delegate { return i++; };
}
于 2012-07-17T14:39:47.227 回答
3

您正在访问修改后的闭包。您的委托只会在它被调用时被评估,并且它已经捕获了循环变量i。在访问 this 时,在循环退出后,它的值将等于strings.Length。但是,通过在循环中引入局部变量k,您可以捕获k循环迭代的特定变量,并且结果是正确的。

如果调用是在循环中进行的,正如您在下面的评论中所建议的那样,那么在i循环前进之前,将在该点评估 的值,并将具有“正确”的值。

于 2012-07-17T14:39:43.733 回答
0

这只是(不幸的)设计。当您i在委托中使用循环变量时,它会像这样被捕获,并且i在您到达f()调用时将具有其最终值。

根据Eric Lippert的这篇博客文章,它很快就会改变foreach变量,但不会改变变量(这就是您在示例中所拥有的)。for

添加:

foreach这是一个使用代替的示例for

  string[] strings = { "zero", "one", "two", "three", "four" }; 

  var list = new List<PrintIt>();

  foreach (var str in strings)
  {
    PrintIt f = delegate { Console.WriteLine(str); };  // captures str
    list.Add(f);
  }
  var f0 = list[0];
  f0();

在我的 .NET 版本(.NET 4.0,C# 4)中,最后一行打印“四”。据我了解,在即将推出的 .NET 版本(.NET 4.5、C# 5、Visual Studio 2012)中,它将打印“零”。颠覆性变化...

当然,在这种情况下delegate { Console.WriteLine(str); }相当于delegate() { Console.WriteLine(str); }相当于() => { Console.WriteLine(str); }

于 2012-07-17T14:41:58.270 回答