封闭如何在幕后工作? (C#)
我觉得我对闭包有一个相当好的理解,如何使用它们,什么时候可以有用。 但是我不明白的是他们是如何在记忆中幕后工作的。 一些示例代码:
public Action Counter() { int count = 0; Action counter = () => { count++; }; return counter; }
通常情况下,如果{count}没有被闭包捕获,它的生命周期将被限制在Counter()方法中,并且在它完成之后,它将消失Counter()的其余堆栈分配。 当它被closures时会发生什么? Counter()这个调用的整个堆栈分配是否存在? 它是否将{count}复制到堆中? 它从来没有真的被分配到堆栈上,但被编译器认为是closures的,因此总是在堆上?
对于这个特定的问题,我主要关心的是如何在C#中工作,但不会反对支持闭包的其他语言进行比较。
编译器 (而不是运行时)创build另一个类/types。 你的闭包函数和你closures/悬挂/捕获的任何variables在你的代码中作为这个类的成员被重写。 .Net中的闭包被实现为这个隐藏类的一个实例。
这意味着你的countvariables完全是一个不同类的成员,并且该类的生命周期和其他clr对象一样工作。 它不符合垃圾收集的条件,直到它不再根植。 这意味着只要你有一个可调用的方法,它不会去任何地方。
你的第三个猜测是正确的。 编译器会生成这样的代码:
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都以几乎相同的方式创build闭包。
谢谢@HenkHolterman。 由于Eric已经解释过,我添加了这个链接来显示编译器生成什么样的实际类来closures它。 我想补充一点,用C#编译器创build显示类可能导致内存泄漏。 例如,在一个函数里面有一个由lambdaexpression式捕获的intvariables,还有另外一个局部variables,它保存对大字节数组的引用。 编译器将创build一个显示类实例,该实例将保存对int和字节数组这两个variables的引用。 但是,在引用lambda之前,字节数组不会被垃圾收集。
埃里克·利波特的回答真的很重要。 但是一般来说,build立一个堆栈帧和捕获图像的工作方式是很好的。 要做到这一点有助于看一个稍微复杂的例子。
这里是捕获代码:
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”的引用,加上方法参数以及局部variables。 (input控制块时,堆栈框架也会生效。)
因此,我们不只有一个对象与捕获的上下文相对应,实际上,我们每个捕获的堆栈框架都有一个对象。
基于这一点,我们可以使用下面的心智模型:堆栈帧保留在堆上(而不是堆栈上),而堆栈本身只包含指向堆上的堆栈帧的指针。 Lambda方法包含一个指向栈帧的指针。 这是通过使用托pipe内存完成的,所以框架会一直粘在堆上,直到不再需要为止。
当堆对象被要求支持一个lambda闭包时,编译器显然可以通过只使用堆实现这一点。
我喜欢这个模型,它提供了一个“收益回报”的综合图片。 我们可以想象一个迭代器方法(使用yield return),就好像它的堆栈是在堆上创build的,并且引用指针存储在调用者的局部variables中,以便在迭代过程中使用。