How does closure work behind the scenes? (FROM#)

I feel that I have a pretty decent understanding of closures, how to use them, and when they can be useful. But I don’t understand how they actually work behind the scenes in memory. Code example:

public Action Counter() { int count = 0; Action counter = () => { count++; }; return counter; } 

Usually, if {count} was not captured by the closure, its life cycle will be tied to the Counter () method, and after its completion it will disappear with the rest of the stack allocation for Counter (). What happens when it closes? Does the entire stack allocation for this call to Counter () cover? Does he copy in a heap? It never gets allocated on the stack, but is recognized by the compiler as closed and therefore always lives on the heap?

In this particular question, I am primarily interested in how it works in C #, but I will not mind comparing it with other languages ​​that support closure.

+44
closures c #
Dec 18 '09 at 14:47
source share
4 answers

The compiler (as opposed to the runtime) creates another class / type. The function with your closure and any variables that you closed on top / raised / captured are overwritten throughout the code as members of this class. Closing in .Net is implemented as one instance of this hidden class.

This means that your variable count is a member of another class entirely, and the lifetime of this class works like any other clr object; he has no right to collect garbage until he is no longer rooted. This means that as long as you have a callable reference to a method, it will not go anywhere.

+32
Dec 18 '09 at 2:51 p.m.
source share

Your third assumption is correct. The compiler will generate the code as follows:

 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; } 

Make sense?

In addition, you requested a comparison. VB and JScript both create closures in much the same way.

+45
Dec 18 '09 at 15:15
source share

Thanks @HenkHolterman. Since this was already explained by Eric, I added a link to show what actual class the compiler generates to close. I would like to add that creating display classes using the C # compiler can lead to a memory leak. For example, inside the function there is an int variable that is captured by the lambda expression, and there is another local variable that simply contains a reference to a large array of bytes. The compiler will create one instance of the display class, which will contain references to ie int variables and an array of bytes. But the byte array will not collect garbage until lambda is specified.

0
Jul 05 '14 at 19:28
source share

Eric Lippert's answer really hits the point. However, it would be nice to create a picture of how the frames and frames of the stack work. To do this, it helps to look at a slightly more complex example.

Here is the capture code:

 public class Scorekeeper { int swish = 7; public Action Counter(int start) { int count = 0; Action counter = () => { count += start + swish; } return counter; } } 

And here is what I think the equivalent would be (if we are lucky, Eric Lippert will comment on whether this is really correct or not):

 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; } } 

The fact is that the local class replaces the entire frame of the stack and is initialized accordingly each time the Counter method is called. Typically, a stack frame includes a reference to 'this', plus method arguments, as well as local variables. (The stack frame also works when the control unit is entered.)

Therefore, we do not have only one object corresponding to the captured context; instead, we actually have one object for each captured frame of the stack.

Based on this, we can use the following mental model: the frames of the stack are stored on the heap (and not on the stack), while the stack itself contains pointers to the frames of the stack that are on the heap. Lambda methods contain a pointer to the stack stack. This is done using managed memory, so the frame overlaps the heap until it is no longer needed.

Obviously, the compiler can implement this using only the heap when the heap object is required to support lambda closure.

What I like about this model is the integrated picture for yield return. We can think of the iterator method (using return return) as if it were created on the heap, and the link pointer stored in the local variable in the caller for use during iteration.

0
Oct 25 '17 at 4:26
source share



All Articles