通过递归遍历所有属性的属性来比较两个对象? [英] Comparing two objects by iterating recursively through all the properties' properties?

查看:26
本文介绍了通过递归遍历所有属性的属性来比较两个对象?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我写了一个测试方法来比较一个类的两个实例(假设类型兼容).我很自豪地检查了所有公共属性,确保返回差异列表.

I've written a test method for comparison between two instances of a class (the assumption of type compatibility is given). Proudly I checked all the public properties making sure to return a list of discrepancies.

问题是一些属性是包含它们自己属性的对象(子属性,如果你愿意的话).这些没有被比较,就我通过逐步完成流程所看到的而言.

The problem is that some of the properties are objects containing their own properties (sub-properties, if you will). Those are not being compared, as far I can see by stepping through the process flow.

如何设计一个深入了解并比较所有子属性的调用?如果方法相对简单,则额外奖励.:)

How can I design a call that goes in on-depth and compares all sub-properties? Extra bonus if the approach is relatively simple. :)

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[]流.有了它,您将比较字段(而不是属性),因此事情可能会有所不同(即使它们的私有字段不同,两个对象也可能在逻辑上相等).最大的优点是序列化将处理一个非常棘手的情况:当对象有循环引用时.

As I said in comment easiest way is to use BinaryFormatter to serialize both objects and compare raw byte[] streams. With that you'll compare fields (and not properties) so things may be different (two objects may be compared as logically equal even if their private fields are different). Biggest advantage is that serialization will handle a very tricky case: when objects have circular references.

大概是这样的:

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[] 缓冲区中) 参见他建议的这篇文章.

As pointed out by Ben Voigt in a comment this algorithm to compare streams is pretty slow, for a fast buffer comparison (MemoryStream keeps data in a byte[] buffer) see this post he suggested.

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

If you need more "control" and actually handle custom comparison then you have to make things more complicated. Next sample is first raw (and untested!) version of this comparison. It doesn't handle a very important thing: circular references.

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

如果你去掉注释,你的代码就会很短.要处理循环引用,您必须避免一次又一次地比较相同的元组,为此您必须像本例中那样拆分函数(我知道这是非常非常幼稚的实现):

If you strip out comments you'll have pretty short code. To handle circular references you have to avoid to compare again and again same tuple, to do that you have to split function like in this example (very very naive implementation, I know):

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

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

With core implementation like this (I rewrite only important part):

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
}

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

Next step is little bit more complicate (get the list of different properties) because it may not be such simple (for example if two properties are lists and they have different number of items). I'll just sketch a possible solution (removing code for circular references for clarity). Note that when equality breaks then subsequent checks may also produce unexpected exceptions so it should be implemented much better than this.

新原型将是:

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

而且实现方法也需要跟踪当前路径":

And implementation method will also need to keep track of "current path":

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

这篇关于通过递归遍历所有属性的属性来比较两个对象?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆