合并Lambda表达式

10
我正在寻求一种方法来合并两个lambda表达式,而不使用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);

这个方法能用,但对我来说有些不太流畅。我还在努力理解表达式,并且想知道是否有更习惯用语的表达方式,我怀疑自己可能会错过一些显而易见的东西。


我之所以想要这样做,是为了与ASP.net mvc 3 Html helpers一起使用。我有一些深层嵌套的ViewModels和一些HtmlHelper扩展,帮助处理它们,因此表达式仅需要成为内置MVC helpers的MemberExpressions集合,才能正确处理它们并构建正确的深度嵌套名称属性值。我的第一反应是使用Expression.Invoke()调用第一个表达式,然后将其链接到第二个表达式,但MVC helpers并不太喜欢它。它失去了它的分层上下文。

3个回答

21

使用访问者将所有参数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);
    }
}

+1 现在我看到了,这很有道理。我在原始问题中没有提到的一件事是:是否有一种方法可以在不改变任何起始表达式的情况下完成这个操作。例如,我有一个核心表达式,需要以几种不同的方式进行扩展,每次调用都会生成新的表达式。 - J. Holmes
@32bitkid 是的!表达式是不可变的;我没有改变它们中的任何一个! - Marc Gravell

6

您的解决方案似乎只适用于特定问题,看起来不太灵活。

在我看来,您可以通过简单的lambda替换直接解决问题:将参数实例(或者在lambda演算中所称的“自由变量”)替换为函数体即可。(请参见Marc的回答以获取一些可执行代码。)

由于表达式树中的参数具有引用标识而不是值标识,因此甚至不需要对它们进行α重命名。

也就是说,您有:

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) 进行搜索和替换,查找参数表达式 b 并将其替换为体 f(a),从而得到新的体 g(f(a))。然后使用该体创建具有参数 a 的新 Lambda 表达式。


@Kobi:我不明白这个问题。什么还是可能的?而且ab不是lambda表达式,它们是形式参数。 - Eric Lippert
这个解决方案适用于两个表达式(Expression)还是仅适用于两个 Func<>?(好的,没关系 - 我现在想我理解了你的答案) - Kobi
@Kobi:我不明白你所说的“适合”的意思。假设您有常数1的表达式和常数2的表达式。您希望对这两个表达式执行什么操作,类似于lambda函数上的函数组合? - Eric Lippert
问题的背景是关于需要内联MVC的问题;这种方法能否实现? - Marc Gravell

0
更新:下面的答案生成了一个“Invoke”,而EF不支持。
我知道这是一个旧帖子,但我有同样的需求,并且我找到了一种更简洁的方法来实现它。假设您可以修改您的“expression2”以使用通用lambda,您可以像这样注入一个:
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"
    }
}

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