【问题标题】:How is a c# lambda capturing variablesc# lambda 如何捕获变量
【发布时间】:2017-06-12 09:31:59
【问题描述】:

为什么下面的代码会打印两次 11?

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

输出 11 11

根据这篇文章中的答案 - How to tell a lambda function to capture a copy instead of a reference in C#? - 将一个 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();
  }
}

在这种情况下,当调用操作时,我的变量不会超出范围吗?那么,这些操作是否带有悬空指针类型的引用?

【问题讨论】:

    标签: c# lambda closures


    【解决方案1】:

    如果是这样,我的示例不应该打印 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 生成的类。
    • @Sumith:它不能使用两个类,因为那样你最终会得到两个变量——如果fn1 修改i,则需要在fn2 中可见。至于如何验证它:使用 ildasm 代替 ilspy,或者(如果可以)禁用 ilspy 中的“优化”(这样重建原始 C# 的工作就更少了)
    • @Sumith:如果你进入 View / Options 并取消勾选“Decompile lambdas”等,你会看到我展示的代码。
    • 这是否是article中描述的闭包示例
    • @Scrobi:是的。
    【解决方案2】:

    确实生成了一个类,它的字段是捕获的变量,而不是它们的值。因此,当执行 lambda 时,运行时将检查 icurrent 值,即 11,因为您在调用 lambda 之前已经更新了它。这就是你看到这个的原因。为了验证这个论点,您可以重新排列语句的顺序,如下所示:

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

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

      • " 因为 i 是被捕获的,而不是它的值。" - 换句话说,它,因为它捕获变量的方式。 (关键是它捕获变量,而不是值)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多