在 C# 中组合两个 lambda 表达式

Posted

技术标签:

【中文标题】在 C# 中组合两个 lambda 表达式【英文标题】:Combining two lambda expressions in c# 【发布时间】:2010-12-15 14:29:32 【问题描述】:

给定这样的类结构:

public class GrandParent

    public Parent Parent  get; set;

public class Parent

    public Child Child  get; set;


public class Child

    public string Name  get; set;

以及以下方法签名:

Expression<Func<TOuter, TInner>> Combine (Expression<Func<TOuter, TMiddle>>> first, Expression<Func<TMiddle, TInner>> second);

我怎样才能实现所说的方法,以便我可以这样调用它:

Expression<Func<GrandParent, Parent>>> myFirst = gp => gp.Parent;
Expression<Func<Parent, string>> mySecond = p => p.Child.Name;

Expression<Func<GrandParent, string>> output = Combine(myFirst, mySecond);

这样输出最终为:

gp => gp.Parent.Child.Name

这可能吗?

每个 Func 的内容永远只能是 MemberAccess。我宁愿不以 output 结束嵌套函数调用。

谢谢

【问题讨论】:

(回复评论 Eric 的回答)如果你不打算调用,为什么不教你现有的解析代码如何阅读Invoke 你说得对,我可以做到,只是感觉很老套。我打算同时使用这两种方法,看看哪一种感觉最好。答案可能是组合表达式真的很简单,在这种情况下会更可取。 【参考方案1】:

好的;相当长的 sn-p,但这是表达式重写器的 starter;它还不能处理一些情况(我稍后会修复它),但它适用于给定的示例和 lot 其他示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;

public class GrandParent

    public Parent Parent  get; set; 

public class Parent

    public Child Child  get; set; 
    public string Method(string s)  return s + "abc"; 


public class Child

    public string Name  get; set; 

public static class ExpressionUtils

    public static Expression<Func<T1, T3>> Combine<T1, T2, T3>(
        this Expression<Func<T1, T2>> outer, Expression<Func<T2, T3>> inner, bool inline)
    
        var invoke = Expression.Invoke(inner, outer.Body);
        Expression body = inline ? new ExpressionRewriter().AutoInline(invoke) : invoke;
        return Expression.Lambda<Func<T1, T3>>(body, outer.Parameters);
    

public class ExpressionRewriter

    internal Expression AutoInline(InvocationExpression expression)
    
        isLocked = true;
        if(expression == null) throw new ArgumentNullException("expression");
        LambdaExpression lambda = (LambdaExpression)expression.Expression;
        ExpressionRewriter childScope = new ExpressionRewriter(this);
        var lambdaParams = lambda.Parameters;
        var invokeArgs = expression.Arguments;
        if (lambdaParams.Count != invokeArgs.Count) throw new InvalidOperationException("Lambda/invoke mismatch");
        for(int i = 0 ; i < lambdaParams.Count; i++) 
            childScope.Subst(lambdaParams[i], invokeArgs[i]);
        
        return childScope.Apply(lambda.Body);
    
    public ExpressionRewriter()
    
         subst = new Dictionary<Expression, Expression>();
    
    private ExpressionRewriter(ExpressionRewriter parent)
    
        if (parent == null) throw new ArgumentNullException("parent");
        subst = new Dictionary<Expression, Expression>(parent.subst);
        inline = parent.inline;
    
    private bool isLocked, inline;
    private readonly Dictionary<Expression, Expression> subst;
    private void CheckLocked() 
        if(isLocked) throw new InvalidOperationException(
            "You cannot alter the rewriter after Apply has been called");

    
    public ExpressionRewriter Subst(Expression from,
        Expression to)
    
        CheckLocked();
        subst.Add(from, to);
        return this;
    
    public ExpressionRewriter Inline() 
        CheckLocked();
        inline = true;
        return this;
    
    public Expression Apply(Expression expression)
    
        isLocked = true;
        return Walk(expression) ?? expression;
    

    private static IEnumerable<Expression> CoalesceTerms(
        IEnumerable<Expression> sourceWithNulls, IEnumerable<Expression> replacements)
    
        if(sourceWithNulls != null && replacements != null) 
            using(var left = sourceWithNulls.GetEnumerator())
            using (var right = replacements.GetEnumerator())
            
                while (left.MoveNext() && right.MoveNext())
                
                    yield return left.Current ?? right.Current;
                
            
        
    
    private Expression[] Walk(IEnumerable<Expression> expressions) 
        if(expressions == null) return null;
        return expressions.Select(expr => Walk(expr)).ToArray();
    
    private static bool HasValue(Expression[] expressions)
    
        return expressions != null && expressions.Any(expr => expr != null);
    
    // returns null if no need to rewrite that branch, otherwise
    // returns a re-written branch
    private Expression Walk(Expression expression)
    
        if (expression == null) return null;
        Expression tmp;
        if (subst.TryGetValue(expression, out tmp)) return tmp;
        switch(expression.NodeType) 
            case ExpressionType.Constant:
            case ExpressionType.Parameter:
                
                    return expression; // never a need to rewrite if not already matched
                
            case ExpressionType.MemberAccess:
                
                    MemberExpression me = (MemberExpression)expression;
                    Expression target = Walk(me.Expression);
                    return target == null ? null : Expression.MakeMemberAccess(target, me.Member);
                
            case ExpressionType.Add:
            case ExpressionType.Divide:
            case ExpressionType.Multiply:
            case ExpressionType.Subtract:
            case ExpressionType.AddChecked:
            case ExpressionType.MultiplyChecked:
            case ExpressionType.SubtractChecked:
            case ExpressionType.And:
            case ExpressionType.Or:
            case ExpressionType.ExclusiveOr:
            case ExpressionType.Equal:
            case ExpressionType.NotEqual:
            case ExpressionType.AndAlso:
            case ExpressionType.OrElse:
            case ExpressionType.Power:
            case ExpressionType.Modulo:
            case ExpressionType.GreaterThan:
            case ExpressionType.GreaterThanOrEqual:
            case ExpressionType.LessThan:
            case ExpressionType.LessThanOrEqual:
            case ExpressionType.LeftShift:
            case ExpressionType.RightShift:
            case ExpressionType.Coalesce:
            case ExpressionType.ArrayIndex:
                
                    BinaryExpression binExp = (BinaryExpression)expression;
                    Expression left = Walk(binExp.Left), right = Walk(binExp.Right);
                    return (left == null && right == null) ? null : Expression.MakeBinary(
                        binExp.NodeType, left ?? binExp.Left, right ?? binExp.Right, binExp.IsLiftedToNull,
                        binExp.Method, binExp.Conversion);
                
            case ExpressionType.Not:
            case ExpressionType.UnaryPlus:
            case ExpressionType.Negate:
            case ExpressionType.NegateChecked:
            case ExpressionType.Convert: 
            case ExpressionType.ConvertChecked:
            case ExpressionType.TypeAs:
            case ExpressionType.ArrayLength:
                
                    UnaryExpression unExp = (UnaryExpression)expression;
                    Expression operand = Walk(unExp.Operand);
                    return operand == null ? null : Expression.MakeUnary(unExp.NodeType, operand,
                        unExp.Type, unExp.Method);
                
            case ExpressionType.Conditional:
                
                    ConditionalExpression ce = (ConditionalExpression)expression;
                    Expression test = Walk(ce.Test), ifTrue = Walk(ce.IfTrue), ifFalse = Walk(ce.IfFalse);
                    if (test == null && ifTrue == null && ifFalse == null) return null;
                    return Expression.Condition(test ?? ce.Test, ifTrue ?? ce.IfTrue, ifFalse ?? ce.IfFalse);
                
            case ExpressionType.Call:
                
                    MethodCallExpression mce = (MethodCallExpression)expression;
                    Expression instance = Walk(mce.Object);
                    Expression[] args = Walk(mce.Arguments);
                    if (instance == null && !HasValue(args)) return null;
                    return Expression.Call(instance, mce.Method, CoalesceTerms(args, mce.Arguments));
                
            case ExpressionType.TypeIs:
                
                    TypeBinaryExpression tbe = (TypeBinaryExpression)expression;
                    tmp = Walk(tbe.Expression);
                    return tmp == null ? null : Expression.TypeIs(tmp, tbe.TypeOperand);
                
            case ExpressionType.New:
                
                    NewExpression ne = (NewExpression)expression;
                    Expression[] args = Walk(ne.Arguments);
                    if (HasValue(args)) return null;
                    return ne.Members == null ? Expression.New(ne.Constructor, CoalesceTerms(args, ne.Arguments))
                        : Expression.New(ne.Constructor, CoalesceTerms(args, ne.Arguments), ne.Members);
                
            case ExpressionType.ListInit:
                
                    ListInitExpression lie = (ListInitExpression)expression;
                    NewExpression ctor = (NewExpression)Walk(lie.NewExpression);
                    var inits = lie.Initializers.Select(init => new
                    
                        Original = init,
                        NewArgs = Walk(init.Arguments)
                    ).ToArray();
                    if (ctor == null && !inits.Any(init => HasValue(init.NewArgs))) return null;
                    ElementInit[] initArr = inits.Select(init => Expression.ElementInit(
                            init.Original.AddMethod, CoalesceTerms(init.NewArgs, init.Original.Arguments))).ToArray();
                    return Expression.ListInit(ctor ?? lie.NewExpression, initArr);

                
            case ExpressionType.NewArrayBounds:
            case ExpressionType.NewArrayInit:
                /* not quite right... leave as not-implemented for now
                
                    NewArrayExpression nae = (NewArrayExpression)expression;
                    Expression[] expr = Walk(nae.Expressions);
                    if (!HasValue(expr)) return null;
                    return expression.NodeType == ExpressionType.NewArrayBounds
                        ? Expression.NewArrayBounds(nae.Type, CoalesceTerms(expr, nae.Expressions))
                        : Expression.NewArrayInit(nae.Type, CoalesceTerms(expr, nae.Expressions));
                */
            case ExpressionType.Invoke:
            case ExpressionType.Lambda:
            case ExpressionType.MemberInit:
            case ExpressionType.Quote:
                throw new NotImplementedException("Not implemented: " + expression.NodeType);
            default:
                throw new NotSupportedException("Not supported: " + expression.NodeType);
        

    

static class Program

    static void Main()
    
        Expression<Func<GrandParent, Parent>> myFirst = gp => gp.Parent;
        Expression<Func<Parent, string>> mySecond = p => p.Child.Name;

        Expression<Func<GrandParent, string>> outputWithInline = myFirst.Combine(mySecond, false);
        Expression<Func<GrandParent, string>> outputWithoutInline = myFirst.Combine(mySecond, true);

        Expression<Func<GrandParent, string>> call =
                ExpressionUtils.Combine<GrandParent, Parent, string>(
                gp => gp.Parent, p => p.Method(p.Child.Name), true);

        unchecked
        
            Expression<Func<double, double>> mathUnchecked =
                ExpressionUtils.Combine<double, double, double>(x => (x * x) + x, x => x - (x / x), true);
        
        checked
        
            Expression<Func<double, double>> mathChecked =
                ExpressionUtils.Combine<double, double, double>(x => x - (x * x) , x => (x / x) + x, true);
        
        Expression<Func<int,int>> bitwise =
            ExpressionUtils.Combine<int, int, int>(x => (x & 0x01) | 0x03, x => x ^ 0xFF, true);
        Expression<Func<int, bool>> logical =
            ExpressionUtils.Combine<int, bool, bool>(x => x == 123, x => x != false, true);
        Expression<Func<int[][], int>> arrayAccess =
            ExpressionUtils.Combine<int[][], int[], int>(x => x[0], x => x[0], true);
        Expression<Func<string, bool>> isTest =
            ExpressionUtils.Combine<string,object,bool>(s=>s, s=> s is Regex, true);

        Expression<Func<List<int>>> f = () => new List<int>(new int[]  1, 1, 1 .Length);
        Expression<Func<string, Regex>> asTest =
            ExpressionUtils.Combine<string, object, Regex>(s => s, s => s as Regex, true);
        var initTest = ExpressionUtils.Combine<int, int[], List<int>>(i => new[] i,i,i, 
                    arr => new List<int>(arr.Length), true);
        var anonAndListTest = ExpressionUtils.Combine<int, int, List<int>>(
                i => new  age = i .age, i => new List<int> i, i, true);
        /*
        var arrBoundsInit = ExpressionUtils.Combine<int, int[], int[]>(
            i => new int[i], arr => new int[arr[0]] , true);
        var arrInit = ExpressionUtils.Combine<int, int, int[]>(
            i => i, i => new int[1]  i , true);*/
    

【讨论】:

难道没有一个ExpressionVisitor 类(或类似的东西)可以很容易地充当这种重写的基类吗?我很确定我曾经使用过类似的东西。 @configurator 是的,现在有(在 4.0 中);不确定 09 年 11 月有没有。我最近使用过 ExpressionVisitor。 抱歉,没注意到这是个老问题 :)【参考方案2】:

我假设您的目标是获得 您将获得的表达式树,如果您实际编译了“组合”lambda。构造一个简单地适当调用给定表达式树的新表达式树要容易得多,但我认为这不是您想要的。

提取 first 的主体,将其转换为 MemberExpression。将此称为 firstBody。 提取第二个主体,称之为第二个主体 提取first的参数。调用这个 firstParam。 提取秒的参数。调用这个 secondParam。 现在,最困难的部分。编写一个访问者模式实现,它通过 secondBody 搜索寻找 secondParam 的单一用法。 (如果您知道它只是成员访问表达式,这将容易得多,但您可以解决一般问题。)找到它后,构造一个与其父级相同类型的新表达式,用 firstBody 替换参数。在回来的路上继续重建变形的树;请记住,您需要重建的只是包含参数引用的树的“脊椎”。 访问者通行证的结果将是一个重写的 secondBody,没有出现 secondParam,只有出现涉及 firstParam 的表达式。 构造一个新的 lambda 表达式,以该主体为主体,firstParam 作为其参数。 大功告成!

Matt Warren 的博客可能是您阅读的好东西。他设计并实现了所有这些东西,并写了很多关于有效重写表达式树的方法。 (我只做了编译器结束的事情。)

更新:

作为 this related answer points out,在 .NET 4 中现在有一个用于表达式重写器的基类,这使得这类事情变得更加容易。

【讨论】:

我一直认为在现有表达式中替换表达式的能力(也许用其他已知表达式替换给定ParameterExpression 的所有实例)是一个被遗漏的技巧。 Expression.Invoke 是一个选项,但 EF 对它的支持很差(不过 LINQ-to-SQL 可以工作)。 (显然是通过某种访问者创建新表达式;npt 更改现有的) +1,非常有趣的解决方案,很高兴看到这一点:-) 对于信息,我前段时间有一个这样的访问者的实现,它适用于大多数 3.5 表达式类型。我应该在某个时候重新审视它(只花了一个小时左右),将其更新为 4.0.0。 @达林;如果您想让我尝试在我的硬盘上找到它,请告诉我(查看个人资料)。 这听起来正是我所需要的。我原则上理解所有这些,但我的知识分解是如何准确地执行第 5 步,如何构建新的 lambda。生病谷歌马特沃伦的博客。 @Marc 我有兴趣看看 :)【参考方案3】:

我不确定你所说的不是嵌套函数调用是什么意思,但这可以解决问题 - 举个例子:

using System;
using System.IO;
using System.Linq.Expressions;

class Test    
    
    static Expression<Func<TOuter, TInner>> Combine<TOuter, TMiddle, TInner>
        (Expression<Func<TOuter, TMiddle>> first, 
         Expression<Func<TMiddle, TInner>> second)
    
        var parameter = Expression.Parameter(typeof(TOuter), "x");
        var firstInvoke = Expression.Invoke(first, new[]  parameter );
        var secondInvoke = Expression.Invoke(second, new[]  firstInvoke );

        return Expression.Lambda<Func<TOuter, TInner>>(secondInvoke, parameter);
    

    static void Main()
    
        Expression<Func<int, string>> first = x => (x + 1).ToString();
        Expression<Func<string, StringReader>> second = y => new StringReader(y);

        Expression<Func<int, StringReader>> output = Combine(first, second);
        Func<int, StringReader> compiled = output.Compile();
        var reader = compiled(10);
        Console.WriteLine(reader.ReadToEnd());
    

我不知道生成的代码与单个 lambda 表达式相比效率如何,但我怀疑它不会太糟糕。

【讨论】:

您可以通过(单独)重用外部表达式中的参数和主体来简化此操作(删除调用参数表达式)。 像这样:return Expression.Lambda&lt;Func&lt;TOuter, TInner&gt;&gt;(Expression.Invoke(second, first.Body), first.Parameters); 还要注意,3.5SP1 中的 EF 讨厌这个 ;-p LINQ-to-SQL 可以,不过。所以它是特定于提供商的。【参考方案4】:

如需完整解决方案,请查看LINQKit:

Expression<Func<GrandParent, string>> output = gp => mySecond.Invoke(myFirst.Invoke(gp));
output = output.Expand().Expand();

output.ToString() 打印出来

gp => gp.Parent.Child.Name

而 Jon Skeet 的解决方案产生了

x => Invoke(p => p.Child.Name,Invoke(gp => gp.Parent,x))

我猜这就是您所说的“嵌套函数调用”。

【讨论】:

【参考方案5】:

试试这个:

public static Expression<Func<TOuter, TInner>> Combine<TOuter, TMiddle, TInner>(
    Expression<Func<TOuter, TMiddle>> first, 
    Expression<Func<TMiddle, TInner>> second)

    return x => second.Compile()(first.Compile()(x));

及用法:

Expression<Func<GrandParent, Parent>> myFirst = gp => gp.Parent;
Expression<Func<Parent, string>> mySecond = p => p.Child.Name;
Expression<Func<GrandParent, string>> output = Combine(myFirst, mySecond);
var grandParent = new GrandParent 
 
    Parent = new Parent 
     
        Child = new Child 
         
            Name = "child name" 
         
     
;
var childName = output.Compile()(grandParent);
Console.WriteLine(childName); // prints "child name"

【讨论】:

我的猜测是生成的表达式树不适合在(比如说)LINQ to SQL 中使用。无论我是否愿意,我不知道 - 但它会将事物保留为表达式树,而不会将它们编译成中间方法,我怀疑这是一个好的开始:) @Jon,我同意你的看法,但我首先想到的是编译表达式 :-)【参考方案6】:
    public static Expression<Func<T, TResult>> And<T, TResult>(this Expression<Func<T, TResult>> expr1, Expression<Func<T, TResult>> expr2)
    
        var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
        return Expression.Lambda<Func<T, TResult>>(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters);
    

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
    
        var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
        return Expression.Lambda<Func<T, bool>>(Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters);
    

【讨论】:

【参考方案7】:

经过半天的挖掘想出了以下解决方案(比公认的答案简单得多):

对于通用 lambda 组合:

    public static Expression<Func<X, Z>> Compose<X, Y, Z>(Expression<Func<Y, Z>> f, Expression<Func<X, Y>> g)
    
        return Expression.Lambda<Func<X, Z>>(Expression.Invoke(f, Expression.Invoke(g, g.Parameters[0])), g.Parameters);
    

这将两个表达式合二为一,即将第一个表达式应用于第二个表达式的结果。

所以如果我们有 f(y) 和 g(x),则 combine(f,g)(x) === f(g(x))

传递性和关联性,因此组合子可以链接

更具体地说,对于属性访问(MVC/EF 需要):

    public static Expression<Func<X, Z>> Property<X, Y, Z>(Expression<Func<X, Y>> fObj, Expression<Func<Y, Z>> fProp)
    
        return Expression.Lambda<Func<X, Z>>(Expression.Property(fObj.Body, (fProp.Body as MemberExpression).Member as PropertyInfo), fObj.Parameters);
    

注意:fProp 必须是简单的属性访问表达式,例如x =&gt; x.Prop

fObj 可以是任何表达式(但必须与 MVC 兼容)

【讨论】:

【参考方案8】:

使用名为Layer Over LINQ 的工具包,有一个扩展方法可以做到这一点,它结合两个表达式来创建一个适用于 LINQ to Entities 的新表达式。

Expression<Func<GrandParent, Parent>>> myFirst = gp => gp.Parent;
Expression<Func<Parent, string>> mySecond = p => p.Child.Name;

Expression<Func<GrandParent, string>> output = myFirst.Chain(mySecond);

【讨论】:

您可以提供您的工具包作为解决方案,但常见问题解答确实声明您必须披露您是作者。 (在答案中,而不仅仅是在您的个人资料中。)

以上是关于在 C# 中组合两个 lambda 表达式的主要内容,如果未能解决你的问题,请参考以下文章

如何在没有调用的情况下合并两个 C# Lambda 表达式?

Lambda 表达式以及如何组合它们?

哪位高人指点一下,C#中两个动态lambda 表达式有啥办法合并成一个? 谢谢!

递归 lambda 表达式在 C# 中遍历树

C# Lambda 运算符 [关闭]

lambda 表达式中的 C# 切换