How do closures work behind the scenes? (C#)

前端 未结 4 955
挽巷
挽巷 2020-11-27 14:41

I feel I have a pretty decent understanding of closures, how to use them, and when they can be useful. But what I don\'t understand is how they actually work behind the sce

相关标签:
4条回答
  • 2020-11-27 15:04

    The compiler (as opposed to the runtime) creates another class/type. The function with your closure and any variables you closed over/hoisted/captured are re-written throughout your code as members of that class. A closure in .Net is implemented as one instance of this hidden class.

    That means your count variable is a member of a different class entirely, and the lifetime of that class works like any other clr object; it's not eligible for garbage collection until it's no longer rooted. That means as long as you have a callable reference to the method it's not going anywhere.

    0 讨论(0)
  • 2020-11-27 15:13

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

    Here is the capturing 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 actually 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 point is that the local class substitutes for the entire stack frame and is initialized accordingly each time the Counter method is invoked. Typically the stack frame includes a reference to 'this', plus method arguments, plus local variables. (The stack frame is also in effect extended when a control block is entered.)

    Consequently we do not have just one object corresponding to the captured context, instead we actually have one object per captured stack frame.

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

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

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

    0 讨论(0)
  • 2020-11-27 15:19

    Thanks @HenkHolterman. Since it was already explained by Eric, I added the link just to show what actual class the compiler generates for closure. I would like to add to that the creation of display classes by C# compiler can lead to memory leaks. For example inside a function there a int variable that is captured by a lambda expression and there another local variable that simply holds a reference to a large byte array. Compiler would create one display class instance which will hold the references to both the variables i.e. int and the byte array. But the byte array will not be garbage collected till the lambda is being referenced.

    0 讨论(0)
  • 2020-11-27 15:20

    Your third guess is correct. The compiler will generate code like this:

    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?

    Also, you asked for comparisons. VB and JScript both create closures in pretty much the same way.

    0 讨论(0)
提交回复
热议问题