Issue with closure variable capture in c# expressi

2019-02-09 01:23发布

问题:

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

回答1:

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.



回答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?