Maneira mais eficiente de testar a igualdade de expressões lambda

Dada uma assinatura de método:

public bool AreTheSame(Expression<Func> exp1, Expression<Func> exp2) 

Qual seria a maneira mais eficiente de dizer se as duas expressões são as mesmas? Isso só precisa funcionar para expressões simples, com isso quero dizer que tudo o que seria “suportado” seria Simple MemberExpressions, por exemplo, c => c.ID.

Um exemplo de chamada pode ser:

 AreTheSame(u1 => u1.ID, u2 => u2.ID); --> would return true 

   

Hmmm … Eu acho que você teria que analisar a tree, verificando o tipo de nó e o membro de cada um. Eu vou bater um exemplo …

 using System; using System.Linq.Expressions; class Test { public string Foo { get; set; } public string Bar { get; set; } static void Main() { bool test1 = FuncTest.FuncEqual(x => x.Bar, y => y.Bar), test2 = FuncTest.FuncEqual(x => x.Foo, y => y.Bar); } } // this only exists to make it easier to call, ie so that I can use FuncTest with // generic-type-inference; if you use the doubly-generic method, you need to specify // both arguments, which is a pain... static class FuncTest { public static bool FuncEqual( Expression> x, Expression> y) { return FuncTest.FuncEqual(x, y); } } static class FuncTest { public static bool FuncEqual( Expression> x, Expression> y) { return ExpressionEqual(x, y); } private static bool ExpressionEqual(Expression x, Expression y) { // deal with the simple cases first... if (ReferenceEquals(x, y)) return true; if (x == null || y == null) return false; if ( x.NodeType != y.NodeType || x.Type != y.Type ) return false; switch (x.NodeType) { case ExpressionType.Lambda: return ExpressionEqual(((LambdaExpression)x).Body, ((LambdaExpression)y).Body); case ExpressionType.MemberAccess: MemberExpression mex = (MemberExpression)x, mey = (MemberExpression)y; return mex.Member == mey.Member; // should really test down-stream expression default: throw new NotImplementedException(x.NodeType.ToString()); } } } 

ATUALIZAÇÃO: Devido ao interesse da minha solução, atualizei o código para que ele suporte matrizes, novos operadores e outras coisas e compare os ASTs de maneira mais elegante.

Aqui está uma versão melhorada do código de Marc e agora está disponível como um pacote nuget :

 public static class LambdaCompare { public static bool Eq( Expression> x, Expression> y) { return ExpressionsEqual(x, y, null, null); } public static bool Eq( Expression> x, Expression> y) { return ExpressionsEqual(x, y, null, null); } public static Expression>, bool>> Eq(Expression> y) { return x => ExpressionsEqual(x, y, null, null); } private static bool ExpressionsEqual(Expression x, Expression y, LambdaExpression rootX, LambdaExpression rootY) { if (ReferenceEquals(x, y)) return true; if (x == null || y == null) return false; var valueX = TryCalculateConstant(x); var valueY = TryCalculateConstant(y); if (valueX.IsDefined && valueY.IsDefined) return ValuesEqual(valueX.Value, valueY.Value); if (x.NodeType != y.NodeType || x.Type != y.Type) { if (IsAnonymousType(x.Type) && IsAnonymousType(y.Type)) throw new NotImplementedException("Comparison of Anonymous Types is not supported"); return false; } if (x is LambdaExpression) { var lx = (LambdaExpression)x; var ly = (LambdaExpression)y; var paramsX = lx.Parameters; var paramsY = ly.Parameters; return CollectionsEqual(paramsX, paramsY, lx, ly) && ExpressionsEqual(lx.Body, ly.Body, lx, ly); } if (x is MemberExpression) { var mex = (MemberExpression)x; var mey = (MemberExpression)y; return Equals(mex.Member, mey.Member) && ExpressionsEqual(mex.Expression, mey.Expression, rootX, rootY); } if (x is BinaryExpression) { var bx = (BinaryExpression)x; var by = (BinaryExpression)y; return bx.Method == @by.Method && ExpressionsEqual(bx.Left, @by.Left, rootX, rootY) && ExpressionsEqual(bx.Right, @by.Right, rootX, rootY); } if (x is UnaryExpression) { var ux = (UnaryExpression)x; var uy = (UnaryExpression)y; return ux.Method == uy.Method && ExpressionsEqual(ux.Operand, uy.Operand, rootX, rootY); } if (x is ParameterExpression) { var px = (ParameterExpression)x; var py = (ParameterExpression)y; return rootX.Parameters.IndexOf(px) == rootY.Parameters.IndexOf(py); } if (x is MethodCallExpression) { var cx = (MethodCallExpression)x; var cy = (MethodCallExpression)y; return cx.Method == cy.Method && ExpressionsEqual(cx.Object, cy.Object, rootX, rootY) && CollectionsEqual(cx.Arguments, cy.Arguments, rootX, rootY); } if (x is MemberInitExpression) { var mix = (MemberInitExpression)x; var miy = (MemberInitExpression)y; return ExpressionsEqual(mix.NewExpression, miy.NewExpression, rootX, rootY) && MemberInitsEqual(mix.Bindings, miy.Bindings, rootX, rootY); } if (x is NewArrayExpression) { var nx = (NewArrayExpression)x; var ny = (NewArrayExpression)y; return CollectionsEqual(nx.Expressions, ny.Expressions, rootX, rootY); } if (x is NewExpression) { var nx = (NewExpression)x; var ny = (NewExpression)y; return Equals(nx.Constructor, ny.Constructor) && CollectionsEqual(nx.Arguments, ny.Arguments, rootX, rootY) && (nx.Members == null && ny.Members == null || nx.Members != null && ny.Members != null && CollectionsEqual(nx.Members, ny.Members)); } if (x is ConditionalExpression) { var cx = (ConditionalExpression)x; var cy = (ConditionalExpression)y; return ExpressionsEqual(cx.Test, cy.Test, rootX, rootY) && ExpressionsEqual(cx.IfFalse, cy.IfFalse, rootX, rootY) && ExpressionsEqual(cx.IfTrue, cy.IfTrue, rootX, rootY); } throw new NotImplementedException(x.ToString()); } private static Boolean IsAnonymousType(Type type) { Boolean hasCompilerGeneratedAttribute = type.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Any(); Boolean nameContainsAnonymousType = type.FullName.Contains("AnonymousType"); Boolean isAnonymousType = hasCompilerGeneratedAttribute && nameContainsAnonymousType; return isAnonymousType; } private static bool MemberInitsEqual(ICollection bx, ICollection by, LambdaExpression rootX, LambdaExpression rootY) { if (bx.Count != by.Count) { return false; } if (bx.Concat(by).Any(b => b.BindingType != MemberBindingType.Assignment)) throw new NotImplementedException("Only MemberBindingType.Assignment is supported"); return bx.Cast().OrderBy(b => b.Member.Name).Select((b, i) => new { Expr = b.Expression, b.Member, Index = i }) .Join( by.Cast().OrderBy(b => b.Member.Name).Select((b, i) => new { Expr = b.Expression, b.Member, Index = i }), o => o.Index, o => o.Index, (xe, ye) => new { XExpr = xe.Expr, XMember = xe.Member, YExpr = ye.Expr, YMember = ye.Member }) .All(o => Equals(o.XMember, o.YMember) && ExpressionsEqual(o.XExpr, o.YExpr, rootX, rootY)); } private static bool ValuesEqual(object x, object y) { if (ReferenceEquals(x, y)) return true; if (x is ICollection && y is ICollection) return CollectionsEqual((ICollection)x, (ICollection)y); return Equals(x, y); } private static ConstantValue TryCalculateConstant(Expression e) { if (e is ConstantExpression) return new ConstantValue(true, ((ConstantExpression)e).Value); if (e is MemberExpression) { var me = (MemberExpression)e; var parentValue = TryCalculateConstant(me.Expression); if (parentValue.IsDefined) { var result = me.Member is FieldInfo ? ((FieldInfo)me.Member).GetValue(parentValue.Value) : ((PropertyInfo)me.Member).GetValue(parentValue.Value); return new ConstantValue(true, result); } } if (e is NewArrayExpression) { var ae = ((NewArrayExpression)e); var result = ae.Expressions.Select(TryCalculateConstant); if (result.All(i => i.IsDefined)) return new ConstantValue(true, result.Select(i => i.Value).ToArray()); } if (e is ConditionalExpression) { var ce = (ConditionalExpression)e; var evaluatedTest = TryCalculateConstant(ce.Test); if (evaluatedTest.IsDefined) { return TryCalculateConstant(Equals(evaluatedTest.Value, true) ? ce.IfTrue : ce.IfFalse); } } return default(ConstantValue); } private static bool CollectionsEqual(IEnumerable x, IEnumerable y, LambdaExpression rootX, LambdaExpression rootY) { return x.Count() == y.Count() && x.Select((e, i) => new { Expr = e, Index = i }) .Join(y.Select((e, i) => new { Expr = e, Index = i }), o => o.Index, o => o.Index, (xe, ye) => new { X = xe.Expr, Y = ye.Expr }) .All(o => ExpressionsEqual(oX, oY, rootX, rootY)); } private static bool CollectionsEqual(ICollection x, ICollection y) { return x.Count == y.Count && x.Cast().Select((e, i) => new { Expr = e, Index = i }) .Join(y.Cast().Select((e, i) => new { Expr = e, Index = i }), o => o.Index, o => o.Index, (xe, ye) => new { X = xe.Expr, Y = ye.Expr }) .All(o => Equals(oX, oY)); } private struct ConstantValue { public ConstantValue(bool isDefined, object value) : this() { IsDefined = isDefined; Value = value; } public bool IsDefined { get; private set; } public object Value { get; private set; } } } 

Observe que não compara o AST completo. Em vez disso, ele recolhe expressões constantes e compara seus valores em vez de sua AST. É útil para a validação de simulação quando o lambda possui uma referência à variável local. No seu caso, a variável é comparada pelo seu valor.

Testes unitários:

 [TestClass] public class Tests { [TestMethod] public void BasicConst() { var f1 = GetBasicExpr1(); var f2 = GetBasicExpr2(); Assert.IsTrue(LambdaCompare.Eq(f1, f2)); } [TestMethod] public void PropAndMethodCall() { var f1 = GetPropAndMethodExpr1(); var f2 = GetPropAndMethodExpr2(); Assert.IsTrue(LambdaCompare.Eq(f1, f2)); } [TestMethod] public void MemberInitWithConditional() { var f1 = GetMemberInitExpr1(); var f2 = GetMemberInitExpr2(); Assert.IsTrue(LambdaCompare.Eq(f1, f2)); } [TestMethod] public void AnonymousType() { var f1 = GetAnonymousExpr1(); var f2 = GetAnonymousExpr2(); Assert.Inconclusive("Anonymous Types are not supported"); } private static Expression> GetBasicExpr2() { var const2 = "some const value"; var const3 = "{0}{1}{2}{3}"; return (i, s) => string.Format(const3, (i + 25).ToString(CultureInfo.InvariantCulture), i + s, const2.ToUpper(), 25); } private static Expression> GetBasicExpr1() { var const1 = 25; return (first, second) => string.Format("{0}{1}{2}{3}", (first + const1).ToString(CultureInfo.InvariantCulture), first + second, "some const value".ToUpper(), const1); } private static Expression> GetPropAndMethodExpr2() { return u => Uri.IsWellFormedUriString(u.ToString(), UriKind.Absolute); } private static Expression> GetPropAndMethodExpr1() { return arg1 => Uri.IsWellFormedUriString(arg1.ToString(), UriKind.Absolute); } private static Expression> GetMemberInitExpr2() { var isSecure = true; return u => new UriBuilder(u) { Host = string.IsNullOrEmpty(u.Host) ? "abc" : "def" , Port = isSecure ? 443 : 80 }; } private static Expression> GetMemberInitExpr1() { var port = 443; return x => new UriBuilder(x) { Port = port, Host = string.IsNullOrEmpty(x.Host) ? "abc" : "def" }; } private static Expression> GetAnonymousExpr2() { return u => new { u.Host , Port = 443, Addr = u.AbsolutePath }; } private static Expression> GetAnonymousExpr1() { return x => new { Port = 443, x.Host, Addr = x.AbsolutePath }; } } 

Uma solução canônica seria ótima. Nesse meio tempo, criei uma versão IEqualityComparer . Esta é uma implementação bastante detalhada, então criei uma essência para ela .

Destina-se a ser um comparador abrangente da tree de syntax abstrata. Para esse fim, ele compara cada tipo de expressão, incluindo expressões que ainda não são suportadas pelo C #, como Try e Switch e Block . Os únicos tipos que não comparam são Goto , Label , Loop e DebugInfo devido ao meu conhecimento limitado deles.

Você pode especificar se e como os nomes dos parâmetros e lambdas devem ser comparados, além de como lidar com o ConstantExpression .

Ele rastreia parâmetros posicionalmente por contexto. Lambdas dentro de lambdas e os parâmetros variables ​​do bloco catch são suportados.

Eu sei que esta é uma pergunta antiga, mas eu rolei minha própria expressão tree comparador de igualdade – https://github.com/yesmarket/yesmarket.Linq.Expressions

A implementação faz uso pesado da class ExpressionVisitor para determinar se duas trees de expressão são iguais. À medida que os nós na tree de expressão são percorridos, os nós individuais são comparados para igualdade.