如何在C#中告诉Lambda函数捕获一个副本而不是一个引用?

44

我一直在学习C#,正在尝试理解Lambda表达式。在下面的示例中,它会打印10十次。

class Program
{
    delegate void Action();
    static void Main(string[] args)
    {
        List<Action> actions = new List<Action>();

        for (int i = 0; i < 10; ++i )
            actions.Add(()=>Console.WriteLine(i));

        foreach (Action a in actions)
            a();
    }
}

显然,lambda后面生成的类正在存储对变量 int i 的引用或指针,并且每次循环迭代时都会给同一引用赋新值。是否有一种方法可以强制 lambda 抓取副本,就像 C++0x 语法一样?

[&](){ ... } // Capture by reference

对比。

[=](){ ... } // Capture copies

1
你可能想阅读我们自己的Jon Skeet所写的这篇文章 - Joel Coehoorn
可能是C#循环中的捕获变量的重复问题。 - nawfal
8
我觉得有趣的是,对于这个问题大部分回答都在解释捕获语义,而提问者已经非常明白了,只有一些人提到了解决方案(临时复制)。难道没有人在回答之前仔细阅读问题吗? - ghord
似乎C#的lambda表达式通过引用捕获引用。如果时机合适,object o = new object(); Task.Run(() => Console.Write($"o is null: {o==null}");); o = null;会打印TRUE。这意味着您必须自己创建一个副本,并在lambda执行之前不修改它,以获得所需的效果。 - Nenad
4个回答

36

我找到的唯一解决方案是先创建一个本地副本:

for (int i = 0; i < 10; ++i)
{
    int copy = i;
    actions.Add(() => Console.WriteLine(copy));
}

但是我不太明白为什么把一个副本放在for循环里面会和lambda捕获i有什么区别。


26
因为int声明在for循环内部,所以每次都会重新创建。有10个不同的int变量名都叫做"copy",而在作用域中只有一个int变量名叫做"i",并被逐渐传递。 - technophile

34

编译器正在执行的操作是将您的lambda和由lambda捕获的任何变量放入编译器生成的嵌套类中。

编译后,您的示例看起来非常像这样:

class Program
{
        delegate void Action();
        static void Main(string[] args)
        {
                List<Action> actions = new List<Action>();

                DisplayClass1 displayClass1 = new DisplayClass1();
                for (displayClass1.i = 0; displayClass1.i < 10; ++displayClass1.i )
                        actions.Add(new Action(displayClass1.Lambda));

                foreach (Action a in actions)
                        a();
        }

        class DisplayClass1
        {
                int i;
                void Lambda()
                {
                        Console.WriteLine(i);
                }
        }
}

通过在 for 循环中创建一个副本,编译器会在每个迭代中生成新的对象,如下所示:

for (int i = 0; i < 10; ++i)
{
    DisplayClass1 displayClass1 = new DisplayClass1();
    displayClass1.i = i;
    actions.Add(new Action(displayClass1.Lambda));
}

谢谢。这真的很有帮助,让我明白了当表达式尝试评估参数时发生了什么。 - Quark Soup
8
不冒犯作者,但这不应该成为被接受的回答。问题是如何强制C#按值捕获变量,而这个答案只解释了机制。我仍然不知道如何按值捕获,除非我使用DisplayClass1而不是lambda(在我看来,这违背了初衷)。 - Robert F.

11

唯一的解决方案是创建本地副本并在lambda内引用它。 在C#(和VB.Net)中,当在闭包中访问变量时,所有变量将具有引用语义而不是复制/值语义。 无论是哪种语言都无法改变这种行为。

注意:实际上它不会编译成引用。 编译器将变量提升为闭包类,并将“i”的访问重定向到给定闭包类内部的字段“i”中。 尽管通常更容易将其视为引用语义。


4

这种行为的描述链接是我认为最好的答案,请点赞。 - yanpas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接