Объединить лямбда-выражения - PullRequest
8 голосов
/ 03 февраля 2012

Я ищу способ объединения двух лямбда-выражений без использования Expression.Invoke в любом выражении.Я хочу по существу построить новое выражение, которое объединяет два отдельных.Рассмотрим следующий код:

class Model {
    public SubModel SubModel { get; set;}
}

class SubModel {
    public Foo Foo { get; set; }
}

class Foo {
    public Bar Bar { get; set; }
}

class Bar {
    public string Value { get; set; }
}

И скажем, у меня было два выражения:

Expression<Func<Model, Foo>> expression1 = m => m.SubModel.Foo;
Expression<Func<Foo, string>> expression2 = f => f.Bar.Value;

И я хочу соединить их вместе, чтобы функционально получить следующее выражение:

Expression<Func<Model, string>> joinedExpression = m => m.SubModel.Foo.Bar.Value;

Единственный способ сделать это - использовать ExpressionVisitor следующим образом:

public class ExpressionExtender<TModel, TIntermediate> : ExpressionVisitor
{
    private readonly Expression<Func<TModel, TIntermediate>> _baseExpression;

    public ExpressionExtender(Expression<Func<TModel, TIntermediate>> baseExpression)
    {
        _baseExpression = baseExpression;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        _memberNodes.Push(node.Member.Name);
        return base.VisitMember(node);
    }

    private Stack<string> _memberNodes;

    public Expression<Func<TModel, T>> Extend<T>(Expression<Func<TIntermediate, T>>  extend)
    {
        _memberNodes = new Stack<string>();
        base.Visit(extend);
        var propertyExpression  = _memberNodes.Aggregate(_baseExpression.Body, Expression.Property);
        return Expression.Lambda<Func<TModel, T>>(propertyExpression, _baseExpression.Parameters);
    }
}

А затем использовать его так:

var expExt = new ExpressionExtender<Model, Foo>(expression1);
var joinedExpression = expExt.Extend(expression2);

Работает, ноэто кажется мне немного неуклюжим.Я все еще пытаюсь обернуть голову выражениями и задаюсь вопросом, есть ли более идиоматический способ выразить это, и у меня есть подлое подозрение, что я упустил что-то очевидное.1021 * Я хочу сделать это, чтобы использовать его с помощниками ASP.net MVC 3 Html.У меня есть несколько глубоко вложенных ViewModels и несколько расширений HtmlHelper, которые помогают справиться с ними, поэтому выражение должно быть просто набором MemberExpressions, чтобы встроенные помощники MVC могли правильно их обработать и построить правильно глубоко вложенные значения атрибута имени.Моим первым инстинктом было использовать Expression.Invoke(), вызывать первое выражение и связывать его со вторым, но помощникам MVC это не очень понравилось.Он потерял свой иерархический контекст.

Ответы [ 3 ]

21 голосов
/ 03 февраля 2012

Используйте посетителя, чтобы поменять местами все экземпляры параметра f на m.SubModel.Foo и создать новое выражение с m в качестве параметра:

internal static class Program
{
    static void Main()
    {

        Expression<Func<Model, Foo>> expression1 = m => m.SubModel.Foo;
        Expression<Func<Foo, string>> expression2 = f => f.Bar.Value;

        var swap = new SwapVisitor(expression2.Parameters[0], expression1.Body);
        var lambda = Expression.Lambda<Func<Model, string>>(
               swap.Visit(expression2.Body), expression1.Parameters);

        // test it worked
        var func = lambda.Compile();
        Model test = new Model {SubModel = new SubModel {Foo = new Foo {
             Bar = new Bar { Value = "abc"}}}};
        Console.WriteLine(func(test)); // "abc"
    }
}
class SwapVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public SwapVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
         return node == from ? to : base.Visit(node);
    }
}
6 голосов
/ 03 февраля 2012

Кажется, что ваше решение узко приспособлено к вашей конкретной проблеме, которая кажется негибкой.

Мне кажется, что вы можете решить свою проблему достаточно просто с помощью простой лямбда-замены: замените экземпляры параметра (илисвободная переменная ", как они это называют в лямбда-исчислении) с телом.(См. Ответ Марка о некотором коде для этого.)

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

Expression<Func<A, B>> ab = a => f(a);  // could be *any* expression using a
Expression<Func<B, C>> bc = b => g(b);  // could be *any* expression using b

и вы хотите создать композицию

Expression<Func<A, C>> ac = a => g(f(a)); // replace all b with f(a).

Итак, возьмите тело g(b), выполните поиск и заменупосетитель ищет ParameterExpression для b и заменяет его на тело f(a), чтобы дать вам новое тело g(f(a)).Затем создайте новую лямбду с параметром a, имеющим это тело.

0 голосов
/ 05 ноября 2014

Обновление: приведенный ниже ответ генерирует "Invoke", который EF не поддерживает.

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

class Program
{
    private static Expression<Func<T, string>> GetValueFromFoo<T>(Func<T, Foo> getFoo)
    {
        return t => getFoo(t).Bar.Value;
    }

    static void Main()
    {
        Expression<Func<Model, string>> getValueFromBar = GetValueFromFoo<Model>(m => m.SubModel.Foo);

        // test it worked
        var func = getValueFromBar.Compile();
        Model test = new Model
        {
            SubModel = new SubModel
            {
                Foo = new Foo
                {
                    Bar = new Bar { Value = "abc" }
                }
            }
        };
        Console.WriteLine(func(test)); // "abc"
    }
}
...