11

I have a function which creates a delegate using expression trees. Within this expression I use a variable captured from multiple parameters passed in to the function. The actual expression tree is rather large so as an example:

Delegate GenerateFunction<T>(T current, IList<T> parents) {
    var currentExpr = Expression.Parameter(typeof(T), "current");
    var parentsExpr = Expression.Parameter(parents.getType(), "parents");
    var parameters = new List<ParameterExpression>();

    ....

    return Expression.Lambda(Expression.Block(new List<ParameterExpression> { parentsExpr, currentExpr }, ....), parameters.ToArray()).Compile();
}

I then invoke this method from another method before passing that function to another function to use. Once that's all done I want to access the content of parents which gets updated within the expression tree.

Everything seems to compile, and my expression looks ok, but when I run it I appear (although I can't really be sure) to be getting null reference exceptions when accessing the parents variable (inside the expression/closure).

I guess I'd like to know if I'm doing something wrong or whether this is possible as well as tips for understanding what's going on. I don't seem to be able to find any hoisted (?) local variables within the method so I'm wondering whether they're being captured at all?

Thanks, Mark

Mark Jerzykowski
  • 812
  • 8
  • 15
  • 1
    `parameters.ToArray()` would be an empty array, so the `ParameterExpressions` end up describing something that does not exist. – Jon Jan 29 '13 at 14:30
  • Yeah, sorry about that, it does actually get filled with some other parameters that are created in between its declaration and the return – Mark Jerzykowski Jan 29 '13 at 14:33

2 Answers2

19

I don't seem to be able to find any hoisted local variables within the method so I'm wondering whether they're being captured at all?

It looks like you are building the expression tree lambda yourself, by "manually" calling the factory methods. The compiler has no idea that that's what you're doing; it just sees method calls. If you want locals to be hoisted then you're going to have to either (1) get the compiler to do it for you, by making it rewrite the lambda, or (2) hoist 'em yourself.

That is:

int x = 123;
Expression<Func<int>> ex = ()=>x; 

the compiler rewrites the lambda and hoists it for you, as though you'd said:

Closure c = new Closure();
c.x = 123;
Expression<Func<int>> ex = ()=>c.x; 

Where c typically becomes a Constant expression.

But if you say

Expression<Func<int>> ex = Expression.Lambda( ...something that uses x ... );

the compiler has no idea that you're doing something where it needs to hoist x; x is not inside a lambda expression. If you're using the factories, the compiler assumes you know what you're doing and doesn't mess around with rewriting it. You'll have to hoist it yourself.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Ah, so how does one hoist the variable? I presumed that passing my outer variables to the Expression.Block did the hoisting for me? – Mark Jerzykowski Jan 29 '13 at 15:35
  • 2
    @MarkJerzykowski: The same way the compiler hoists the variable. Make a class, make the variable a field of the class, and then update every usage of that variable to instead refer to that field on an instance of the class. Inside the expression block you'd typically make a Constant expression that held onto an instance of the closure class, and then do a member access expression every time you want the value of the local. – Eric Lippert Jan 29 '13 at 16:03
  • You're a star, thanks very much! Might be worth noting that I initially tried to use the Closure class in System.Runtime.CompilerServices but that leads to unboxing issues so I did what I probably should have done and created a strongly typed class for my closure to use! Thanks again – Mark Jerzykowski Jan 29 '13 at 17:41
  • @Eric: Doesn't your detailed answer here: http://stackoverflow.com/questions/3716492/what-does-expression-quote-do-that-expression-constant-cant-already-do suggest that the Expression Trees can do the variable capture without one having to make the closure class explicitly? – Govert Feb 01 '14 at 10:35
2

I think you're looking for Expression.Quote, which supports the variable capture in Lambda expressions. Basically the inner LambdaExpression (which will reference the captured variables) need to be wrapped in an Expression.Quote(...) call.

Examples and discussion here: What does Expression.Quote() do that Expression.Constant() can’t already do?

Community
  • 1
  • 1
Govert
  • 16,387
  • 4
  • 60
  • 70