【问题标题】:Why does a lambda expression preserve enclosing scope variable values after method terminates?为什么 lambda 表达式在方法终止后保留封闭范围变量值?
【发布时间】:2015-05-23 13:58:31
【问题描述】:

我的印象是 C# 中的 lambda 表达式上下文包含对其中使用的父函数范围的变量的引用。考虑:

public class Test
{
    private static System.Action<int> del;

    public static void test(){
        int i = 100500;
        del = a => System.Console.WriteLine("param = {0}, i = {1}", a, i);
        del(1);

        i = 10;
        del(1);

    }

    public static void Main()
    {
        test();
    }
}

输出

param = 1, i = 100500
param = 1, i = 10

但是,如果这是真的,那么以下内容将是非法的,因为 lambda 上下文将引用超出范围的局部变量:

public class Test
{
    private static System.Action<int> del;

    public static void test(){
        int i = 100500;
        del = a => System.Console.WriteLine("param = {0}, i = {1}", a, i);
    }

    public static void Main()
    {
        test();
        del(1);
    }
}

但是,这会编译、运行和输出

param = 1, i = 100500

这意味着要么发生了一些奇怪的事情,要么上下文保留了局部变量的值,而不是对它们的引用。但如果这是真的,它必须在每次 lambda 调用时更新它们,而且我看不出当原始变量超出范围时它会如何工作。此外,在处理大值类型时,这似乎会产生开销。

我知道,例如,在 C++ 中,这是 UB(已在回复 this question 时确认)。

问题是,这是 C# 中定义明确的行为吗? (我认为 C#确实有一些 UB,或者至少有一些 IB,对吧?)

如果定义明确,它是如何以及为什么实际起作用的? (实现逻辑会很有趣)

【问题讨论】:

  • Lambda Expressions: "Lambda 可以引用外部变量 (...),这些变量在定义 lambda 函数的方法的范围内,或在包含 lambda 表达式的类型的范围内。以这种方式捕获的变量被存储以供 lambda 表达式使用,即使这些变量会超出范围并被垃圾回收......”(我的重点)
  • 这应该回答你的问题:stackoverflow.com/questions/9591476/…
  • Skeet 的article about closures 更深入一点。此外,此答案包含有关实际生成代码的一些信息:stackoverflow.com/a/7142956/996081
  • 发生的情况是编译器生成了一个类,而您正在使用的变量被“提升”到这个类上。在您的方法内部,构造了该类的一个新实例,并且该方法中该变量的所有用法都经过该实例。您声明的实际委托也作为普通方法提升到此类。
  • 因此它不捕获,而是捕获变量

标签: c# lambda


【解决方案1】:

与 C# 中的 lambda 语法相关的闭包概念是一个非常大的话题,对于我来说,我无法仅在这个答案中涵盖所有内容,但至少让我们在这里尝试回答具体问题。实际答案在底部,其余是理解答案所需的背景。


当编译器尝试使用匿名方法编译方法时,会发生某种程度的重写方法。

基本上,会生成一个新类,并将匿名方法提升到该类中。它被赋予了一个名称,尽管是一个内部名称,因此对于编译器来说,它有点从匿名方法转换为命名方法。但是,您不必知道或处理该名称。

此方法所需的任何变量,除了匿名方法之外声明的变量,但在使用/声明匿名方法的同一方法中,也将被解除,然后所有用法这些变量被重写。

现在这里涉及到几种方法,因此很难阅读上面的文字,所以让我们做一个例子:

public Func<int, int> Test1()
{
    int a = 42;
    return value => a + value;
}

这个方法被改写成这样:

public Func<int, int> Test1()
{
    var dummy = new <>c__DisplayClass1();
    dummy.a = 42;
    return dummy.<Test1>b__0;
}

internal class <>c__DisplayClass1
{
    public int a;
    public int <Test1>b__0(int value)
    {
        return a + value;
    }
}

编译器可以处理所有这些时髦的名称(是的,它们确实是用所有类似的括号命名的),因为它指的是带有 id 和对象引用的东西,名称不再是编译器的问题。但是,您永远不能使用这些名称声明类或方法,因此编译器不会产生恰好已经存在的类的风险。

这是一个LINQPad 示例,它显示了我声明的一个类,尽管其名称中的括号较少,但看起来与编译器生成的类相同:

void Main()
{
    var f1 = Test1();
    f1(10).Dump();
    f1.Dump();

    var f2 = Test2();
    f2(10).Dump();
    f2.Dump();
}

public Func<int, int> Test1()
{
    int a = 42;
    return value => a + value;
}

public Func<int, int> Test2()
{
    var dummy = new __c__DisplayClass1();
    dummy.a = 42;
    return dummy._Test2_b__0;
}

public class __c__DisplayClass1
{
    public int a;
    public int _Test2_b__0(int value)
    {
        return a + value;
    }
}

输出:

如果您查看上面的屏幕截图,您会注意到每个委托变量有两件事,一个Method 属性和一个Target 属性。

调用该方法时,会使用引用Target 对象的this 引用来调用它。因此,委托捕获了两件事:调用哪个方法,以及调用它的对象。

所以基本上,生成的类的对象作为委托的一部分仍然存在,因为它是方法的目标。


考虑到所有这些,让我们看看你的问题:

为什么 lambda 表达式在方法终止后保留封闭范围变量值?

答:如果 lambda 存在,所有捕获的变量也会存在,因为它们不再是声明它们的方法的局部变量。相反,它们被提升到一个也具有 lambda 方法的新对象上,因此无论走到哪里都“跟随” lambda。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-04-21
    • 1970-01-01
    • 2015-02-04
    • 1970-01-01
    • 2011-11-24
    • 1970-01-01
    • 1970-01-01
    • 2019-08-02
    相关资源
    最近更新 更多