I’m interested in retrieving the names of local variables (and parameters) at run-time in a refactor-safe manner. I have the following extension method:
public static string GetVariableName<T>(Expression<Func<T>> variableAccessExpression)
{
var memberExpression = variableAccessExpression.Body as MemberExpression;
return memberExpression.Member.Name;
}
…which returns the name of the variable captured through a lambda expression:
static void Main(string[] args)
{
Console.WriteLine(GetVariableName(() => args));
// Output: "args"
int num = 0;
Console.WriteLine(GetVariableName(() => num));
// Output: "num"
}
However, this only works because the C# compiler promotes any local variables (and parameters) that are captured in anonymous functions to instance variables of the same name within a compiler-generated class behind the scenes (per Jon Skeet). If this were not the case, the cast of Body
to MemberExpression
would fail, since MemberExpression
represents field or property access.
Is this variable promotion documented behaviour, or is it an implementation detail subject to change in other versions of the framework?
Note: This question is a generalization of my former one on argument validation.
A lambda expression can't define any new scope as an anonymous inner class does, so we can't declare a local variable with the same which is already declared in the enclosing scope of a lambda expression. Inside lambda expression, we can't assign any value to some local variable declared outside the lambda expression.
The rule is that a lambda expression can only access local variables from an enclosing scope that are effectively final. An effectively final variable is never modified—it either is or could be declared as final.
A lambda expression can access a variable of it's enclosing scope. A Lambda expression has access to both instance and static variables of it's enclosing class and also it can access local variables which are effectively final or final.
Forcing the variable to be final avoids giving the impression that incrementing start inside the lambda could actually modify the start method parameter.
Update: This is no longer an issue from C# 6, which has introduced the
nameof
operator to address such scenarios (see MSDN).
It appears that the answer to my question is no; the feature is non-standardized. The situation seems even bleaker than I’d originally suspected; not only is the promotion of captured variables non-standardized, but so is the entire specification of converting anonymous functions to their expression tree representations.
The implication of this is that even straightforward anonymous functions, such as the below, are not guaranteed to result in consistent expression trees across different implementations of the framework (until the conversion is standardized):
Expression<Func<int, int, int>> add = (int x, int y) => x + y;
The following excerpts are taken from the C# Language Specification 4.0 (emphasis added in all cases).
From “4.6 Expression tree types”:
The exact definition of the generic type
Expression<D>
as well as the precise rules for constructing an expression tree when an anonymous function is converted to an expression tree type, are both outside the scope of this specification, and are described elsewhere.
From “6.5.2 Evaluation of anonymous function conversions to expression tree types”:
Conversion of an anonymous function to an expression tree type produces an expression tree (§4.6). More precisely, evaluation of the anonymous function conversion leads to the construction of an object structure that represents the structure of the anonymous function itself. The precise structure of the expression tree, as well as the exact process for creating it, are implementation defined.
The third example in “6.5.3 Implementation example” demonstrates the conversion of an anonymous function that captures a local variable, and confirms the variable promotion mentioned in my question:
The lifetime of the local variable must now be extended to at least the lifetime of the anonymous function delegate. This can be achieved by “hoisting” the local variable into a field of a compiler generated class. Instantiation of the local variable (§7.15.5.2) then corresponds to creating an instance of the compiler generated class, and accessing the local variable corresponds to accessing a field in the instance of the compiler generated class.
This is further corroborated at the end of the section:
The same technique applied here to capture local variables can also be used when converting anonymous functions to expression trees: References to the compiler generated objects can be stored in the expression tree, and access to the local variables can be represented as field accesses on these objects. The advantage of this approach is that it allows the “lifted” local variables to be shared between delegates and expression trees.
However, there is a disclaimer at the beginning of the section:
The implementation described here is based on the same principles used by the Microsoft C# compiler, but it is by no means a mandated implementation, nor is it the only one possible. It only briefly mentions conversions to expression trees, as their exact semantics are outside the scope of this specification.
P.S. Eric Lippert confirms in this comment that the expression tree specs were never shipped. There exists an Expression Trees v2 Spec under the DLR documentation on CodePlex, but its scope does not appear to cover the conversion of anonymous functions to expression trees in C#.
This is a behavior you should not rely upon.
Take a look at Abuse of C# lambda expressions or Syntax brilliance?
Now read the comments from Eric Lippert who is on the C# design team. They include:
I just asked Anders (and the rest of the design team) what they thought. Let's just say the results would not be printable in a family-friendly newspaper
and
As for why this is horrid, we could start with unobvious, clever (remember, clever is bad, clever code is hard to maintain), not at all within the by-design use cases envisaged by the designers of lambdas, slow, brittle, unportable, and unnecessary
From these statements I would say that it will not become a documented or supported behavior.
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