我觉得我对闭包有很好的理解,如何使用它们,以及它们何时有用。但我不明白的是他们实际上是如何在幕后的幕后工作的。一些示例代码:
public Action Counter()
{
int count = 0;
Action counter = () =>
{
count++;
};
return counter;
}
通常情况下,如果闭包没有捕获{count},它的生命周期将限定为Counter()方法,并且在它完成之后它将消除Counter()的其余堆栈分配。什么时候关闭会发生什么?这个Counter()调用的整个堆栈分配是否存在?它会将{count}复制到堆中吗?它是否从未实际分配到堆栈中,但是被编译器识别为关闭,因此总是存在于堆上?
对于这个特殊问题,我主要关注它在C#中是如何工作的,但不会反对与支持闭包的其他语言进行比较。
答案 0 :(得分:46)
你的第三个猜测是正确的。编译器将生成如下代码:
private class Locals
{
public int count;
public void Anonymous()
{
this.count++;
}
}
public Action Counter()
{
Locals locals = new Locals();
locals.count = 0;
Action counter = new Action(locals.Anonymous);
return counter;
}
有意义吗?
另外,您要求进行比较。 VB和JScript都以完全相同的方式创建闭包。
答案 1 :(得分:33)
编译器(与运行时相对)创建另一个类/类型。您的闭包函数以及您关闭/提升/捕获的任何变量将作为该类的成员在您的代码中重写。 .Net中的闭包实现为此隐藏类的一个实例。
这意味着你的count变量完全是一个不同类的成员,并且该类的生命周期与任何其他clr对象一样。在它不再生根之前,它不符合垃圾收集的条件。这意味着只要你有一个可调用的方法引用它就不会去任何地方。
答案 2 :(得分:0)
谢谢@HenkHolterman。由于已经由Eric解释过,我添加了链接只是为了显示编译器为闭包生成的实际类。我想补充一点,C#编译器创建显示类可能会导致内存泄漏。例如,在函数内部有一个由lambda表达式捕获的int变量,另一个局部变量只保存对大字节数组的引用。编译器将创建一个显示类实例,它将保存对两个变量的引用,即int和byte数组。但是,在引用lambda之前,字节数组不会被垃圾收集。
答案 3 :(得分:0)
Eric Lippert的回答确实很明显。然而,建立一个堆栈帧和捕获如何工作的图片会很好。要做到这一点,有助于查看稍微复杂的示例。
以下是捕获代码:
public class Scorekeeper {
int swish = 7;
public Action Counter(int start)
{
int count = 0;
Action counter = () => { count += start + swish; }
return counter;
}
}
以下是我认为相同的内容(如果我们幸运的话,Eric Lippert会评论这是否真的正确):
private class Locals
{
public Locals( Scorekeeper sk, int st)
{
this.scorekeeper = sk;
this.start = st;
}
private Scorekeeper scorekeeper;
private int start;
public int count;
public void Anonymous()
{
this.count += start + scorekeeper.swish;
}
}
public class Scorekeeper {
int swish = 7;
public Action Counter(int start)
{
Locals locals = new Locals(this, start);
locals.count = 0;
Action counter = new Action(locals.Anonymous);
return counter;
}
}
重点是本地类替换整个堆栈帧,并在每次调用Counter方法时进行相应的初始化。通常,堆栈帧包括对“this”的引用,加上方法参数以及局部变量。 (进入控制块时,堆栈帧也会有效扩展。)
因此,我们没有一个对应于捕获的上下文的对象,而是每个捕获的堆栈帧实际上有一个对象。
基于此,我们可以使用以下心理模型:堆栈帧保存在堆上(而不是堆栈上),而堆栈本身只包含指向堆上堆栈帧的指针。 Lambda方法包含指向堆栈帧的指针。这是使用托管内存完成的,因此框架会粘在堆上,直到不再需要它为止。
显然,当需要堆对象支持lambda闭包时,编译器只能使用堆来实现它。
我喜欢这个模型的是它提供了“收益率回报”的综合图片。我们可以想到一个迭代器方法(使用yield return),好像它的堆栈帧是在堆上创建的,而引用指针存储在调用者的局部变量中,以便在迭代期间使用。