代码之家  ›  专栏  ›  技术社区  ›  JontyMC

测试lambda表达式相等性的最有效方法

  •  40
  • JontyMC  · 技术社区  · 16 年前

    给定方法签名:

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

    如果两个表达式相同,最有效的表达方式是什么?这只需要对简单表达式有效,我的意思是所有“支持”的都是简单的成员表达式,例如c=>c.id。

    示例调用可能是:

    AreTheSame<User>(u1 => u1.ID, u2 => u2.ID); --> would return true
    
    4 回复  |  直到 7 年前
        1
  •  32
  •   Marc Gravell    16 年前

    六羟甲基三聚氰胺六甲醚。。。我想您必须解析树,检查每个树的节点类型和成员。我来举个例子…

    using System;
    using System.Linq.Expressions;
    class Test {
        public string Foo { get; set; }
        public string Bar { get; set; }
        static void Main()
        {
            bool test1 = FuncTest<Test>.FuncEqual(x => x.Bar, y => y.Bar),
                test2 = FuncTest<Test>.FuncEqual(x => x.Foo, y => y.Bar);
        }
    
    }
    // this only exists to make it easier to call, i.e. so that I can use FuncTest<T> with
    // generic-type-inference; if you use the doubly-generic method, you need to specify
    // both arguments, which is a pain...
    static class FuncTest<TSource>
    {
        public static bool FuncEqual<TValue>(
            Expression<Func<TSource, TValue>> x,
            Expression<Func<TSource, TValue>> y)
        {
            return FuncTest.FuncEqual<TSource, TValue>(x, y);
        }
    }
    static class FuncTest {
        public static bool FuncEqual<TSource, TValue>(
            Expression<Func<TSource,TValue>> x,
            Expression<Func<TSource,TValue>> 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());
            }
        }
    }
    
        2
  •  30
  •   neleus    7 年前

    更新: 由于对我的解决方案感兴趣,我更新了代码,使它支持数组、新的操作符和其他东西,并以更优雅的方式比较AST。

    这是Marc代码的改进版本,现在 它可以作为 nuget package :

    public static class LambdaCompare
    {
        public static bool Eq<TSource, TValue>(
            Expression<Func<TSource, TValue>> x,
            Expression<Func<TSource, TValue>> y)
        {
            return ExpressionsEqual(x, y, null, null);
        }
    
        public static bool Eq<TSource1, TSource2, TValue>(
            Expression<Func<TSource1, TSource2, TValue>> x,
            Expression<Func<TSource1, TSource2, TValue>> y)
        {
            return ExpressionsEqual(x, y, null, null);
        }
    
        public static Expression<Func<Expression<Func<TSource, TValue>>, bool>> Eq<TSource, TValue>(Expression<Func<TSource, TValue>> 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<MemberBinding> bx, ICollection<MemberBinding> 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<MemberAssignment>().OrderBy(b => b.Member.Name).Select((b, i) => new { Expr = b.Expression, b.Member, Index = i })
                .Join(
                      by.Cast<MemberAssignment>().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<Expression> x, IEnumerable<Expression> 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(o.X, o.Y, rootX, rootY));
        }
    
        private static bool CollectionsEqual(ICollection x, ICollection y)
        {
            return x.Count == y.Count
                   && x.Cast<object>().Select((e, i) => new { Expr = e, Index = i })
                       .Join(y.Cast<object>().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(o.X, o.Y));
        }
    
        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; }
        }
    }
    

    请注意,它不会比较完整的AST。相反,它折叠常量表达式并比较它们的值,而不是它们的AST。 当lambda引用局部变量时,它对模拟验证很有用。在这种情况下,变量是通过其值进行比较的。

    单元测试:

    [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<Func<int, string, string>> 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<Func<int, string, string>> 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<Func<Uri, bool>> GetPropAndMethodExpr2()
        {
            return u => Uri.IsWellFormedUriString(u.ToString(), UriKind.Absolute);
        }
    
        private static Expression<Func<Uri, bool>> GetPropAndMethodExpr1()
        {
            return arg1 => Uri.IsWellFormedUriString(arg1.ToString(), UriKind.Absolute);
        }
    
        private static Expression<Func<Uri, UriBuilder>> GetMemberInitExpr2()
        {
            var isSecure = true;
            return u => new UriBuilder(u) { Host = string.IsNullOrEmpty(u.Host) ? "abc" : "def" , Port = isSecure ? 443 : 80 };
        }
    
        private static Expression<Func<Uri, UriBuilder>> GetMemberInitExpr1()
        {
            var port = 443;
            return x => new UriBuilder(x) { Port = port, Host = string.IsNullOrEmpty(x.Host) ? "abc" : "def" };
        }
    
        private static Expression<Func<Uri, object>> GetAnonymousExpr2()
        {
            return u => new { u.Host , Port = 443, Addr = u.AbsolutePath };
        }
    
        private static Expression<Func<Uri, object>> GetAnonymousExpr1()
        {
            return x => new { Port = 443, x.Host, Addr = x.AbsolutePath };
        }
    }
    
        3
  •  3
  •   jnm2    9 年前

    一个规范的解决方案会很好。同时,我创建了一个 IEqualityComparer<Expression> 版本。 这是一个相当冗长的实现,所以我 created a gist for it .

    它是一个综合的抽象语法树比较器。为此,它比较了所有表达式类型,包括尚不受C类支持的表达式 Try Switch Block . 它唯一没有比较的类型是 Goto , Label , Loop DebugInfo 因为我对它们的了解有限。

    您可以指定是否以及如何比较参数和lambda的名称,以及如何处理 ConstantExpression .

    它根据上下文按位置跟踪参数。支持lambdas中的lambda和catch块变量参数。

        4
  •  2
  •   Ryan.Bartsch    8 年前

    我知道这是一个古老的问题,但我推出了我自己的表达式树平等比较器- https://github.com/yesmarket/yesmarket.Linq.Expressions

    该实现大量使用ExpressionVisitor类来确定两个表达式树是否相等。当遍历表达式树中的节点时,将比较各个节点是否相等。