【问题标题】:How to 'unquote' when creating Expression tree from lambda?从 lambda 创建表达式树时如何“取消引用”?
【发布时间】:2016-11-25 17:47:10
【问题描述】:

假设我有一些函数c 返回Expression

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

现在我想创建另一个Expression,但在创建过程中我想调用函数c并将其结果作为新表达式的一部分嵌入:

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

我不能这样做,因为它会将c(3) 解释为要转换为表达式的函数调用,并且我会收到无法添加intExpression&lt;Func&lt;int&gt;&gt; 的错误

我希望 d 具有以下值:

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

我也有兴趣让它处理更复杂的表达式,而不仅仅是这个玩具示例。

你会如何在 C# 中做到这一点?

或者,您将如何在我可以在我的 C# 项目中使用的任何其他 CLR 语言中尽可能轻松地做到这一点?


更复杂的例子:

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

3+x 应该在结果表达式中只计算一次,即使它出现在 c 的主体中的两个位置。


我有一种强烈的感觉,它不能在 C# 中实现,因为将 lambda 分配给 Expression 是由编译器完成的,并且是编译时 const 表达式文字。这类似于让编译器理解纯字符串文字 "test" 理解模板字符串文字 "test ${a+b} other" 而 C# 编译器还没有处于这个开发阶段。

所以我的主要问题实际上是:

哪种 CLR 语言支持的语法可以让我方便地构建表达式树,嵌入由其他函数构造的部分?

其他可能性是一些库可以帮助我使用某种运行时编译的模板以这种方式构建表达式树,但我猜这样我会松散我的表达式代码的代码完成。


似乎 F# 能够“引用”和“取消引用”(拼接)代码:

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

【问题讨论】:

  • 您可以通过LinqKit 库使用表达式来构建其他表达式。您可以手动执行类似的操作,但需要更多的工作。我不认为你问的只是在其他表达式中使用简单的表达式。你想存储一个临时值。您可以通过从旧表达式构建新表达式来手动完成此操作。 ExpressionVisitor 在这里可能会有所帮助。
  • @KamilSzot,一个主要问题是c 本身是一个函数(真实代码),而不是表达式(代码作为数据)。您需要将其转换为 Expression&lt;Func&lt;int,int&gt;&gt; c = (int a) =&gt; a * (a + 3);
  • 其实这会帮助你嵌入它。但是,您的限制是另一回事。
  • 我(可能还有 Yacoub)的意思是,如果你有这个Func&lt;int, Expression&lt;Func&lt;int&gt;&gt;&gt; c = (int a) =&gt; () =&gt; a + 3; 而不是这个Expression&lt;Func&lt;int,int&gt;&gt; c = (int a) =&gt; (a + 3) - 那么你实际上可以获得你想要的结果((Expression&lt;Func&lt;int&gt;&gt;)( () =&gt; 2 + 3 + 3 ))。例如使用 LinqKit。
  • @Evk Wow LinqKit 很酷。感谢您把它的名字放在这里。

标签: c# linq lambda f# expression-trees


【解决方案1】:

对于您的两个示例,这实际上可以通过两个表达式访问者来完成(代码已注释):

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);
        }
    }
}

可以这样使用:

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)))"

简单案例:

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)

有多个参数:

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)))"

请注意,FakeInvoke 不是类型安全的(您应该显式设置返回类型和参数,而不是检查)。但这只是举例,在实际使用中你可以创建很多 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();
}

上面的代码应该稍作修改以正确处理此类调用(因为参数现在不在单个 NewArrayExpression 中),但这很容易做到。有了这样的重载,你可以这样做:

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.

【讨论】:

  • 谢谢。你的回答让我深入挖掘。
【解决方案2】:

从 lambdas 返回表达式的情况非常困难,因为这些表达式实际上是闭包,其中包含非公共 (System.Runtime.CompilerServices.Closure?) 对象,其中包含 lambda 关闭的值。所有这些都使得在表达式树中用实际参数准确地替换形式参数变得非常困难。


Evk response 的启发,我为更简单的情况找到了相当优雅的解决方案:

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);
        }
    }
}

【讨论】:

  • 那么您的更复杂的案例呢?
  • @Evk 我让它工作了,但是代码真的很乱,在有很多共享名称的参数和对象字段的更复杂的情况下,它可能会错误地替换参数。为了区分它们,我需要访问包含封闭值的 LambdaExpression 内部闭包对象。我什至看到这个对象的唯一方法是编译闭包 lambda 并查看其上的 Target.Constants 字段。我刚刚决定尝试仅使用静态(非闭包)表达式进行管理。
【解决方案3】:

使用LinqKit,可以通过在第一个实体类型上调用AsExpandable() 来简单地使用它的可扩展查询包装器。这个可扩展的包装器完成了编写表达式所需的工作,使它们与 EF 兼容。

下面是它的使用示例(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." );

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-06
    相关资源
    最近更新 更多