Kon*_*ten 0 c# testing recursion properties
我已经编写了一个测试方法,用于比较一个类的两个实例(给出了类型兼容性的假设).我自豪地检查了所有的公共财产,确保返回一个差异列表.
问题是某些属性是包含自己属性的对象(子属性,如果可以的话).通过逐步完成流程,我可以看到,这些都没有被比较.
如何设计深入调用并比较所有子属性的调用?如果方法相对简单,额外奖励.:)
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;
    }
  }
}
正如我在评论中所说,最简单的方法是使用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<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);
    }
}