【问题标题】:C# implementation of deep/recursive object comparison in .net 3.5.net 3.5 中深度/递归对象比较的 C# 实现
【发布时间】:2018-05-09 16:14:05
【问题描述】:

我正在寻找递归或深度对象比较的 C# 特定的开源(或可用源代码)实现。

我目前有两个活动对象图表,我希望将它们相互比较,比较的结果是图表中的一组差异。这些对象是一组在运行时(但不一定在编译时)已知的类的实例化。

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

【问题讨论】:

  • 我知道这对你没有多大帮助,但我写了这样一个东西,相当复杂。我想让它开源,但必须先问我的老板。我还有一些其他很酷的东西,比如对象创建器。它开箱即用,但可以通过一些流畅的语法对每个细节进行配置。但我在说什么......恐怕你在接下来的几周内不会明白。

标签: c# recursion comparison


【解决方案1】:

我在www.kellermansoftware.com 找到了一个非常好的免费实现,名为 Compare .NET Objects,可以在here 找到。强烈推荐。


似乎已迁移到 github - 最新版本可用 here

【讨论】:

  • 这些解决方案是否检查循环引用?
  • 是的。比较 .NET 对象。
  • CompareNETObjects 看起来很像,但它没有使用表达式树(或其种类)来加快比较速度。在某些条件下,简单地使用反射可能是不可接受的。
【解决方案2】:

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

无论如何,这将是我的默认方法。

【讨论】:

  • 这真是太好了;在进行比较之前,我没有想过将它们序列化。无论如何,我不认为这会很漂亮。为了我的使用,我必须构建一个包含所有冲突对象的结构。我认为 XML-Diff 输出不会特别容易
  • 这对我来说是 C#(和 java)中最悲伤的部分。你需要这个疯狂的拜占庭系统来做一个简单的深度比较,而在 C++ 中它非常自然。
  • @v.oddou 在考虑多个指针链接对象时,c++ 以什么方式使这更容易?
  • @MarcGravell:我认为这要归功于编译器提供的复制构造函数和赋值运算符,这要容易得多。使用原始指针变得越来越少,现在通常使用定义明确的语义指针,unique_ptr、shared_ptr、value_ptr。我使用零规则来最大限度地利用编译器并最大限度地减少编写的代码。 flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html
  • @v.oddou 分配不是比较。为什么我主动要求你解释,你却指责我心胸狭窄?
【解决方案3】:

这是我们用于比较复杂图形的 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);
        }
    }
}

【讨论】:

  • 您提供的代码缺少DatesEqualConstraint()方法和所有相关方法(如.Within())。
  • @Daniel T.,我当时没有发表评论,但我在您发表评论后将DatesEqualConstraint 添加到答案中。
  • 'tolerance' 代码中的变量不存在。更新不完全。
  • @Thomas G. Mayfield, tolerance 在 NUnit 框架内的父类 Constraint 中声明。
  • @Thomas G. Mayfield,看起来tolerance 在 NUnit 2.5 中已被删除。上面的代码是针对 NUnit 2.4.6 编写的。它需要稍作修改才能与 NUnit 2.5 一起使用(他们确实会改变很多 API)。
【解决方案4】:

这实际上是一个简单的过程。使用反射,您可以比较对象上的每个字段。

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

【讨论】:

  • 根据经验,对于深度对象来说,它可能会变得非常棘手——尤其是在比较子集合时。
  • 这确实意味着这是一个糟糕的解决方案。在大多数情况下,这种方法效果很好。
  • 是的,当我运行它时,我会遇到集合问题。此外,没有内置循环检测 - 此方法适用于单向树,不适用于通用图。
  • 您还需要通过调用 Equal、IComparable 或一些自定义比较来定义如何递归地比较对象。比较列表和字典并非易事。实际上,您总是有一些例外,您想忽略的字段。
  • 是的,你是对的 - Jesse 在上面链接到的 www.kellermansoftware.com 上的代码似乎可以正确处理所有事情。
【解决方案5】:

使用 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】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-27
      • 2017-11-05
      • 2013-11-30
      • 2019-03-23
      • 2020-08-03
      相关资源
      最近更新 更多