Il modo più efficace per testare l’uguaglianza delle espressioni lambda

Data una firma del metodo:

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

Quale sarebbe il modo più efficace per dire se le due espressioni sono le stesse? Questo ha solo bisogno di funzionare per espressioni semplici, con questo voglio dire che tutto ciò che sarebbe “supportato” sarebbe semplice MemberExpressions, ad esempio c => c.ID.

Un esempio di chiamata potrebbe essere:

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

Hmmm … Immagino che dovresti analizzare l’albero, controllando il tipo di nodo e il membro di ciascuno. Farò un esempio …

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

AGGIORNAMENTO: A causa dell’interesse per la mia soluzione, ho aggiornato il codice in modo da supportare matrici, nuovi operatori e altre cose e mette a confronto gli AST in modo più elegante.

Ecco una versione migliorata del codice di Marc e ora è disponibile come pacchetto 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; } } } 

Si noti che non confronta AST completo. Invece, collassa le espressioni costanti e confronta i loro valori piuttosto che il loro AST. È utile per la convalida del mock quando il lambda ha un riferimento alla variabile locale. Nel suo caso la variabile viene confrontata dal suo valore.

Test unitari:

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

Una soluzione canonica sarebbe grandiosa. Nel frattempo, ho creato una versione di IEqualityComparer . Questa è piuttosto un’implementazione prolissa, quindi ne ho creato un esempio .

È inteso per essere un comparatore di albero di syntax astratto completo. A tal fine, confronta ogni tipo di espressione incluse le espressioni che non sono ancora supportate da C # come Try and Switch e Block . Gli unici tipi che non confronta sono Goto , Label , Loop e DebugInfo causa della mia conoscenza limitata di essi.

È ansible specificare se e come devono essere confrontati i nomi di parametri e lambda, nonché come gestire ConstantExpression .

Traccia i parametri in base al contesto. Lambdas all’interno di lambdas e parametri variabili di blocco catch sono supportati.

So che questa è una vecchia domanda, ma ho eseguito il rollover del mio comparatore di uguaglianza dell’albero delle espressioni – https://github.com/yesmarket/yesmarket.Linq.Expressions

L’implementazione fa un uso pesante della class ExpressionVisitor per determinare se due alberi di espressioni sono uguali. Mentre i nodes dell’albero dell’espressione vengono attraversati, i singoli nodes vengono confrontati per l’uguaglianza.