C#中的lambda如何捕获变量

10
为什么以下代码会打印两次11?
int i = 10;
Action fn1 = () => Console.WriteLine(i);
i = 11;
Action fn2 = () => Console.WriteLine(i);
fn1();
fn2();

输出 11 11

根据这篇帖子中的回答 - 如何告诉C# lambda函数捕获一个副本而不是引用? - lambda函数被转换为一个包含被捕获变量副本的类。如果是这样,我的例子不应该打印10和11吗?

现在,当lambda函数通过引用捕获时,它如何影响所捕获变量的生命周期。例如,假设上述代码片段在一个函数中,并且Actions的作用域对于变量是全局的,像这样:

class Test
{
  Action _fn1;
  Action _fn2;

  void setActions()
  {
    int i = 10;
    _fn1 = () => Console.WriteLine(i);
    i = 11;
    _fn2 = () => Console.WriteLine(i);
  }

  static void Main()
  {
    setActions();
    _fn1();
    _fn2();
  }
}

在这种情况下,当操作被调用时,变量i会超出作用域吗?因此,这些操作留下了一种类似于悬空指针的引用吗?
3个回答

10
如果那是情况的话,我的例子不应该打印10和11吗?不,因为你只有一个变量 - fn1 捕获了变量,而不是其当前值。因此,像这样的方法:
static void Foo()
{
    int i = 10;
    Action fn1 = () => Console.WriteLine(i);
    i = 11;
    Action fn2 = () => Console.WriteLine(i);
    fn1();
    fn2(); 
}

翻译成中文是这样的:

class Test
{
    class MainVariableHolder
    {
        public int i;
        public void fn1() => Console.WriteLine(i);
        public void fn2() => Console.WriteLine(i);
    }

    void Foo()
    {
        var holder = new MainVariableHolder();
        holder.i = 10;
        Action fn1 = holder.fn1;
        holder.i = 11;
        Action fn2 = holder.fn2;
        fn1();
        fn2();
    }
}

这也回答了你的第二个问题:变量被"提升"到一个类中,所以它的生命周期会随着委托对象的存在而有效延长。

谢谢你的回答。但是,为什么这两个lambda表达式会被表示为一个类呢?我以为它们会各自有一个单独的类。你能给我指出证据吗?我尝试通过ILSpy查看IL代码,但它并没有显示生成lambda表达式的类。 - Social Developer
1
@Sumith:它不能使用两个类,因为这样你会得到两个变量 - 如果fn1修改了i,那么在fn2中需要可见。至于如何验证它:使用ildasm而不是ilspy,或者(如果可以的话)在ilspy中禁用“优化”(这样它就可以少做一些工作来重构原始的C#代码)。 - Jon Skeet
1
@Sumith:如果你进入“查看/选项”并取消勾选“反编译lambda”等选项,你将会看到像我展示的代码。 - Jon Skeet
1
这是否是闭包的一个例子,就像这篇文章中描述的那样? - Scrobi
1
@Scrobi:是的,没错。 - Jon Skeet
我希望它的工作方式更像C++的lambda表达式,可以清楚地指定是按值捕获还是按引用捕获。(顺便说一句,我还没有阅读@Scrobi分享的《闭包之美》) - Social Developer

0

实际上,一个类被生成,其字段是已捕获变量,而不是它们的值。因此,当lambda被执行时,运行时将检查i当前值,因为在调用lambda之前你已经更新了这个值,所以得到了这样的结果。为了验证这个论点,你可以按照以下语句重新排列顺序:

int i = 10;
Action fn1 = () => Console.WriteLine(i);
fn1();
i = 11;
Action fn2 = () => Console.WriteLine(i);
fn2();

-1

它打印两次是因为延迟执行,而不是因为捕获变量的方式。

当执行被延迟时,它将打印 i 的最新值,因为捕获的是 i,而不是它的值。


2
换句话说,它是因为它捕获变量而不是值。因为i被捕获了,而不是它的值。 - Jon Skeet

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