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

在.NET3.5中实现深度/递归对象比较

  •  48
  • blueberryfields  · 技术社区  · 16 年前

    我正在寻找一种特定于C语言的、开源(或可用的源代码)的递归或深层对象比较实现。

    有一个特定的要求是能够从图表中的差异映射回包含差异的对象。

    6 回复  |  直到 16 年前
        1
  •  44
  •   blueberryfields    9 年前

    我发现了一个非常好的免费实现 www.kellermansoftware.com 调用比较可以找到的.NET对象 here


    似乎已重新定位到github-最新版本可用 here

        2
  •  16
  •   Marc Gravell    8 年前

    这是一个复杂的领域;我在运行时做过一些类似的事情,但很快就会变得一团糟。如果可能的话,您可能会发现实现这一点的最简单方法是序列化对象并比较序列化的表单(可能是xml diff和xml) XmlSerializer 小的 通过直到运行时才知道的类型,但不是很大的类型(您总是可以使用 new XmlSerializer(obj.GetType()) 等等)。

    那将是我的梦想 不管怎么说,都是接近。

        3
  •  9
  •   Samuel Neff    16 年前

    这里有一个NUnit 2.4.6自定义约束,我们用于比较复杂图形。它支持嵌入集合、父引用、设置数值比较的容差、标识要忽略的字段名(甚至在层次结构的深处),以及装饰要始终忽略的类型。

    我确信这段代码可以在NUnit之外使用,大部分代码并不依赖于NUnit。

    我们在数千个单元测试中使用了它。

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Reflection;
    using System.Text;
    using NUnit.Framework;
    using NUnit.Framework.Constraints;
    
    namespace Tests
    {
        public class ContentsEqualConstraint : Constraint
        {
            private readonly object expected;
            private Constraint failedEquality;
            private string expectedDescription;
            private string actualDescription;
    
            private readonly Stack<string> typePath = new Stack<string>();
            private string typePathExpanded;
    
            private readonly HashSet<string> _ignoredNames = new HashSet<string>();
            private readonly HashSet<Type> _ignoredTypes = new HashSet<Type>();
            private readonly LinkedList<Type> _ignoredInterfaces = new LinkedList<Type>();
            private readonly LinkedList<string> _ignoredSuffixes = new LinkedList<string>();
            private readonly IDictionary<Type, Func<object, object, bool>> _predicates = new Dictionary<Type, Func<object, object, bool>>();
    
            private bool _withoutSort;
            private int _maxRecursion = int.MaxValue;
    
            private readonly HashSet<VisitedComparison> _visitedObjects = new HashSet<VisitedComparison>();
    
            private static readonly HashSet<string> _globallyIgnoredNames = new HashSet<string>();
            private static readonly HashSet<Type> _globallyIgnoredTypes = new HashSet<Type>();
            private static readonly LinkedList<Type> _globallyIgnoredInterfaces = new LinkedList<Type>();
    
            private static object _regionalTolerance;
    
            public ContentsEqualConstraint(object expectedValue)
            {
                expected = expectedValue;
            }
    
            public ContentsEqualConstraint Comparing<T>(Func<T, T, bool> predicate)
            {
                Type t = typeof (T);
    
                if (predicate == null)
                {
                    _predicates.Remove(t);
                }
                else
                {
                    _predicates[t] = (x, y) => predicate((T) x, (T) y);
                }
                return this;
            }
    
            public ContentsEqualConstraint Ignoring(string fieldName)
            {
                _ignoredNames.Add(fieldName);
                return this;
            }
    
            public ContentsEqualConstraint Ignoring(Type fieldType)
            {
                if (fieldType.IsInterface)
                {
                    _ignoredInterfaces.AddFirst(fieldType);
                }
                else
                {
                    _ignoredTypes.Add(fieldType);
                }
                return this;
            }
    
            public ContentsEqualConstraint IgnoringSuffix(string suffix)
            {
                if (string.IsNullOrEmpty(suffix))
                {
                    throw new ArgumentNullException("suffix");
                }
                _ignoredSuffixes.AddLast(suffix);
                return this;
            }
    
            public ContentsEqualConstraint WithoutSort()
            {
                _withoutSort = true;
                return this;
            }
    
            public ContentsEqualConstraint RecursingOnly(int levels)
            {
                _maxRecursion = levels;
                return this;
            }
    
            public static void GlobalIgnore(string fieldName)
            {
                _globallyIgnoredNames.Add(fieldName);
            }
    
            public static void GlobalIgnore(Type fieldType)
            {
                if (fieldType.IsInterface)
                {
                    _globallyIgnoredInterfaces.AddFirst(fieldType);
                }
                else
                {
                    _globallyIgnoredTypes.Add(fieldType);
                }
            }
    
            public static IDisposable RegionalIgnore(string fieldName)
            {
                return new RegionalIgnoreTracker(fieldName);
            }
    
            public static IDisposable RegionalIgnore(Type fieldType)
            {
                return new RegionalIgnoreTracker(fieldType);
            }
    
            public static IDisposable RegionalWithin(object tolerance)
            {
                return new RegionalWithinTracker(tolerance);
            }
    
            public override bool Matches(object actualValue)
            {
                typePathExpanded = null;
                actual = actualValue;
                return Matches(expected, actualValue);
            }
    
            private bool Matches(object expectedValue, object actualValue)
            {
    
                bool matches = true;
    
                if (!MatchesNull(expectedValue, actualValue, ref matches))
                {
                    return matches;
                }
                // DatesEqualConstraint supports tolerance in dates but works as equal constraint for everything else
                Constraint eq = new DatesEqualConstraint(expectedValue).Within(tolerance ?? _regionalTolerance);
                if (eq.Matches(actualValue))
                {
                    return true;
                }
    
                if (MatchesVisited(expectedValue, actualValue, ref matches))
                {
                    if (MatchesDictionary(expectedValue, actualValue, ref matches) &&
                        MatchesList(expectedValue, actualValue, ref matches) &&
                        MatchesType(expectedValue, actualValue, ref matches) &&
                        MatchesPredicate(expectedValue, actualValue, ref matches))
                    {
                        MatchesFields(expectedValue, actualValue, eq, ref matches);
                    }
                }
    
                return matches;
            }
    
            private bool MatchesNull(object expectedValue, object actualValue, ref bool matches)
            {
                if (IsNullEquivalent(expectedValue))
                {
                    expectedValue = null;
                }
    
                if (IsNullEquivalent(actualValue))
                {
                    actualValue = null;
                }
    
                if (expectedValue == null && actualValue == null)
                {
                    matches = true;
                    return false;
                }
    
                if (expectedValue == null)
                {
                    expectedDescription = "null";
                    actualDescription = "NOT null";
                    matches = Failure;
                    return false;
                }
    
                if (actualValue == null)
                {
                    expectedDescription = "not null";
                    actualDescription = "null";
                    matches = Failure;
                    return false;
                }
    
                return true;
            }
    
            private bool MatchesType(object expectedValue, object actualValue, ref bool matches)
            {
                Type expectedType = expectedValue.GetType();
                Type actualType = actualValue.GetType();
    
                if (expectedType != actualType)
                {
                    try
                    {
                        Convert.ChangeType(actualValue, expectedType);
                    }
                    catch(InvalidCastException)             
                    {
                        expectedDescription = expectedType.FullName;
                        actualDescription = actualType.FullName;
                        matches = Failure;
                        return false;
                    }
    
                }
                return true;
            }
    
            private bool MatchesPredicate(object expectedValue, object actualValue, ref bool matches)
            {
                Type t = expectedValue.GetType();
                Func<object, object, bool> predicate;
    
                if (_predicates.TryGetValue(t, out predicate))
                {
                    matches = predicate(expectedValue, actualValue);
                    return false;
                }
                return true;
            }
    
            private bool MatchesVisited(object expectedValue, object actualValue, ref bool matches)
            {
                var c = new VisitedComparison(expectedValue, actualValue);
    
                if (_visitedObjects.Contains(c))
                {
                    matches = true;
                    return false;
                }
    
                _visitedObjects.Add(c);
    
                return true;
            }
    
            private bool MatchesDictionary(object expectedValue, object actualValue, ref bool matches)
            {
                if (expectedValue is IDictionary && actualValue is IDictionary)
                {
                    var expectedDictionary = (IDictionary)expectedValue;
                    var actualDictionary = (IDictionary)actualValue;
    
                    if (expectedDictionary.Count != actualDictionary.Count)
                    {
                        expectedDescription = expectedDictionary.Count + " item dictionary";
                        actualDescription = actualDictionary.Count + " item dictionary";
                        matches = Failure;
                        return false;
                    }
    
                    foreach (DictionaryEntry expectedEntry in expectedDictionary)
                    {
                        if (!actualDictionary.Contains(expectedEntry.Key))
                        {
                            expectedDescription = expectedEntry.Key + " exists";
                            actualDescription = expectedEntry.Key + " does not exist";
                            matches = Failure;
                            return false;
                        }
                        if (CanRecurseFurther)
                        {
                            typePath.Push(expectedEntry.Key.ToString());
                            if (!Matches(expectedEntry.Value, actualDictionary[expectedEntry.Key]))
                            {
                                matches = Failure;
                                return false;
                            }
                            typePath.Pop();
                        }
                    }
                    matches = true;
                    return false;
                }
                return true;
            }
    
            private bool MatchesList(object expectedValue, object actualValue, ref bool matches)
            {
                if (!(expectedValue is IList && actualValue is IList))
                {
                    return true;
                }
    
                var expectedList = (IList) expectedValue;
                var actualList = (IList) actualValue;
    
                if (!Matches(expectedList.Count, actualList.Count))
                {
                    matches = false;
                }
                else
                {
                    if (CanRecurseFurther)
                    {
                        int max = expectedList.Count;
    
                        if (max != 0 && !_withoutSort)
                        {
                            SafeSort(expectedList);
                            SafeSort(actualList);
                        }
    
                        for (int i = 0; i < max; i++)
                        {
                            typePath.Push(i.ToString());
    
                            if (!Matches(expectedList[i], actualList[i]))
                            {
                                matches = false;
                                return false;
                            }
                            typePath.Pop();
                        }
                    }
                    matches = true;
                }
                return false;
            }
    
            private void MatchesFields(object expectedValue, object actualValue, Constraint equalConstraint, ref bool matches)
            {
                Type expectedType = expectedValue.GetType();
    
                FieldInfo[] fields = expectedType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
    
                // should have passed the EqualConstraint check
                if (expectedType.IsPrimitive ||
                    expectedType == typeof(string) ||
                    expectedType == typeof(Guid) ||
                    fields.Length == 0)
                {
                    failedEquality = equalConstraint;
                    matches = Failure;
                    return;
                }
    
                if (expectedType == typeof(DateTime))
                {
                    var expectedDate = (DateTime)expectedValue;
                    var actualDate = (DateTime)actualValue;
    
                    if (Math.Abs((expectedDate - actualDate).TotalSeconds) > 3.0)
                    {
                        failedEquality = equalConstraint;
                        matches = Failure;
                        return;
                    }
                    matches = true;
                    return;
                }
    
                if (CanRecurseFurther)
                {
                    while(true)
                    {
                        foreach (FieldInfo field in fields)
                        {
                            if (!Ignore(field))
                            {
                                typePath.Push(field.Name);
                                if (!Matches(GetValue(field, expectedValue), GetValue(field, actualValue)))
                                {
                                    matches = Failure;
                                    return;
                                }
                                typePath.Pop();
                            }
                        }
                        expectedType = expectedType.BaseType;
                        if (expectedType == null)
                        {
                            break;
                        }
                        fields = expectedType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
                    }
                }
                matches = true;
                return;
            }
    
            private bool Ignore(FieldInfo field)
            {
                if (_ignoredNames.Contains(field.Name) ||
                    _ignoredTypes.Contains(field.FieldType) ||
                    _globallyIgnoredNames.Contains(field.Name) ||
                    _globallyIgnoredTypes.Contains(field.FieldType) ||
                    field.GetCustomAttributes(typeof (IgnoreContentsAttribute), false).Length != 0)
                {
                    return true;
                }
    
                foreach(string ignoreSuffix in _ignoredSuffixes)
                {
                    if (field.Name.EndsWith(ignoreSuffix))
                    {
                        return true;
                    }
                }
    
                foreach (Type ignoredInterface in _ignoredInterfaces)
                {
                    if (ignoredInterface.IsAssignableFrom(field.FieldType))
                    {
                        return true;
                    }
                }
                return false;
            }
    
            private static bool Failure
            {
                get
                {
                    return false;
                }
            }
    
            private static bool IsNullEquivalent(object value)
            {
                return value == null ||
                        value == DBNull.Value ||
                       (value is int && (int) value == int.MinValue) ||
                       (value is double && (double) value == double.MinValue) ||
                       (value is DateTime && (DateTime) value == DateTime.MinValue) ||
                       (value is Guid && (Guid) value == Guid.Empty) ||
                       (value is IList && ((IList)value).Count == 0);
            }
    
            private static object GetValue(FieldInfo field, object source)
            {
                try
                {
                    return field.GetValue(source);
                }
                catch(Exception ex)
                {
                    return ex;
                }
            }
    
            public override void WriteMessageTo(MessageWriter writer)
            {
                if (TypePath.Length != 0)
                {
                    writer.WriteLine("Failure on " + TypePath);
                }
    
                if (failedEquality != null)
                {
                    failedEquality.WriteMessageTo(writer);
                }
                else
                {
                    base.WriteMessageTo(writer);
                }
            }
            public override void WriteDescriptionTo(MessageWriter writer)
            {
                writer.Write(expectedDescription);
            }
    
            public override void WriteActualValueTo(MessageWriter writer)
            {
                writer.Write(actualDescription);
            }
    
            private string TypePath
            {
                get
                {
                    if (typePathExpanded == null)
                    {
                        string[] p = typePath.ToArray();
                        Array.Reverse(p);
                        var text = new StringBuilder(128);
                        bool isFirst = true;
                        foreach(string part in p)
                        {
                            if (isFirst)
                            {
                                text.Append(part);
                                isFirst = false;
                            }
                            else
                            {
                                int i;
                                if (int.TryParse(part, out i))
                                {
                                    text.Append("[" + part + "]");
                                }
                                else
                                {
                                    text.Append("." + part);
                                }
                            }
                        }
                        typePathExpanded = text.ToString();
                    }
                    return typePathExpanded;
                }
            }
    
            private bool CanRecurseFurther
            {
                get
                {
                    return typePath.Count < _maxRecursion;
                }
            }
    
            private static bool SafeSort(IList list)
            {
                if (list == null)
                {
                    return false;
                }
    
                if (list.Count < 2)
                {
                    return true;
                }
    
                try
                {
                    object first = FirstNonNull(list) as IComparable;
                    if (first == null)
                    {
                        return false;
                    }
    
                    if (list is Array)
                    {
                        Array.Sort((Array)list);
                        return true;
                    }
                    return CallIfExists(list, "Sort");
                }
                catch
                {
                    return false;
                }
            }
    
            private static object FirstNonNull(IEnumerable enumerable)
            {
                if (enumerable == null)
                {
                    throw new ArgumentNullException("enumerable");
                }
                foreach (object item in enumerable)
                {
                    if (item != null)
                    {
                        return item;
                    }
                }
                return null;
            }
    
            private static bool CallIfExists(object instance, string method)
            {
                if (instance == null)
                {
                    throw new ArgumentNullException("instance");
                }
                if (String.IsNullOrEmpty(method))
                {
                    throw new ArgumentNullException("method");
                }
                Type target = instance.GetType();
                MethodInfo m = target.GetMethod(method, new Type[0]);
                if (m != null)
                {
                    m.Invoke(instance, null);
                    return true;
                }
                return false;
            }
    
            #region VisitedComparison Helper
    
            private class VisitedComparison
            {
                private readonly object _expected;
                private readonly object _actual;
    
                public VisitedComparison(object expected, object actual)
                {
                    _expected = expected;
                    _actual = actual;
                }
    
                public override int GetHashCode()
                {
                    return GetHashCode(_expected) ^ GetHashCode(_actual);
                }
    
                private static int GetHashCode(object o)
                {
                    if (o == null)
                    {
                        return 0;
                    }
                    return o.GetHashCode();
                }
    
                public override bool Equals(object obj)
                {
                    if (obj == null)
                    {
                        return false;
                    }
    
                    if (obj.GetType() != typeof(VisitedComparison))
                    {
                        return false;
                    }
    
                    var other = (VisitedComparison) obj;
                    return _expected == other._expected &&
                           _actual == other._actual;
                }
            }
    
            #endregion
    
            #region RegionalIgnoreTracker Helper
    
            private class RegionalIgnoreTracker : IDisposable
            {
                private readonly string _fieldName;
                private readonly Type _fieldType;
    
                public RegionalIgnoreTracker(string fieldName)
                {
                    if (!_globallyIgnoredNames.Add(fieldName))
                    {
                        _globallyIgnoredNames.Add(fieldName);
                        _fieldName = fieldName;
                    }
                }
    
                public RegionalIgnoreTracker(Type fieldType)
                {
                    if (!_globallyIgnoredTypes.Add(fieldType))
                    {
                        _globallyIgnoredTypes.Add(fieldType);
                        _fieldType = fieldType;
                    }
                }
    
                public void Dispose()
                {
                    if (_fieldName != null)
                    {
                        _globallyIgnoredNames.Remove(_fieldName);
                    }
                    if (_fieldType != null)
                    {
                        _globallyIgnoredTypes.Remove(_fieldType);
                    }
                }
            }
    
            #endregion
    
            #region RegionalWithinTracker Helper
    
            private class RegionalWithinTracker : IDisposable
            {
                public RegionalWithinTracker(object tolerance)
                {
                    _regionalTolerance = tolerance;
                }
    
                public void Dispose()
                {
                    _regionalTolerance = null;
                }
            }
    
            #endregion
    
            #region IgnoreContentsAttribute
    
            [AttributeUsage(AttributeTargets.Field)]
            public sealed class IgnoreContentsAttribute : Attribute
            {
            }
    
            #endregion
        }
        public class DatesEqualConstraint : EqualConstraint
        {
            private readonly object _expected;
    
            public DatesEqualConstraint(object expectedValue) : base(expectedValue)
            {
                _expected = expectedValue;
            }
    
            public override bool Matches(object actualValue)
            {
                if (tolerance != null && tolerance is TimeSpan)
                {
                    if (_expected is DateTime && actualValue is DateTime)
                    {
                        var expectedDate = (DateTime) _expected;
                        var actualDate = (DateTime) actualValue;
                        var toleranceSpan = (TimeSpan) tolerance;
    
                        if ((actualDate - expectedDate).Duration() <= toleranceSpan)
                        {
                            return true;
                        }
                    }
                    tolerance = null;
                }
                return base.Matches(actualValue);
            }
        }
    }
    
        4
  •  4
  •   ChaosPandion    16 年前

    public static Boolean ObjectMatches(Object x, Object y)
    {
        if (x == null && y == null)
            return true;
        else if ((x == null && y != null) || (x != null && y == null))
            return false;       
    
        Type tx = x.GetType();
        Type ty = y.GetType();
    
        if (tx != ty)
            return false;
    
        foreach(FieldInfo field in tx.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
        {
            if (field.FieldType.IsValueType && (field.GetValue(x).ToString() != field.GetValue(y).ToString()))
                return false;
            else if (field.FieldType.IsClass && !ObjectMatches(field.GetValue(x), field.GetValue(y)))
                return false;               
        }
    
        return true;
    }
    
        5
  •  2
  •   Rahbek    9 年前

    使用Jesse建议的Nuget和这段代码,我成功地比较了两个对象,获得了很好的结果。

    using KellermanSoftware.CompareNetObjects;
    using System;
    
    namespace MyProgram.UnitTestHelper
    {
      public class ObjectComparer
      {
    
        public static bool ObjectsHaveSameValues(object first, object second)
        {
          CompareLogic cl = new CompareLogic();
          ComparisonResult result = cl.Compare(first, second);
    
          if (!result.AreEqual)
            Console.WriteLine(result.DifferencesString);
    
          return result.AreEqual;
        }
      }
    }
    
        6
  •  0
  •   Mark Uebel    9 年前

    下面是一个简单的比较器,我们在单元测试中使用它来断言两个对象具有相同的属性。它是各种文章中的想法的集合体,并处理循环引用。

    public static class TestHelper
    {
        public static void AssertAreEqual(Object expected, Object actual, String name)
        {
            // Start a new check with an empty list of actual objects checked
            // The list of actual objects checked is used to ensure that circular references don't result in infinite recursion
            List<Object> actualObjectsChecked = new List<Object>();
            AssertAreEqual(expected, actual, name, actualObjectsChecked);
        }
    
        private static void AssertAreEqual(Object expected, Object actual, String name, List<Object> actualObjectsChecked)
        {
            // Just return if already checked the actual object
            if (actualObjectsChecked.Contains(actual))
            {
                return;
            }
    
            actualObjectsChecked.Add(actual);
    
            // If both expected and actual are null, they are considered equal
            if (expected == null && actual == null)
            {
                return;
            }
    
            if (expected == null && actual != null)
            {
                Assert.Fail(String.Format("The actual value of {0} was not null when null was expected.", name));
            }
    
            if (expected != null && actual == null)
            {
                Assert.Fail(String.Format("The actual value of {0} was null when an instance was expected.", name));
            }
    
            // Get / check type info
            // Note: GetType always returns instantiated (i.e. most derived) type
            Type expectedType = expected.GetType();
            Type actualType = actual.GetType();
            if (expectedType != actualType)
            {
                Assert.Fail(String.Format("The actual type of {0} was not the same as the expected type.", name));
            }
    
            // If expected is a Primitive, Value, or IEquatable type, assume Equals is sufficient to check
            // Note: Every IEquatable type should have also overridden Object.Equals
            if (expectedType.IsPrimitive || expectedType.IsValueType || expectedType.IsIEquatable())
            {
                Assert.IsTrue(expected.Equals(actual), "The actual {0} is not equal to the expected.", name);
                return;
            }
    
            // If expected is an IEnumerable type, assume comparing enumerated items is sufficient to check
            IEnumerable<Object> expectedEnumerable = expected as IEnumerable<Object>;
            IEnumerable<Object> actualEnumerable = actual as IEnumerable<Object>;
            if ((expectedEnumerable != null) && (actualEnumerable != null))
            {
                Int32 actualEnumerableCount = actualEnumerable.Count();
                Int32 expectedEnumerableCount = expectedEnumerable.Count();
    
                // Check size first
                if (actualEnumerableCount != expectedEnumerableCount)
                {
                    Assert.Fail(String.Format("The actual number of enumerable items in {0} did not match the expected number.", name));
                }
    
                // Check items in order, assuming order is the same
                for (int i = 0; i < actualEnumerableCount; ++i)
                {
                    AssertAreEqual(expectedEnumerable.ElementAt(i), actualEnumerable.ElementAt(i), String.Format("{0}[{1}]", name, i), actualObjectsChecked);
                }
    
                return;
            }
    
            // If expected is not a Primitive, Value, IEquatable, or Ienumerable type, assume comparing properties is sufficient to check
            // Iterate through properties
            foreach (PropertyInfo propertyInfo in actualType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                // Skip properties that can't be read or require parameters
                if ((!propertyInfo.CanRead) || (propertyInfo.GetIndexParameters().Length != 0))
                {
                    continue;
                }
    
                // Get properties from both
                Object actualProperty = propertyInfo.GetValue(actual, null);
                Object expectedProperty = propertyInfo.GetValue(expected, null);
    
                AssertAreEqual(expectedProperty, actualProperty, String.Format("{0}.{1}", name, propertyInfo.Name), actualObjectsChecked);
            }
        }
    
        public static Boolean IsIEquatable(this Type type)
        {
            return type.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEquatable<>));
        }
    
    }