闭包中的变量捕获详解
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,当捕获的变量之一是类字段而另一个不是时会发生什么?匿名方法是否指向该实例的字段?这个实例是否以某种方式传递给生成的类?还有一个问题 - 编译器是否可能不知道哪些匿名方法共享某些捕获的变量?以上是关于闭包中的变量捕获详解的主要内容,如果未能解决你的问题,请参考以下文章