闭包中变量捕获的详细说明

Duc*_*tro 57 .net c# closures value-type reference-type

我已经看到关于变量捕获如何为变量创建闭包的无数帖子,但是它们似乎都没有具体细节,并且把整个事情称为"编译魔术".

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

  1. 如何实际捕获局部变量.
  2. 捕获值类型与引用类型之间的差异(如果有).
  3. 并且是否存在关于值类型的任何装箱.

我倾向于根据值和指针(更接近内部发生的核心)的答案,尽管我会接受一个涉及值和引用的明确答案.

Jon*_*eet 79

  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++;
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

现在这就是编译器为你做的 - 除了它会使用在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++;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果捕获循环中声明的变量,则最终会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);
    }
}
Run Code Online (Sandbox Code Playgroud)

...和扩展:

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++;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @Jon您说捕获是变量而不是值。我猜这意味着,如果在同一方法中声明的两个lambda引用相同的变量,那么它们都将捕获相同的变量。如果一个lambda变量修改了该变量,则另一个变量会看到该变量保留的修改后的值。还是我印象深刻? (2认同)
  • @David:是的,完全正确。在这种情况下,生成的类中将有两个实例方法,并且两个委托都将引用相同的目标实例。 (2认同)
  • @乔恩谢谢。我实际上不懂任何 C#,我的大部分时间都花在 Delphi 上。Delphi 等效项的行为方式相同。在大多数用例中它往往不会出现,但我认为大多数人天真的期望是捕获值。你可能会因为这个特定的误解而走得很远。 (2认同)
  • @David:是的,你可以.特别是因为这是匿名类在Java中的工作方式:( (2认同)