Closures中变量捕获的详细解释

66

我曾经看过无数关于变量捕获的帖子,这些帖子都提到了创建闭包时会引入变量,但是它们似乎都没有详细解释,而只是称整个过程为“编译器的魔法”。

我正在寻找一个清晰明了的解释:

  1. 局部变量是如何被实际捕获的。
  2. 在捕获值类型和引用类型之间的区别(如果有的话)。
  3. 是否存在任何与值类型相关的装箱操作。

我更喜欢以值和指针的方式回答(更接近内部发生的情况),但是如果有明确的答案涉及值和引用,我也会接受。


3
你有阅读文档吗? - David Heffernan
什么让你觉得涉及到指针?请记住,这是在C#本身的级别上完成的 - 不是由CLR完成的。 - Jon Skeet
引用在底层是指针。我只寻找那种底层解释,如果它能让事情更清晰易懂的话。 - DuckMaestro
1
在底层,引用是指向某些当前实现的指针,即使在它们中也不能保证它们会保持这种方式。一个将.NET解释器或编译器转换为FPGA的程序可能会做出不同的选择,但仍然可以运行所有有效的非不安全代码,而不需要任何指针概念。 - Julien Roncaglia
3
@DuckMaestro:VirtualBlackFox说得完全正确。指针的实现对C#语言规范提供的保证是无关紧要的。在理解功能时,试图保持适当的思考水平肯定是值得的——闭包可以毫无疑问地被理解,而不必考虑虚拟机(或其他东西)究竟在做什么。 - Jon Skeet
谢谢你们两位。很有道理。我会更新标题,减少指针的强调。 - DuckMaestro
1个回答

87
  1. 这有点棘手。等一下再说。
  2. 没有区别-在两种情况下,被捕获的是变量本身。
  3. 不,不会发生装箱。

最好通过一个例子来演示捕获如何工作...

这是使用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++;
        }
    }
}

3
@Jon,你说捕获的是变量而不是值。我想这意味着如果在同一个方法中声明的两个lambda表达式引用同一变量,则它们都捕获同一个变量。因此,如果其中一个lambda修改了该变量,那么另一个lambda就会看到该变量保存的修改后的值。我理解得对吗? - David Heffernan
2
@David:是的,完全正确。在这种情况下,生成的类中将有两个实例方法,并且两个委托都将引用相同的目标实例。 - Jon Skeet
2
@Jon 谢谢。实际上,我并不了解任何C#,大部分时间都在使用Delphi。Delphi的等效行为方式相同。它在大多数情况下并不会出现,但我认为大多数人天真地期望值被捕获。你可以在这种误解上走得很远。 - David Heffernan
2
@David:是的,你可以这样做。特别是在Java中匿名类的工作方式就是这样的 :( - Jon Skeet
1
@JonSkeet,当其中一个捕获的变量是类字段,另一个不是时会发生什么?匿名方法是否指向该实例的字段?这个实例是否以某种方式传递给生成的类?还有一个问题 - 编译器是否可能不知道哪些匿名方法共享某些捕获的变量? - Bart Juriewicz
显示剩余13条评论

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