简体   繁体   English

从lambda创建表达式树时如何“取消引用”?

[英]How to 'unquote' when creating Expression tree from lambda?

Let's suppose I have some function c that return Expression : 假设我有一些返回Expression函数c

Func<int, Expression<Func<int>>> c = (int a) => () => a + 3;

Now I want to create another Expression , but during its creation I'd like to call the function c and embed its result as the part of new expression: 现在我想创建另一个Expression ,但在创建它时我想调用函数c并将其结果作为新表达式的一部分嵌入:

Expression<Func<int>> d = () => 2 + c(3);

I can't do this way because it will interpret c(3) like a functions call to be converted to expression and I'll get the error that I cant add int and Expression<Func<int>> 我不能这样做,因为它会将c(3)解释为函数调用转换为表达式,我将得到错误,我无法添加intExpression<Func<int>>

I'd like d to have a value of: 我想d有值:

(Expression<Func<int>>)( () => 2 + 3 + 3 )

I'm also interested in getting this to work on more complex expressions, not just this toy example. 我也有兴趣让这个更复杂的表达式,而不仅仅是这个玩具的例子。

How would you do it in C#? 你会怎么用C#做的?

Alternatively, how would you do it in any other CLR language that I could use in my C# project with as little hassle as possible? 或者,您如何使用我可以在我的C#项目中使用的任何其他CLR语言尽可能少的麻烦?


More complex examples: 更复杂的例子:

Func<int, Expression<Func<int>>> c = (int a) => () => a*(a + 3);
Expression<Func<int, int>> d = (x) => 2 + c(3 + x);

3+x should be evaluated just once in resulting expression even though it occurs in body of c in two places. 3+x应该只在结果表达式中评估一次,即使它出现在两个地方的c体中。


I have a strong feeling that it cannot be achieved in C# because assigning lambda to Expression is done by the compiler and is sort of compile time const expression literal. 我强烈感觉它无法在C#中实现,因为将lambda赋值给Expression是由编译器完成的,并且是编译时const表达式文字的一种。 It would be akin to making compiler that understands plain string literal "test" understand template string literal "test ${a+b} other" and C# compiler is not at this stage of development yet. 它类似于使编译器理解普通字符串文字"test"理解模板字符串文字"test ${a+b} other"而C#编译器还没有处于这个开发阶段。

So my main question actually is: 所以我的主要问题实际上是:

What CLR language supports syntax that would allow me to conveniently build Expression trees embedding parts that are constructed by other functions? 什么CLR语言支持语法,可以方便地构建嵌入由其他函数构造的部分的表达式树?

Other possibility is some library that would help me build expression trees in this way using some sorts of run-time compiled templates but I'm guessing this way I'd loose code completion for my expression code. 其他可能性是一些库,它可以帮助我使用某种运行时编译模板以这种方式构建表达式树,但我猜这种方式我的表达式代码松开了代码。


It seems that F# has ability to 'quote' and 'unquote' (splice) the code: 似乎F#能够“引用”和“取消引用”(拼接)代码:

https://docs.microsoft.com/en-us/dotnet/articles/fsharp/language-reference/code-quotations https://docs.microsoft.com/en-us/dotnet/articles/fsharp/language-reference/code-quotations

For both of your examples this can actually be done with two expression visitors (code is commented): 对于这两个示例,实际上可以使用两个表达式访问者来完成(代码已注释):

static class Extensions {
    public static TResult FakeInvoke<TResult>(this Delegate instance, params object[] parameters)
    {
        // this is not intended to be called directly
        throw new NotImplementedException();
    }

    public static TExpression Unwrap<TExpression>(this TExpression exp) where TExpression : Expression {
        return (TExpression) new FakeInvokeVisitor().Visit(exp);
    }

    class FakeInvokeVisitor : ExpressionVisitor {
        protected override Expression VisitMethodCall(MethodCallExpression node) {
            // replace FakeInvoke call
            if (node.Method.Name == "FakeInvoke") {
                // first obtain reference to method being called (so, for c.FakeInvoke(...) that will be "c")
                var func = (Delegate)Expression.Lambda(node.Arguments[0]).Compile().DynamicInvoke();
                // explore method argument names and types
                var argumentNames = new List<string>();
                var dummyArguments = new List<object>();
                foreach (var arg in func.Method.GetParameters()) {
                    argumentNames.Add(arg.Name);
                    // create default value for each argument
                    dummyArguments.Add(arg.ParameterType.IsValueType ? Activator.CreateInstance(arg.ParameterType) : null);
                }
                // now, invoke function with default arguments to obtain expression (for example, this one () => a*(a + 3)).
                // all arguments will have default value (0 in this case), but they are not literal "0" but a reference to "a" member with value 0
                var exp = (Expression) func.DynamicInvoke(dummyArguments.ToArray());
                // this is expressions representing what we passed to FakeInvoke (for example expression (x + 3))
                var argumentExpressions = (NewArrayExpression)node.Arguments[1];
                // now invoke second visitor
                exp = new InnerFakeInvokeVisitor(argumentExpressions, argumentNames.ToArray()).Visit(exp);
                return ((LambdaExpression)exp).Body;
            }
            return base.VisitMethodCall(node);
        }
    }

    class InnerFakeInvokeVisitor : ExpressionVisitor {
        private readonly NewArrayExpression _args;
        private readonly string[] _argumentNames;
        public InnerFakeInvokeVisitor(NewArrayExpression args, string[] argumentNames) {
            _args =  args;
            _argumentNames = argumentNames;
        }
        protected override Expression VisitMember(MemberExpression node) {
            // if that is a reference to one of our arguments (for example, reference to "a")
            if (_argumentNames.Contains(node.Member.Name)) {
                // find related expression
                var idx = Array.IndexOf(_argumentNames, node.Member.Name);
                var argument = _args.Expressions[idx];
                var unary = argument as UnaryExpression;
                // and replace it. So "a" is replaced with expression "x + 3"
                return unary?.Operand ?? argument;
            }
            return base.VisitMember(node);
        }
    }
}

Can be used like this: 可以像这样使用:

Func<int, Expression<Func<int>>> c = (int a) => () => a * (a + 3);
Expression<Func<int, int>> d = (x) => 2 + c.FakeInvoke<int>(3 + x);
d = d.Unwrap(); // this is now "x => (2 + ((3 + x) * ((3 + x) + 3)))"

Simple case: 简单案例:

Func<int, Expression<Func<int>>> c = (int a) => () => a + 3;
Expression<Func<int>> d = () => 2 + c.FakeInvoke<int>(3);
d = d.Unwrap(); // this is now "() => 2 + (3 + 3)

With multiple arguments: 有多个参数:

Func<int, int, Expression<Func<int>>> c = (int a, int b) => () => a * (a + 3) + b;
Expression<Func<int, int>> d = (x) => 2 + c.FakeInvoke<int>(3 + x, x + 5);
d = d.Unwrap(); // "x => (2 + (((3 + x) * ((3 + x) + 3)) + (x + 5)))"

Note that FakeInvoke is not type-safe (you should explicitly set return type and arguments and not checked). 请注意,FakeInvoke不是类型安全的(您应该显式设置返回类型和参数,而不是检查)。 But that's just for example, in real use you can create many overloads of FakeInvoke, like this: 但这仅仅是例如,在实际使用中你可以创建很多FakeInvoke的重载,如下所示:

public static TResult FakeInvoke<TArg, TResult>(this Func<TArg, Expression<Func<TResult>>> instance, TArg argument) {
        // this is not intended to be called directly
    throw new NotImplementedException();
}

Code above should be modified a bit to handle such calls correctly (because arguments are now not in single NewArrayExpression), but that's quite easy to do. 上面的代码应该被修改一下以正确处理这样的调用(因为参数现在不在单个NewArrayExpression中),但这很容易做到。 With such overloads you can just do: 有了这样的重载,你可以这样做:

Expression<Func<int, int>> d = (x) => 2 + c.FakeInvoke(3 + x); // this is type-safe now, you cannot pass non-integer as "3+x", nor you can pass more or less arguments than required.

The case where Expressions are returned from lambdas is really hard because those Expressions are actually closures that have non-public ( System.Runtime.CompilerServices.Closure ?) object inside them that contains the values that lambda closes over. 从lambdas返回表达式的情况非常困难,因为那些表达式实际上是包含lambda关闭的值的非公共( System.Runtime.CompilerServices.Closure ?)对象的闭包。 All of that makes really hard to accurately replace formal parameters with actual parameters in Expression tree. 所有这些都很难准确地将形式参数替换为表达式树中的实际参数。


Inspired by Evk response I found fairly elegant solution for simpler case: 受到Evk响应的启发,我找到了更简单案例的优雅解决方案:

Expression<Func<int, int>> c = (int a) => a * (a + 3);
var d = Extensions.Splice<Func<int, int>>((x) => 2 + c.Embed(3 + x));

// d is now x => (2 + ((3 + x) * ((3 + x) + 3))) expression

public static class Extensions
{
    public static T Embed<T>(this Expression<Func<T>> exp) { throw new Exception("Should not be executed"); }
    public static T Embed<A, T>(this Expression<Func<A, T>> exp, A a) { throw new Exception("Should not be executed"); }
    public static T Embed<A, B, T>(this Expression<Func<A, B, T>> exp, A a, B b) { throw new Exception("Should not be executed"); }
    public static T Embed<A, B, C, T>(this Expression<Func<A, B, C, T>> exp, A a, B b, C c) { throw new Exception("Should not be executed"); }
    public static T Embed<A, B, C, D, T>(this Expression<Func<A, B, C, D, T>> exp, A a, B b, C c) { throw new Exception("Should not be executed"); }

    public static Expression<T> Splice<T>(Expression<T> exp)
    {
        return new SplicingVisitor().Visit(exp) as Expression<T>;
    }
    class SplicingVisitor : ExpressionVisitor
    {
        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.Name == "Embed")
            {
                var mem = node.Arguments[0] as MemberExpression;

                var getterLambda = Expression.Lambda<Func<object>>(mem, new ParameterExpression[0]);
                var lam = getterLambda.Compile().DynamicInvoke() as LambdaExpression;

                var parameterMapping = lam.Parameters.Select((p, index) => new
                {
                    FormalParameter = p,
                    ActualParameter = node.Arguments[index+1]
                }).ToDictionary(o => o.FormalParameter, o => o.ActualParameter);

                return new ParameterReplacerVisitor(parameterMapping).Visit(lam.Body);
            }
            return base.VisitMethodCall(node);
        }
    }
    public class ParameterReplacerVisitor : ExpressionVisitor
    {
        private Dictionary<ParameterExpression, Expression> parameterMapping;
        public ParameterReplacerVisitor(Dictionary<ParameterExpression, Expression> parameterMapping)
        {
            this.parameterMapping = parameterMapping;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if(parameterMapping.ContainsKey(node))
            {
                return parameterMapping[node];
            }
            return base.VisitParameter(node);
        }
    }
}

Using LinqKit , it simple to consume it's expandable query wrapper by calling AsExpandable() on the first entity type. 使用LinqKit ,通过在第一个实体类型上调用AsExpandable()来简单地使用它的可扩展查询包装器。 This expandable wrapper does the work necessary to compose expressions make them compatible with EF. 这个可扩展的包装器完成了组合表达式所需的工作,使它们与EF兼容。

A toy example of it's usage is below ( Person is an EF Code First entity) - 它的用法的玩具示例如下( Person是EF Code First实体) -

var ctx = new Test();

Expression<Func<Person, bool>> ageFilter = p => p.Age < 30;

var filtered = ctx.People.AsExpandable()
    .Where(p => ageFilter.Invoke(p) && p.Name.StartsWith("J"));
Console.WriteLine( $"{filtered.Count()} people meet the criteria." );

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM