【问题标题】:Combining two lambda expressions in c#在 C# 中组合两个 lambda 表达式
【发布时间】: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
  • 你说得对,我可以做到,只是感觉很老套。我打算同时使用这两种方法,看看哪一种感觉最好。答案可能是组合表达式真的很简单,在这种情况下会更可取。

标签: c# lambda


【解决方案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);
        

        【讨论】:

        • 您可以提供您的工具包作为解决方案,但常见问题解答确实声明您必须披露您是作者。 (在答案中,而不仅仅是在您的个人资料中。)
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多