在 C# 中组合两个 lambda 表达式
Posted
技术标签:
【中文标题】在 C# 中组合两个 lambda 表达式【英文标题】:Combining two lambda expressions in c# 【发布时间】: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
?
你说得对,我可以做到,只是感觉很老套。我打算同时使用这两种方法,看看哪一种感觉最好。答案可能是组合表达式真的很简单,在这种情况下会更可取。
【参考方案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<Func<TOuter, TInner>>(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 => 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);
【讨论】:
您可以提供您的工具包作为解决方案,但常见问题解答确实声明您必须披露您是作者。 (在答案中,而不仅仅是在您的个人资料中。)以上是关于在 C# 中组合两个 lambda 表达式的主要内容,如果未能解决你的问题,请参考以下文章
如何在没有调用的情况下合并两个 C# Lambda 表达式?