Reputation: 988
Usually, when compiling an expression tree, I would have thought constants which are not primitives types or strings would be impossible. However, this code:
public class A
{ public int mint = -1; }
public static void Main(String[] pArgs)
{
//Run(pArgs);
Action pact = Thing();
pact();
}
public static Action Thing()
{
var a = new A();
a.mint = -1;
LambdaExpression p =
Expression.Lambda<Action>(Expression.Assign(Expression.Field(Expression.Constant(a, typeof(A)), Strong.Instance<A>.Field<int>(b => b.mint)), Expression.Constant(3, typeof(int))));
return ((Expression<Action>)p).Compile();
}
not only compiles but it actually runs! If you run the compiled method in the Thing() method, then you can actually see the variable a change its field from -1 to 3
I don't see how this makes sense/is possible. How can a method refer to a local variable outside its scope (when inspecting the IL of Thing(), the variable a is just a standard local variable, not on the heap like with a closure). Is there some kind of hidden context past around? How can pact run in Main when the local variable a has presumably been removed from the stack!
Upvotes: 0
Views: 150
Reputation: 113302
How can a method refer to a local variable outside its scope
It can't, and it doesn't.
It can sometimes refer to the object the local variable points to though.
Whether it can or not depends on which way the expression is compiled or otherwise used.
There are three ways that Expressions itself will compile an expression into a method:
Compile()
to IL in a DynamicMethod
.CompileToMethod()
(not available in all versions.Compile()
into an interpreted set of instructions with a thunk delegate that runs the interpretation.The first is used if IL compilation is available, unless true
is passed to prefer interpretation (on those version with that overload) and interpretation is not also available. Here an array is used for the closure and it is much the same as closing over a local is, in a delegate.
The second is used to write to another assembly and cannot close in this way. Many constants that will work with Compile()
will not work with CompileToMethod()
for this reason.
The third is used if IL compilation is not available, or true
was passed in those versions that have that overload to prefer interpretation. Here a reference to the object is put into an array of "constants" that the interpreter can then refer to.
The other possibility is that something else interprets the expression entirely, e.g. in producing SQL code. Generally this will fail with non-primitive constants other than string though, but if the query processor is aware of the type of the constant (e.g. if it is of a type of entity that it knows about) then code to produce the equivalent to that entity could be produced.
Upvotes: 1
Reputation: 1063058
It is only a
that is a local variable; the actual object (from new A()
) was always on the heap. When you used Expression.Constant(a, typeof(A))
, it wasn't a
that you put in as a constant - it was the value of a
, i.e. the object reference. So: the scope of a
is irrelevant as far as the tree is concerned. This is actually exactly how captured variables (closures) are usually implemented by the compiler (although you don't see it usually, and the C#-based expression compiler doesn't allow assignment operators), so as far as the expression tree is concerned: this is business as usual.
As a comparable example using the C# expression compiler, see here, where
public void M() {
int mint = -1;
Expression<Func<int>> lambda = () => mint;
}
compiles to:
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
public int mint;
}
public void M()
{
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.mint = -1;
Expression.Lambda<Func<int>>(Expression.Field(Expression.Constant(<>c__DisplayClass0_, typeof(<>c__DisplayClass0_0)), FieldInfo.GetFieldFromHandle((RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/)), Array.Empty<ParameterExpression>());
}
Upvotes: 0