Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does getting a member expression member name differ between C# and VB.NET?

I have the following C# method:

private static string GetMemberName<T>(Expression<Func<T>> expr)
{
    MemberExpression memberExpr = expr.Body as MemberExpression;

    if (memberExpr == null) 
        throw new ArgumentOutOfRangeException("Wrong type of lambda...");

    return memberExpr.Member.Name;
}

And I can use it like this to print the name of a class-level field, method param, or local var (note this is pre-C# 6.0 nameof operator):

private static int _myFieldVar  = 62;

private static void DoStuff(int myMethodParam)
{
    int myLocalVar = 2;
    Debug.Print(GetMemberName(() => myMethodParam)); // prints "myMethodParam"
    Debug.Print(GetMemberName(() => myLocalVar)); // prints "myLocalVar"
    Debug.Print(GetMemberName(() => _myFieldVar)); // _myFieldVariable
}

Now I want to convert this code to VB.NET, so here is the GetMemberName method:

Private Function GetMemberName(Of T)(expr As Expression(Of Func(Of T))) As String
    Dim memberExpr As MemberExpression = TryCast(expr.Body, MemberExpression)

    If memberExpr Is Nothing Then _
        Throw New ArgumentOutOfRangeException("Wrong type of lambda...")

    Return memberExpr.Member.Name
End Function

However, I'm noticing different results when I get the method param and local variable names, i.e. they are both prefixed with "$VB$Local_":

Private _myFieldVar As Integer = 62

Private Sub DoThis(myMethodParam As Integer)
    Dim myLocalVar = 2
    Debug.Print(GetMemberName(Function() myMethodParam)) ' prints "$VB$Local_myMethodParam""
    Debug.Print(GetMemberName(Function() myLocalVar)) ' prints "$VB$Local_myLocalVar"
    Debug.Print(GetMemberName(Function() _myFieldVar)) ' prints "_myFieldVar()" 
End Sub

I googled "$VB$Local_" and found this post which is very similar. However, I think my question is different because I'm not getting this behavior with properties. If I call this:

Debug.Print(GetMemberName(Function() MyProperty))

I get "MyProperty". Moreover, my fundamental question is "why is the behavior different between C# and VB.NET, i.e. what is the meaning of "$VB$Local_" and why is it absent in C#", whereas that post is more concerned with how to avoid that behavior in VB.NET.

like image 871
rory.ap Avatar asked Mar 31 '16 16:03

rory.ap


1 Answers

As mentioned by Hans Passant the 2 compilers use slightly different naming strategies for handling local variables within a Linq expression tree. Let's take a look at what both output's look like decompiled. I used ILSpy with all of the options unchecked in View => Options => decompiler tab. I also simplified the output expression tree a bit to keep the answer concise.

C#

    public static string Test()
    {
        int myLocalVar = 2;
        int myLocalVar2 = 2; // local varible never exported! note how it is not in the generated class
        myLocalVar2++;
        return GetMemberName(() => myLocalVar);
    }

output

    [CompilerGenerated]
    private sealed class <>c__DisplayClass1_0
    {
        public int myLocalVar;
    }

    public static string Test()
    {
        Class1.<>c__DisplayClass1_0 <>c__DisplayClass1_ = new Class1.<>c__DisplayClass1_0();
        <>c__DisplayClass1_.myLocalVar = 2;
        int num = 2;
        num++;
        return Class1.GetMemberName<int>(
            Expression.Lambda<Func<int>>(
                Expression.MakeMemberAccess(
                    Expression.Constant(<>c__DisplayClass1_, typeof(Class1.<>c__DisplayClass1_0)),
                    typeof(Class1.<>c__DisplayClass1_0).GetMember("myLocalVar")
                )
            )
        );
    }

VB

Public Shared Function Test() As String
    Dim myLocalVar As Integer = 2
    Dim myLocalVar2 As Integer = 2
    myLocalVar2 = myLocalVar2 + 1
    Return GetMemberName(Function() myLocalVar)
End Function

output

    [CompilerGenerated]
    internal sealed class _Closure$__2-0
    {
        public int $VB$Local_myLocalVar;
    }

    public static string Test()
    {
        Class1._Closure$__2-0 closure$__2- = new Class1._Closure$__2-0();
        closure$__2-.$VB$Local_myLocalVar = 2;
        int num = 2;
        num++;
        return Class1.GetMemberName<int>(
            Expression.Lambda<Func<int>>(
                Expression.MakeMemberAccess(
                    Expression.Constant(closure$__2-, typeof(Class1._Closure$__2-0)), 
                    typeof(Class1._Closure$__2-0).GetMember("$VB$Local_myLocalVar")
                )
            )
        );
    }

Both compilers create a private sealed class for myLocalVar. This is done to satisfy the requirements of the Linq expression tree. The expression tree needs to capture a reference to the local variable. The below example shows why this is needed.

       int localVar = 1;
       Expression<Func<int>> express = () => localVar;
       var compiledExpression = express.Compile();
       Console.WriteLine(compiledExpression());//1
       localVar++;
       Console.WriteLine(compiledExpression());//2
       Console.ReadLine();

Back to the question- why is the behavior different between C# and VB.NET, i.e. what is the meaning of "$VB$Local_" and why is it absent in C#?

The compilers generate an unbelievable amount of code for us, C# and VB.NET both do it slightly diffrently. So I am only going to answer why does VB insert $VB$Local_. ** To avoid name collisions.** Both C#'s DisplayClass and VB.Net's Closure is used for more than one purpose. To keep from having a collision, the name is prefixed with a key that represents the source. It just works out that C#'s key is nothing, all other language features that contribute to DisplayClass prefix with something else. Try decompiling the folowing VB.net to see why the prefix key is needed.

Sub Main()
    Dim myLocalVar As Integer = 2
    Dim x1 As System.Action(Of Integer) = Sub(x)
                                              System.Console.WriteLine(x)
                                              GetMemberName(Function() myLocalVar)

                                          End Sub
    x1(2)
End Sub

The compiled closure will be the following.

    [CompilerGenerated]
    internal sealed class _Closure$__2-0
    {
        public int $VB$Local_myLocalVar;

        internal void _Lambda$__0(int x)
        {
            Console.WriteLine(x);
            Class1.GetMemberName<int>(Expression.Lambda<Func<int>>(Expression.Field(Expression.Constant(this, typeof(Class1._Closure$__2-0)), fieldof(Class1._Closure$__2-0.$VB$Local_myLocalVar)), new ParameterExpression[0]));
        }

async and await also use closures in this way, however they generate a lot of boilerplate code that I didn't want to paste in here.


Final remarks

Your passing in a local variable and parameter into a method named GetMemberName. It was only luck that under the hood the 2 compilers automatically generated types and members to satisfy linq expression trees in the first place. Fortunately the latest iteration of the compilers have the nameof operator, which is a much better way to handle this problem.

like image 102
Chad Carisch Avatar answered Nov 15 '22 00:11

Chad Carisch