Expression/Statement trees

2020-06-09 08:04发布

Updated Question Further Down

I've been experimenting with expression trees in .NET 4 to generate code at runtime and I've been trying to implement the foreach statement by building an expression tree.

In the end, the expression should be able to generate a delegate that does this:

Action<IEnumerable<int>> action = source => 
{
  var enumerator = source.GetEnumerator();
  while(enumerator.MoveNext())
  {
    var i = enumerator.Current;
    // the body of the foreach that I don't currently have yet
  }
}

I've come up with the following helper method that generates a BlockExpression from an IEnumerable:

public static BlockExpression ForEachExpr<T>(this IEnumerable<T> source, string collectionName, string itemName)
{
        var item = Expression.Variable(typeof(T), itemName);

        var enumerator = Expression.Variable(typeof(IEnumerator<T>), "enumerator");

        var param = Expression.Parameter(typeof(IEnumerable<T>), collectionName);

        var doMoveNext = Expression.Call(enumerator, typeof(IEnumerator).GetMethod("MoveNext"));

        var assignToEnum = Expression.Assign(enumerator, Expression.Call(param, typeof(IEnumerable<T>).GetMethod("GetEnumerator")));

        var assignCurrent = Expression.Assign(item, Expression.Property(enumerator, "Current"));

        var @break = Expression.Label();

        var @foreach = Expression.Block(
            assignToEnum,
            Expression.Loop(
                Expression.IfThenElse(
                Expression.NotEqual(doMoveNext, Expression.Constant(false)),
                    assignCurrent
                , Expression.Break(@break))
            ,@break)
        );
        return @foreach;

}

The following code:

var ints = new List<int> { 1, 2, 3, 4 };
var expr = ints.ForEachExpr("ints", "i");
var lambda = Expression.Lambda<Action<IEnumerable<int>>>(expr, Expression.Parameter(typeof(IEnumerable<int>), "ints"));

Generates this expression tree:

.Lambda #Lambda1<System.Action`1[System.Collections.Generic.IEnumerable`1[System.Int32]]>(System.Collections.Generic.IEnumerable`1[System.Int32] $ints)
{
    .Block() {
        $enumerator = .Call $ints.GetEnumerator();
        .Loop  {
            .If (.Call $enumerator.MoveNext() != False) {
                $i = $enumerator.Current
            } .Else {
                .Break #Label1 { }
            }
        }
        .LabelTarget #Label1:
    }
}

This seems to be OK, but calling Compile on that expression results in an exception:

"variable 'enumerator' of type 'System.Collections.Generic.IEnumerator`1[System.Int32]' referenced from scope '', but it is not defined"

Didn't I define it here:

    var enumerator = Expression.Variable(typeof(IEnumerator<T>), "enumerator");

?

Of course, the example here is contrived and doesn't have a practical use yet, but I'm trying to get the hang of expression trees that have bodies, in order to dynamically combine them at runtime in the future.


EDIT: My initial problem was solved by Alexandra, thanks! Of course, I've run into the next problem now. I've declared a BlockExpression that has a variable in it. Inside that expression, I want another expression that references that variable. But I don't have an actual reference to that variable, just its name, because the expression is supplied externally.

var param = Expression.Variable(typeof(IEnumerable<T>), "something");

var block = Expression.Block(
                new [] { param },
                body
            );

The body variable is passed in externally and has no direct reference to param, but does know the name of the variable in the expression ("something"). It looks like this:

var body = Expression.Call(typeof(Console).GetMethod("WriteLine",new[] { typeof(bool) }), 
               Expression.Equal(Expression.Parameter(typeof(IEnumerable<int>), "something"), Expression.Constant(null)));

This is the "code" that this generates:

.Lambda #Lambda1<System.Action`1[System.Collections.Generic.IEnumerable`1[System.Int32]]>(System.Collections.Generic.IEnumerable`1[System.Int32] $something)
{
    .Block(System.Collections.Generic.IEnumerable`1[System.Int32] $something) {
        .Call System.Console.WriteLine($something== null)
    }
}

However, it doesn't compile. With the same error as before.

TLDR: How do I reference a variable by identifier in an expression tree?

3条回答
Juvenile、少年°
2楼-- · 2020-06-09 08:22

Don't forget to dispose IEnumerator in try/finally - lots of code (such as File.ReadLines()) depends on that.

查看更多
我只想做你的唯一
3楼-- · 2020-06-09 08:30

You problem is that you didn't pass parameters and variables to your block expression. You use them in the "inner" expressions, but the block expression knows nothing about them. Basically, all you need to do is to pass all your parameters and variables to a block expression.

        var @foreach = Expression.Block(
            new ParameterExpression[] { item, enumerator, param },
            assignToEnum,
            Expression.Loop(
                Expression.IfThenElse(
                    Expression.NotEqual(doMoveNext, Expression.Constant(false)),
                    assignCurrent,
                    Expression.Break(@break))
            , @break)
        );
查看更多
Luminary・发光体
4楼-- · 2020-06-09 08:38

Sorry if this is thread necromancy, but in case other people are running into the same or similar problem:

You can try to write an ExpressionVisitor that replaces a parameter with the same name and type in the external body expression with the variable parameter you have declared when creating the block expression. That way the parameter in the body will be the same object as the parameter in the block declaration and so the LambdaExpression should compile.

查看更多
登录 后发表回答