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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With