What are 'closures' in .NET?

前端 未结 12 1934
执念已碎
执念已碎 2020-11-21 13:53

What is a closure? Do we have them in .NET?

If they do exist in .NET, could you please provide a code snippet (preferably in C#) explaining it?

12条回答
  •  别那么骄傲
    2020-11-21 14:11

    If you write an inline anonymous method (C#2) or (preferably) a Lambda expression (C#3+), an actual method is still being created. If that code is using an outer-scope local variable - you still need to pass that variable to the method somehow.

    e.g. take this Linq Where clause (which is a simple extension method which passes a lambda expression):

    var i = 0;
    var items = new List
    {
        "Hello","World"
    };   
    var filtered = items.Where(x =>
    // this is a predicate, i.e. a Func written as a lambda expression
    // which is still a method actually being created for you in compile time 
    {
        i++;
        return true;
    });
    

    if you want to use i in that lambda expression, you have to pass it to that created method.

    So the first question that arises is: should it be passed by value or reference?

    Pass by reference is (I guess) more preferable as you get read/write access to that variable (and this is what C# does; I guess the team in Microsoft weighed the pros and cons and went with by-reference; According to Jon Skeet's article, Java went with by-value).

    But then another question arises: Where to allocate that i?

    Should it actually/naturally be allocated on the stack? Well, if you allocate it on the stack and pass it by reference, there can be situations where it outlives it's own stack frame. Take this example:

    static void Main(string[] args)
    {
        Outlive();
        var list = whereItems.ToList();
        Console.ReadLine();
    }
    
    static IEnumerable whereItems;
    
    static void Outlive()
    {
        var i = 0;
        var items = new List
        {
            "Hello","World"
        };            
        whereItems = items.Where(x =>
        {
            i++;
            Console.WriteLine(i);
            return true;
        });            
    }
    

    The lambda expression (in the Where clause) again creates a method which refers to an i. If i is allocated on the stack of Outlive, then by the time you enumerate the whereItems, the i used in the generated method will point to the i of Outlive, i.e. to a place in the stack that is no longer accessible.

    Ok, so we need it on the heap then.

    So what the C# compiler does to support this inline anonymous/lambda, is use what is called "Closures": It creates a class on the Heap called (rather poorly) DisplayClass which has a field containing the i, and the Function that actually uses it.

    Something that would be equivalent to this (you can see the IL generated using ILSpy or ILDASM):

    class <>c_DisplayClass1
    {
        public int i;
    
        public bool b__0()
        {
            this.i++;
            Console.WriteLine(i);
            return true;
        }
    }
    

    It instantiates that class in your local scope, and replaces any code relating to i or the lambda expression with that closure instance. So - anytime you are using the i in your "local scope" code where i was defined, you are actually using that DisplayClass instance field.

    So if I would change the "local" i in the main method, it will actually change _DisplayClass.i ;

    i.e.

    var i = 0;
    var items = new List
    {
        "Hello","World"
    };  
    var filtered = items.Where(x =>
    {
        i++;
        return true;
    });
    filtered.ToList(); // will enumerate filtered, i = 2
    i = 10;            // i will be overwriten with 10
    filtered.ToList(); // will enumerate filtered again, i = 12
    Console.WriteLine(i); // should print out 12
    

    it will print out 12, as "i = 10" goes to that dispalyclass field and changes it just before the 2nd enumeration.

    A good source on the topic is this Bart De Smet Pluralsight module (requires registration) (also ignore his erroneous use of the term "Hoisting" - what (I think) he means is that the local variable (i.e. i) is changed to refer to the the new DisplayClass field).


    In other news, there seems to be some misconception that "Closures" are related to loops - as I understand "Closures" are NOT a concept related to loops, but rather to anonymous methods / lambda expressions use of local scoped variables - although some trick questions use loops to demonstrate it.

提交回复
热议问题