我已经编写了一个测试方法,用于比较一个类的两个实例(给出了类型兼容性的假设)。我自豪地检查了所有的公共场所,确保返回一个差异列表。
问题是某些属性是包含自己属性的对象(子属性,如果可以的话)。通过逐步完成流程,我可以看到这些并没有被比较。
如何设计深入调用并比较所有子属性的调用?如果方法相对简单,额外奖励。 :)
public static class Extensions
{
public static IEnumerable<string> DiffersOn<Generic>(
this Generic self, Generic another) where Generic : class
{
if (self == null || another == null)
yield return null;
Type type = typeof(Generic);
IEnumerable<PropertyInfo> 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;
}
}
}
答案 0 :(得分:5)
正如我在评论中所说的最简单的方法是使用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[]
缓冲区中)请参阅this post他建议。
如果您需要更多“控制”并实际处理自定义比较,那么您必须使事情变得更复杂。下一个示例是此比较的第一个原始(和未经测试!)版本。它没有处理一个非常重要的事情:循环引用。
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<T>
// 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<Tuple<object, object>>(), a, b);
}
使用这样的核心实现(我只重写了重要部分):
static bool CheckForEquality(List<Tuple<object, object>> 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
}
下一步有点复杂(获取不同属性的列表),因为它可能不那么简单(例如,如果两个属性是列表并且它们具有不同数量的项)。我只是草图一个可能的解决方案(为清晰起见,删除循环引用的代码)。请注意,当相等性中断时,后续检查也可能产生意外的异常,因此应该比这更好地实现。
新原型将是:
static void CheckForEquality(object a, object b, List<string> differences)
{
CheckForEquality("", a, b, differences);
}
实施方法还需要跟踪“当前路径”:
static void CheckForEquality(string path,
object a, object b,
List<string> 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);
}
}