通过递归遍历所有属性的属性来比较两个对象?

我已经编写了一个测试方法,用于比较一个类的两个实例(给出了类型兼容性的假设)。 我自豪地检查了所有的公共财产,确保返回一个差异列表。

问题是某些属性是包含自己属性的对象(子属性,如果可以的话)。 通过逐步完成流程,我可以看到这些没有被比较。

如何设计深入调用并比较所有子属性的调用? 如果方法相对简单,则额外奖励。 🙂

public static class Extensions { public static IEnumerable DiffersOn( this Generic self, Generic another) where Generic : class { if (self == null || another == null) yield return null; Type type = typeof(Generic); IEnumerable properties = type.GetProperties( BindingFlags.Public | BindingFlags.Instance); foreach (PropertyInfo property in properties) { var selfie = type.GetProperty(property.Name).GetValue(self); var othie = type.GetProperty(property.Name).GetValue(another); if (selfie != othie && (selfie == null || !selfie.Equals(othie))) yield return property.Name; } } } 

正如我在评论中所说,最简单的方法是使用BinaryFormatter序列化两个对象并比较原始byte[]流。 这样你就可以比较字段(而不是属性),所以事情可能会有所不同(即使两个对象的私有字段不同,也可以在逻辑上相等)。 最大的优点是序列化将处理一个非常棘手的情况:当对象具有循环引用时。

大概是这样的:

 static bool CheckForEquality(object a, object b) { BinaryFormatter formatter = new BinaryFormatter(); using (MemoryStream streamA = new MemoryStream()) using (MemoryStream streamB = new MemoryStream()) { formatter.Serialize(streamA, a); formatter.Serialize(streamB, b); if (streamA.Length != streamB.Length) return false; streamA.Seek(0, SeekOrigin.Begin); streamB.Seek(0, SeekOrigin.Begin); for (int value = 0; (value = streamA.ReadByte()) >= 0; ) { if (value != streamB.ReadByte()) return false; } return true; } } 

正如Ben Voigt在评论中指出的那样,比较流的算法非常慢,因为快速缓冲比较( MemoryStream将数据保存在byte[]缓冲区中),请参阅他建议的这篇文章 。

如果您需要更多“控制”并实际处理自定义比较,那么您必须使事情变得更复杂。 下一个示例是此比较的第一个原始(和未经测试!)版本。 它没有处理一个非常重要的事情:循环引用。

 static bool CheckForEquality(object a, object b) { if (Object.ReferenceEquals(a, b)) return true; // This is little bit arbitrary, if b has a custom comparison // that may equal to null then this will bypass that. However // it's pretty uncommon for a non-null object to be equal // to null (unless a is null and b is Nullable // without value). Mind this... if (Object.ReferenceEquals(a, null) return false; // Here we handle default and custom comparison assuming // types are "well-formed" and with good habits. Hashcode // checking is a micro optimization, it may speed-up checking // for inequality (if hashes are different then we may safely // assume objects aren't equal...in "well-formed" objects). if (!Object.ReferenceEquals(b, null) && a.GetHashCode() != b.GetHashCode()) return false; if (a.Equals(b)) return true; var comparableA = a as IComparable; if (comparableA != null) return comparableA.CompareTo(b) == 0; // Different instances and one of them is null, they're different unless // it's a special case handled by "a" object (with IComparable). if (Object.ReferenceEquals(b, null)) return false; // In case "b" has a custom comparison for objects of type "a" // but not vice-versa. if (b.Equals(a)) return true; // We assume we can compare only the same type. It's not true // because of custom comparison operators but it should also be // handled in Object.Equals(). var type = a.GetType(); if (type != b.GetType()) return false; // Special case for lists, they won't match but we may consider // them equal if they have same elements and each element match // corresponding one in the other object. // This comparison is order sensitive so A,B,C != C,B,A. // Items must be first ordered if this isn't what you want. // Also note that a better implementation should check for // ICollection as a special case and IEnumerable should be used. // An even better implementation should also check for // IStructuralComparable and IStructuralEquatable implementations. var listA = a as System.Collections.ICollection; if (listA != null) { var listB = b as System.Collections.ICollection; if (listA.Count != listB.Count) return false; var aEnumerator = listA.GetEnumerator(); var bEnumerator = listB.GetEnumerator(); while (aEnumerator.MoveNext() && bEnumerator.MoveNext()) { if (!CheckForEquality(aEnumerator.Current, bEnumerator.Current)) return false; } // We don't return true here, a class may implement IList and also have // many other properties, go on with our comparison } // If we arrived here we have to perform a property by // property comparison recursively calling this function. // Note that here we check for "public interface" equality. var properties = type.GetProperties().Where(x => x.GetMethod != null); foreach (var property in properties) { if (!CheckForEquality(property.GetValue(a), property.GetValue(b))) return false; } // If we arrived here then objects can be considered equal return true; } 

如果你删除评论,你将拥有相当短的代码。 要处理循环引用,你必须避免一次又一次地比较相同的元组,要做到这一点,你必须分割函数,就像在这个例子中一样(非常非常天真的实现,我知道):

 static bool CheckForEquality(object a, object b) { return CheckForEquality(new List>(), a, b); } 

像这样的核心实现(我只重写了重要部分):

 static bool CheckForEquality(List> visitedObjects, object a, object b) { // If we compared this tuple before and we're still comparing // then we can consider them as equal (or irrelevant). if (visitedObjects.Contains(Tuple.Create(a, b))) return true; visitedObjects.Add(Tuple.Create(a, b)); // Go on and pass visitedObjects to recursive calls } 

下一步稍微复杂一点(获取不同属性的列表)因为它可能不那么简单(例如,如果两个属性是列表并且它们具有不同数量的项)。 我将简要介绍一种可能的解决方案(为了清晰起见,删除循环引用的代码)。 请注意,当相等性中断时,后续检查也可能产生意外exception,因此应该比这更好地实现。

新原型将是:

 static void CheckForEquality(object a, object b, List differences) { CheckForEquality("", a, b, differences); } 

并且实现方法还需要跟踪“当前路径”:

 static void CheckForEquality(string path, object a, object b, List differences) { if (a.Equals(b)) return; var comparableA = a as IComparable; if (comparableA != null && comparableA.CompareTo(b) != 0) differences.Add(path); if (Object.ReferenceEquals(b, null)) { differences.Add(path); return; // This is mandatory: nothing else to compare } if (b.Equals(a)) return true; var type = a.GetType(); if (type != b.GetType()) { differences.Add(path); return; // This is mandatory: we can't go on comparing different types } var listA = a as System.Collections.ICollection; if (listA != null) { var listB = b as System.Collections.ICollection; if (listA.Count == listB.Count) { var aEnumerator = listA.GetEnumerator(); var bEnumerator = listB.GetEnumerator(); int i = 0; while (aEnumerator.MoveNext() && bEnumerator.MoveNext()) { CheckForEquality( String.Format("{0}[{1}]", path, i++), aEnumerator.Current, bEnumerator.Current, differences); } } else { differences.Add(path); } } var properties = type.GetProperties().Where(x => x.GetMethod != null); foreach (var property in properties) { CheckForEquality( String.Format("{0}.{1}", path, property.Name), property.GetValue(a), property.GetValue(b), differences); } }