Search code examples
c#javascriptvb.netscopescope-chain

does C# and VB lambdas have **scope chain** issues similar to javascript?


I've read that due to how the scope chain works in javascript, if we wish to refer to a variable V within a function F that is not declared within the F's scope, it is beneficial (yes in terms of performance) to declare a local variable V2 in F that references V, then accessing the object referenced by V through V2.

i'm wondering if this concept applies to the closures in C# and VB (accessing local variables in functions through lambdas)

Public Shared Function Example()
    Dim a = 1
    Dim b = New Object
    Return Sub()
               'when we use the variables a and b from here does it have to "go up the scope chain"
           End Sub
End Function

btw i would prefer if the answer isn't premature optimization is the root of all evil


Solution

  • Short answer: no. .NET doesn't need to walk up the scope chain to find the variables.

    Long answer:

    Start with this example:

    static Func<string> CaptureArgs(int a, int b)
    {
        return () => String.Format("a = {0}, b = {1}", a, b);
    }
    
    static void Main(string[] args)
    {
        Func<string> f = CaptureArgs(5, 10);
        Console.WriteLine("f(): {0}", f());
        // prints f(): a = 5, b = 10
    }
    

    In the CaptureArgs method, a and b exist on the stack. Intuitively, if we reference the variables in an anonymous function, return the function and popping the stack frame should remove a and b from memory. (This is called the upward funargs problem).

    C# doesn't suffer from the upwards funargs problem because, behind the scenes, an anonymous function is just fancy syntax sugar over a compiler-generated class. The C# code above turns into:

    private sealed class <>c__DisplayClass1
    {
        // Fields
        public int a;
        public int b;
    
        // Methods
        public string <CaptureArgs>b__0()
        {
            return string.Format("a = {0}, b = {1}", this.a, this.b);
        }
    }
    

    The compiler creates and returns a new instance of <>c__DisplayClass1, initializes its a and b fields from the a and b passed into the CaptureArgs method (this effectively copies a and b from the stack to fields existing on the heap), and returns it to the caller. Calling f() is really a call to <>c__DisplayClass1.<CaptureArgs>b__0().

    Since the a and b referenced in <CaptureArgs>b__0 are vanilla fields, they can be referenced directly by the delegate, they don't require any special sort of scope chaining rules.