闭包中的变量捕获详解

Posted

技术标签:

【中文标题】闭包中的变量捕获详解【英文标题】:Detailed Explanation of Variable Capture in Closures 【发布时间】:2011-03-25 21:27:31 【问题描述】:

我看过无数关于变量捕获如何引入变量以创建闭包的帖子,但它们似乎都没有具体细节,而是将整个事情称为“编译器魔法”。

我正在寻找一个明确的解释:

    如何实际捕获局部变量。 捕获值类型与引用类型之间的区别(如果有)。 以及是否存在与值类型相关的装箱。

我更喜欢根据值和指针(更接近内部发生的事情的核心)给出答案,但我也会接受涉及值和引用的明确答案。

【问题讨论】:

您阅读文档了吗? 是什么让您认为涉及指针?请记住,这是在 C# 本身级别完成的 - 它不是由 CLR 完成的。 在底层引用是指针。只有当它使事情更清楚易懂时,我才会寻找这种幕后解释。 在底层引用是一些当前实现的指针,即使在它们中也不能保证它们会保持这种方式。 FPGA 的 .Net 解释器或编译器可以在没有任何指针概念的情况下运行所有​​有效的非不安全代码。 @DuckMaestro:VirtualBlackFox 完全正确。指针的实现与 C# 语言规范提供的保证无关。在理解特性时,绝对值得尝试保持适当的思考水平——而且绝对可以理解闭包,而无需考虑虚拟机(或其他)到底在做什么。 【参考方案1】:
    很棘手。马上就会讲到。 没有区别 - 在这两种情况下,捕获的都是变量本身。 不,没有拳击发生。

通过示例演示捕获的工作原理可能是最简单的...

下面是一些使用 lambda 表达式捕获单个变量的代码:

using System;

class Test

    static void Main()
    
        Action action = CreateShowAndIncrementAction();
        action();
        action();
    

    static Action CreateShowAndIncrementAction()
    
        Random rng = new Random();
        int counter = rng.Next(10);
        Console.WriteLine("Initial value for counter: 0", counter);
        return () =>
        
            Console.WriteLine(counter);
            counter++;
        ;
    

现在这是编译器为您做的事情 - 除了它会使用 C# 中不会真正出现的“不可描述”的名称。

using System;

class Test

    static void Main()
    
        Action action = CreateShowAndIncrementAction();
        action();
        action();
    

    static Action CreateShowAndIncrementAction()
    
        ActionHelper helper = new ActionHelper();        
        Random rng = new Random();
        helper.counter = rng.Next(10);
        Console.WriteLine("Initial value for counter: 0", helper.counter);

        // Converts method group to a delegate, whose target will be a
        // reference to the instance of ActionHelper
        return helper.DoAction;
    

    class ActionHelper
    
        // Just for simplicity, make it public. I don't know if the
        // C# compiler really does.
        public int counter;

        public void DoAction()
        
            Console.WriteLine(counter);
            counter++;
        
    

如果您捕获在循环中声明的变量,您最终会为循环的每次迭代获得一个新的 ActionHelper 实例 - 因此您可以有效地捕获变量的不同“实例”。

当您从不同范围捕获变量时,它会变得更加复杂...如果您真的想要那种详细程度的细节,请告诉我,或者您可以编写一些代码,在 Reflector 中反编译并遵循它:)

注意方法:

不涉及拳击 不涉及指针或任何其他不安全代码

编辑:这是两个代表共享一个变量的示例。一个代表显示counter 的当前值,另一个增加它:

using System;

class Program

    static void Main(string[] args)
    
        var tuple = CreateShowAndIncrementActions();
        var show = tuple.Item1;
        var increment = tuple.Item2;

        show(); // Prints 0
        show(); // Still prints 0
        increment();
        show(); // Now prints 1
    

    static Tuple<Action, Action> CreateShowAndIncrementActions()
    
        int counter = 0;
        Action show = () =>  Console.WriteLine(counter); ;
        Action increment = () =>  counter++; ;
        return Tuple.Create(show, increment);
    

...和扩展:

using System;

class Program

    static void Main(string[] args)
    
        var tuple = CreateShowAndIncrementActions();
        var show = tuple.Item1;
        var increment = tuple.Item2;

        show(); // Prints 0
        show(); // Still prints 0
        increment();
        show(); // Now prints 1
    

    static Tuple<Action, Action> CreateShowAndIncrementActions()
    
        ActionHelper helper = new ActionHelper();
        helper.counter = 0;
        Action show = helper.Show;
        Action increment = helper.Increment;
        return Tuple.Create(show, increment);
    

    class ActionHelper
    
        public int counter;

        public void Show()
        
            Console.WriteLine(counter);
        

        public void Increment()
        
            counter++;
        
    

【讨论】:

@Jon 你说捕获是变量而不是值。这意味着,我猜,如果在同一个方法中声明的两个 lambda 引用同一个变量,那么它们都捕获同一个变量。如果其中一个 lambda 修改了该变量,那么另一个会看到该变量持有的修改后的值。还是我偏离了标准? @David:是的,完全正确。在这种情况下,生成的类中会有两个实例方法,并且两个委托都会引用同一个目标实例。 @Jon 谢谢。我实际上并不了解任何 C#,而且我的大部分时间都花在了 Delphi 上。 Delphi 等价物的行为方式相同。它在大多数用例中往往不会出现,但我认为大多数人的天真期望是价值被捕获。在这种特殊的误解上,你可能会有很长的路要走。 @David:是的,你可以。特别是因为这就是匿名类在 Java 中的工作方式:( @JonSkeet,当捕获的变量之一是类字段而另一个不是时会发生什么?匿名方法是否指向该实例的字段?这个实例是否以某种方式传递给生成的类?还有一个问题 - 编译器是否可能不知道哪些匿名方法共享某些捕获的变量?

以上是关于闭包中的变量捕获详解的主要内容,如果未能解决你的问题,请参考以下文章

Swift 中的Closures(闭包)详解

Swift之深入解析闭包Closures的使用和捕获变量的原理

正确使用和理解C#中的闭包

➽06闭包

理解 Javascript/Node 中闭包的变量捕获

C#由变量捕获引起对闭包的思考