在C#中合并两个Lambda表达式

28

假设有以下类结构:

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

这是可能的吗?

每个函数的内容只会是一个 MemberAccess。我不想让 output 成为一个嵌套的函数调用。

谢谢


1
(回复Eric的答案评论)如果您不打算调用,为什么不教您现有的解析代码如何读取“Invoke”? - Marc Gravell
1
你说得对,我可以这样做,但感觉有点不专业。我会尝试两种方法,看哪一种更好。如果可以很简单地合并这些表达式,那就更好了。 - Andrew Bullock
8个回答

26

好的,代码比较长,但是这里提供一个表达式重写器的起始版本;它还没有处理一些情况(稍后我会修复它),但是对于给定的示例和许多其他示例都可以正常工作:

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

2
难道没有一个名为ExpressionVisitor的类(或类似的东西),可以轻松地作为这个重写的基类吗?我很确定我曾经使用过类似的东西。 - configurator
2
@configurator 是的,在4.0版本中有;不确定在2009年11月是否有。我在最近的使用中使用了ExpressionVisitor。 - Marc Gravell
1
抱歉,没有注意到这是一个旧问题 :) - configurator

20

我假设您的目标是获得您实际编译"combined" lambda时所获得的表达式树。构造一个新的表达式树只需适当地调用给定的表达式树就容易得多,但我认为这不是您想要的。

  • 提取第一个lambda表达式的主体,并将其转换为MemberExpression。将其称为firstBody。
  • 提取第二个lambda表达式的主体,将其称为secondBody。
  • 提取第一个lambda表达式的参数,将其称为firstParam。
  • 提取第二个lambda表达式的参数,将其称为secondParam。
  • 现在,难点来了。编写访问者模式实现,搜索secondBody以查找对secondParam的单次使用。(如果您知道它只是成员访问表达式,那么这将容易得多,但您可以在一般情况下解决问题。)找到它后,构造与其父项相同类型的新表达式,将firstBody替换为该参数。继续重建变换树;记住,您只需要重建包含参数引用的树的“脊柱”。
  • 访问者通过的结果将是重新编写的secondBody,其中不包含secondParam的任何出现,只包含涉及firstParam的表达式。
  • 使用该主体构造一个新的lambda表达式,firstParam作为其参数。
  • 完成!

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

更新:

正如这个相关答案所指出的,在.NET 4中,现在有一个基类用于表达式重写器,使得这种事情变得更加容易。


2
我一直认为,在现有表达式中替换表达式的能力(例如,将给定的“ParameterExpression”的所有实例替换为某些其他已知表达式)是一个被忽视的技巧。Expression.Invoke是一种选择,但在 EF 中支持不佳(LINQ-to-SQL 可以工作)。 - Marc Gravell
1
(显然通过某种访问者创建新表达式;不更改现有表达式) - Marc Gravell
2
+1,非常有趣的解决方案,很希望能看到它在实际应用中的效果 :-) - Darin Dimitrov
1
我曾经实现过这样一个访问者,可以处理大多数3.5表达式类型。不久前,我应该会重新审视它(只需要花费一个小时左右),并将其更新为4.0版本。@Darin;如果您想让我在我的硬盘上找到它,请告诉我(请参阅我的个人资料)。 - Marc Gravell
1
这听起来正是我所需要的。原则上我理解了所有内容,但我的知识在第5步如何构建新的lambda时就断掉了。我会去谷歌一下Matt Warren的博客。@Marc 我很想看看它 :) - Andrew Bullock
显示剩余3条评论

14

我不确定你所说的不是嵌套函数调用是什么意思,但以下代码可以解决问题,并附有示例:

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表达式相比效率如何,但我怀疑它不会太差。


1
你可以通过重复使用外部表达式中的参数和主体(分别)来简化此操作(删除调用和参数表达式)。 - Marc Gravell
1
像这样:return Expression.Lambda<Func<TOuter,TInner>>(Expression.Invoke(second,first.Body),first.Parameters); - Marc Gravell
1
请注意,EF在3.5SP1中不支持此操作;-p。但是,LINQ-to-SQL可以正常使用。因此,这取决于提供程序。 - Marc Gravell

5

如果您想要完整的解决方案,请查看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))

我猜你指的是“嵌套函数调用”。

2

试试这个:

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"

1
我的猜测是,生成的表达式树可能不适合在(比如)LINQ to SQL 中使用。至于我的表达式树是否适用,我不确定 - 但它将事物保持为表达式树而不将其编译为中间方法,我认为这是一个很好的开始 :) - Jon Skeet
1
@Jon,我同意你的看法,但编译表达式是我首先想到的事情 :-) - Darin Dimitrov

1

经过半天的挖掘,我找到了以下解决方案(比被接受的答案简单得多):

对于通用的 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 => x.PropfObj 可以是任何表达式(但必须是 MVC 兼容的)。

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

0
使用一个名为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);

2
你提供你的工具包作为解决方案是可以的,但是常见问题解答中确实指出你必须披露自己是作者。(不仅在你的个人资料中,在回答中也要说明。) - Danny Varod

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接