Объединение двух лямбда-выражений в C#

Учитывая структуру класса, как это:

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 будучи вложенным вызовом функции.

Спасибо

8 ответов

Решение

ХОРОШО; довольно длинный фрагмент, но вот стартер для переписчика выражений; он еще не обрабатывает несколько случаев (я исправлю это позже), но работает для данного примера и многих других:

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

Я предполагаю, что ваша цель состоит в том, чтобы получить дерево выражений, которое вы бы получили, если бы вы на самом деле скомпилировали "комбинированную" лямбду. Гораздо проще создать новое дерево выражений, которое просто соответствующим образом вызывает заданные деревья выражений, но я предполагаю, что это не то, что вам нужно.

  • сначала извлеките тело, приведите его к MemberExpression. Назовите это первым телом.
  • извлеките тело секунды, назовите этот secondBody
  • извлечь параметр первого. Назовите это первым Парамом.
  • извлечь параметр второй. Назовите этот второй Парам.
  • Теперь сложная часть. Напишите реализацию шаблона посетителя, которая ищет в secondBody поиски единственного использования secondParam. (Это будет намного проще, если вы знаете, что это только выражения доступа к элементу, но вы можете решить проблему в целом.) Когда вы найдете его, создайте новое выражение того же типа, что и его родительский элемент, заменив в параметре firstBody. Продолжайте восстанавливать трансформированное дерево на обратном пути; помните, все, что вам нужно перестроить, это "позвоночник" дерева, который содержит ссылку на параметр.
  • результат прохода посетителя будет переписан secondBody без вхождений secondParam, только вхождения выражений с участием firstParam.
  • создайте новое лямбда-выражение с этим телом в качестве его тела и firstParam в качестве его param.
  • и вы сделали!

Блог Мэтта Уоррена может быть полезен для вас. Он спроектировал и реализовал все эти вещи и много писал о способах эффективного переписывания деревьев выражений. (Я только сделал конец компилятора вещей.)

ОБНОВИТЬ:

Как указывает этот связанный ответ, в.NET 4 теперь есть базовый класс для переписывателей выражений, который значительно упрощает подобные вещи.

Я не уверен, что вы подразумеваете под не вложенным вызовом функции, но это поможет - на примере:

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

Я не знаю, насколько эффективно сгенерированный код будет сравниваться с одним лямбда-выражением, но я подозреваю, что он не будет слишком плохим.

Для полного решения взгляните на LINQKit:

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

output.ToString() распечатывает

gp => gp.Parent.Child.Name

в то время как решение Джона Скита дает

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

Я думаю, это то, что вы называете "вызовами вложенных функций".

Попробуй это:

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"

После полдня копания пришло следующее решение (гораздо более простое, чем принятый ответ):

Для общей лямбда-композиции:

    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), объедините (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.Prop,

fObj может быть любым выражением (но должно быть MVC-совместимым)

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

С инструментарием, называемым 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);
Другие вопросы по тегам