比较两个复杂对象的最佳方法


112

我有两个复杂的对象,例如Object1Object2它们具有大约5个级别的子对象。

我需要最快的方法来说明它们是否相同。

在C#4.0中怎么做?

Answers:


101

在所有自定义类型上实现IEquatable<T>(通常与覆盖继承Object.EqualsObject.GetHashCode方法一起)。对于复合类型,请在包含的类型Equals内调用包含的类型的方法。对于包含的集合,请使用SequenceEqual扩展方法,该方法在内部IEquatable<T>.EqualsObject.Equals在每个元素上调用。显然,这种方法将需要您扩展类型的定义,但其结果比涉及序列化的任何通用解决方案都快。

编辑:这是一个人为设计的示例,具有三个嵌套级别。

对于值类型,通常可以只调用它们的Equals方法。即使从未显式分配字段或属性,它们仍将具有默认值。

对于引用类型,应首先调用ReferenceEquals,以检查引用是否相等–当您碰巧引用同一对象时,这可以提高效率。它还将处理两个引用均为空的情况。如果该检查失败,请确认您实例的字段或属性不为null(以避免NullReferenceException),然后调用其Equals方法。由于我们的成员类型正确,因此IEquatable<T>.Equals将直接调用该方法,而绕过重写的Object.Equals方法(由于强制类型转换,其执行速度会稍慢)。

当您覆盖时Object.Equals,您还应该覆盖Object.GetHashCode; 为了简洁起见,我在下面没有这样做。

public class Person : IEquatable<Person>
{
    public int Age { get; set; }
    public string FirstName { get; set; }
    public Address Address { get; set; }

    public override bool Equals(object obj)
    {
        return this.Equals(obj as Person);
    }

    public bool Equals(Person other)
    {
        if (other == null)
            return false;

        return this.Age.Equals(other.Age) &&
            (
                object.ReferenceEquals(this.FirstName, other.FirstName) ||
                this.FirstName != null &&
                this.FirstName.Equals(other.FirstName)
            ) &&
            (
                object.ReferenceEquals(this.Address, other.Address) ||
                this.Address != null &&
                this.Address.Equals(other.Address)
            );
    }
}

public class Address : IEquatable<Address>
{
    public int HouseNo { get; set; }
    public string Street { get; set; }
    public City City { get; set; }

    public override bool Equals(object obj)
    {
        return this.Equals(obj as Address);
    }

    public bool Equals(Address other)
    {
        if (other == null)
            return false;

        return this.HouseNo.Equals(other.HouseNo) &&
            (
                object.ReferenceEquals(this.Street, other.Street) ||
                this.Street != null &&
                this.Street.Equals(other.Street)
            ) &&
            (
                object.ReferenceEquals(this.City, other.City) ||
                this.City != null &&
                this.City.Equals(other.City)
            );
    }
}

public class City : IEquatable<City>
{
    public string Name { get; set; }

    public override bool Equals(object obj)
    {
        return this.Equals(obj as City);
    }

    public bool Equals(City other)
    {
        if (other == null)
            return false;

        return
            object.ReferenceEquals(this.Name, other.Name) ||
            this.Name != null &&
            this.Name.Equals(other.Name);
    }
}

更新:这个答案是几年前写的。从那时起,我就开始不再IEquality<T>为此类情况实现可变类型的实现了。平等有两种概念:同一性等价性。在内存表示级别上,通常将它们区分为“引用相等”和“值相等”(请参阅​​“ 相等比较”)。但是,相同的区别也可以在域级别应用。假设您的Person班级有一个PersonId属性,该属性对于每个实际的人都是唯一的。具有相同PersonId但不同Age值的两个对象是否应视为相等或不同?上面的答案假设一个在等价之后。但是,IEquality<T>接口(例如集合),它们假定此类实现提供了identity。例如,如果要填充,则HashSet<T>通常希望TryGetValue(T,T)调用返回仅共享参数标识的现有元素,而不必返回内容完全相同的等效元素。此概念由以下注释强制执行GetHashCode

通常,对于可变引用类型,GetHashCode()仅在以下情况下才应覆盖:

  • 您可以从不可变的字段中计算哈希码;要么
  • 您可以确保在对象包含在依赖于其哈希代码的集合中时,该可变对象的哈希代码不会更改。

我可以通过RIA Services获得此对象...我可以对那些对象使用IEquatable <Foo>并在WPF客户端下获取它吗?
开发人员

1
您是说这些类是自动生成的?我没有使用RIA Services,但是我假设任何生成的类都将声明为partial–在这种情况下,可以的,您可以Equals通过手动添加的部分类声明来实现其方法,该声明引用自动生成的字段/属性之一。
道格拉斯

如果“地址地址”实际上是“地址[]地址”怎么办?
guiomie 2012年

2
您可以Enumerable.SequenceEqual在数组上调用LINQ 方法:this.Addresses.SequenceEqual(other.Addresses)Address.Equals假设Address该类实现了IEquatable<Address>接口,这将在内部为每对对应的地址调用您的方法。
道格拉斯

2
开发人员可以检查的另一种比较类别是“ WorksLike”。对我来说,这意味着即使两个实例可能具有不相等的属性值,该程序也会通过处理两个实例产生相同的结果。
约翰·库兹

95

序列化两个对象并比较结果字符串


1
我不知道为什么会这样。序列化通常是一个优化的过程,无论如何您都需要访问每个属性的值。
JoelFan

5
有很大的代价。您正在生成数据流,追加字符串,然后测试字符串相等性。数量级,就在那。更不用说序列化将默认使用反射。
杰罗姆·哈尔托姆

2
数据流没什么大不了的,我不明白为什么您需要附加字符串...测试字符串相等性是目前最优化的操作之一....您可能会有所反思...但是整个序列化不会比其他方法差“数量级”。如果您怀疑性能问题,则应进行基准测试...我没有遇到这种方法的性能问题
JoelFan

12
+1之所以这么简单,是因为我从未想过以这种方式进行基于值的平等比较。很简单。可以看到一些与此代码相对应的基准。
托马斯

1
这不是一个好的解决方案,因为两个序列化都可能以类似的方式出错。例如,源对象的某些属性可能尚未序列化,反序列化时将在目标对象中将其设置为null。在这种情况下,比较字符串的测试将通过,但实际上两个对象都不相同!
stackMeUp19年

35

您可以使用扩展方法,递归来解决此问题:

public static bool DeepCompare(this object obj, object another)
{     
  if (ReferenceEquals(obj, another)) return true;
  if ((obj == null) || (another == null)) return false;
  //Compare two object's class, return false if they are difference
  if (obj.GetType() != another.GetType()) return false;

  var result = true;
  //Get all properties of obj
  //And compare each other
  foreach (var property in obj.GetType().GetProperties())
  {
      var objValue = property.GetValue(obj);
      var anotherValue = property.GetValue(another);
      if (!objValue.Equals(anotherValue)) result = false;
  }

  return result;
 }

public static bool CompareEx(this object obj, object another)
{
 if (ReferenceEquals(obj, another)) return true;
 if ((obj == null) || (another == null)) return false;
 if (obj.GetType() != another.GetType()) return false;

 //properties: int, double, DateTime, etc, not class
 if (!obj.GetType().IsClass) return obj.Equals(another);

 var result = true;
 foreach (var property in obj.GetType().GetProperties())
 {
    var objValue = property.GetValue(obj);
    var anotherValue = property.GetValue(another);
    //Recursion
    if (!objValue.DeepCompare(anotherValue))   result = false;
 }
 return result;
}

或通过使用Json进行比较(如果对象非常复杂),可以使用Newtonsoft.Json:

public static bool JsonCompare(this object obj, object another)
{
  if (ReferenceEquals(obj, another)) return true;
  if ((obj == null) || (another == null)) return false;
  if (obj.GetType() != another.GetType()) return false;

  var objJson = JsonConvert.SerializeObject(obj);
  var anotherJson = JsonConvert.SerializeObject(another);

  return objJson == anotherJson;
}

1
第一个解决方案很棒!我喜欢您不必json序列化或实现向对象本身添加任何代码。仅在比较单元测试时适用。我可能建议添加一个简单的比较,以防objValue和anotherValue都等于null?Equals()//如果(objValue == anotherValue)继续执行RedundantJumpStatement,则ReSharper禁用一次;这将避免在尝试执行null时引发NullReferenceException。//空引用保护,否则if(!objValue.Equals(anotherValue))失败(预期,实际);
马克·康威

3
有什么理由要使用,DeepCompare而不是简单地CompareEx递归调用?
–'apostolov

3
这可能会不必要地比较整个结构。替换resultreturn false会使其更有效率。
蒂姆·西尔维斯特

24

如果您不想实现IEquatable,则始终可以使用Reflection来比较所有属性:-如果它们是值类型,则只比较它们-如果它们是引用类型,则递归调用函数以比较其“内部”属性。

我不是在考虑性能,而是在考虑简单性。但是,这取决于对象的确切设计。根据对象的形状,它可能会变得复杂(例如,如果属性之间存在循环依赖性)。但是,您可以使用多种解决方案,例如:

另一个选择是将对象序列化为文本,例如使用JSON.NET,并比较序列化结果。(JSON.NET可以处理属性之间的循环依赖关系)。

我不知道您所说的最快是实现它的最快方法还是运行得快的代码。在知道是否需要优化之前,不应该进行优化。过早的优化是万恶之源


1
我几乎不认为IEquatable<T>实现有资格被视为过早优化的情况。反射将大大减慢。Equals自定义值类型的默认实现确实使用反射;Microsoft本身建议对其进行重写Equals以提高性能:“ 为特定类型重写方法以改善该方法的性能,并更紧密地代表该类型的相等性概念。”
道格拉斯

1
这取决于他要运行equals方法多少次:1、10、100、100,一百万?那将会有很大的不同。如果他可以使用通用解决方案而不执行任何措施,那么他将节省一些宝贵的时间。如果速度太慢,那么是时候实现IEquatable了(甚至可能尝试制作可缓存的或智能的GetHashCode)就反射的速度而言,我必须同意它的速度较慢...或慢得多,具体取决于您的操作方式(即重用PropertyInfos类型等等。
JotaBe'5

@ Worthy7确实如此。请参阅项目的内容。测试是记录示例的好方法。但是,比这更好的是,如果您寻找它,则会找到.chm帮助文件。因此,与大多数项目相比,该项目的文档要好得多。
JotaBe

抱歉,您是对的,我完全错过了“维基”标签。我已经习惯了每个人都在自述文件中写东西。
Worthy7年7

9

序列化两个对象并通过@JoelFan比较结果字符串

为此,请创建一个像这样的静态类,并使用Extensions扩展所有对象(以便您可以将任何类型的对象,集合等传递到方法中)

using System;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;

public static class MySerializer
{
    public static string Serialize(this object obj)
    {
        var serializer = new DataContractJsonSerializer(obj.GetType());
        using (var ms = new MemoryStream())
        {
            serializer.WriteObject(ms, obj);
            return Encoding.Default.GetString(ms.ToArray());
        }
    }
}

在任何其他文件中引用此静态类后,即可执行以下操作:

Person p = new Person { Firstname = "Jason", LastName = "Argonauts" };
Person p2 = new Person { Firstname = "Jason", LastName = "Argonaut" };
//assuming you have already created a class person!
string personString = p.Serialize();
string person2String = p2.Serialize();

现在,您可以简单地使用.Equals进行比较。我用它来检查对象是否也在集合中。它真的很好。


如果对象的内容是浮点数数组怎么办?将这些字符串转换为字符串效率非常低,并且该转换需要进行中定义的转换CultrureInfo。仅当内部数据主要是字符串和整数时,这才起作用。否则将是一场灾难。
John Alexiou

3
如果新任总监告诉您淘汰C#并将其替换为Python怎么办。作为开发人员,我们需要学习如果问题必须在某个地方停止该怎么办。解决问题,继续下一个。如果您有时间,请回到...
ozzy432836 '16

2
Python在语法和用法上更像MATLAB。从静态类型安全的语言过渡到像python这样的Mishmash脚本必须有一个非常好的理由。
John Alexiou

5

我假设您不是指字面上相同的对象

Object1 == Object2

您可能正在考虑对两者进行内存比较

memcmp(Object1, Object2, sizeof(Object.GetType())

但这甚至不是c#中的真实代码:)。因为所有数据可能都是在堆上创建的,所以内存不是连续的,并且您不能仅以不可知论的方式比较两个对象的相等性。您将必须以自定义方式一次比较每个值。

考虑将IEquatable<T>接口添加到您的类,并Equals为您的类型定义一个自定义方法。然后,使用该方法,手动测试每个值。IEquatable<T>如果可以,请再次添加封闭的类型,然后重复该过程。

class Foo : IEquatable<Foo>
{
  public bool Equals(Foo other)
  {
    /* check all the values */
    return false;
  }
}


3

我发现此功能用于比较对象。

 static bool Compare<T>(T Object1, T object2)
 {
      //Get the type of the object
      Type type = typeof(T);

      //return false if any of the object is false
      if (object.Equals(Object1, default(T)) || object.Equals(object2, default(T)))
         return false;

     //Loop through each properties inside class and get values for the property from both the objects and compare
     foreach (System.Reflection.PropertyInfo property in type.GetProperties())
     {
          if (property.Name != "ExtensionData")
          {
              string Object1Value = string.Empty;
              string Object2Value = string.Empty;
              if (type.GetProperty(property.Name).GetValue(Object1, null) != null)
                    Object1Value = type.GetProperty(property.Name).GetValue(Object1, null).ToString();
              if (type.GetProperty(property.Name).GetValue(object2, null) != null)
                    Object2Value = type.GetProperty(property.Name).GetValue(object2, null).ToString();
              if (Object1Value.Trim() != Object2Value.Trim())
              {
                  return false;
              }
          }
     }
     return true;
 }

我正在使用它,对我来说效果很好。


1
第一个if意味着那Compare(null, null) == false不是我期望的。
蒂姆·西尔维斯特

3

根据此处已经给出的一些答案,我决定主要支持JoelFan的答案。我喜欢扩展方法,当其他解决方案都无法使用它们来比较我的复杂类时,这些方法对我非常有用。

扩展方法

using System.IO;
using System.Xml.Serialization;

static class ObjectHelpers
{
    public static string SerializeObject<T>(this T toSerialize)
    {
        XmlSerializer xmlSerializer = new XmlSerializer(toSerialize.GetType());

        using (StringWriter textWriter = new StringWriter())
        {
            xmlSerializer.Serialize(textWriter, toSerialize);
            return textWriter.ToString();
        }
    }

    public static bool EqualTo(this object obj, object toCompare)
    {
        if (obj.SerializeObject() == toCompare.SerializeObject())
            return true;
        else
            return false;
    }

    public static bool IsBlank<T>(this T obj) where T: new()
    {
        T blank = new T();
        T newObj = ((T)obj);

        if (newObj.SerializeObject() == blank.SerializeObject())
            return true;
        else
            return false;
    }

}

使用范例

if (record.IsBlank())
    throw new Exception("Record found is blank.");

if (record.EqualTo(new record()))
    throw new Exception("Record found is blank.");

2

我会这样说:

Object1.Equals(Object2)

就是您要找的东西。那就是如果您要查看对象是否相同,这就是您似乎要问的问题。

如果要检查所有子对象是否相同,请使用Equals()方法循环运行它们。


2
当且仅当它们提供非相等的Equals重载。
user7116 2012年

每个类必须实现自己的比较方式。如果作者的类没有Equals()方法的重写,则它们将使用System.Object()类的基本方法,这将导致逻辑​​错误。
迪马2012年

2
public class GetObjectsComparison
{
    public object FirstObject, SecondObject;
    public BindingFlags BindingFlagsConditions= BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
}
public struct SetObjectsComparison
{
    public FieldInfo SecondObjectFieldInfo;
    public dynamic FirstObjectFieldInfoValue, SecondObjectFieldInfoValue;
    public bool ErrorFound;
    public GetObjectsComparison GetObjectsComparison;
}
private static bool ObjectsComparison(GetObjectsComparison GetObjectsComparison)
{
    GetObjectsComparison FunctionGet = GetObjectsComparison;
    SetObjectsComparison FunctionSet = new SetObjectsComparison();
    if (FunctionSet.ErrorFound==false)
        foreach (FieldInfo FirstObjectFieldInfo in FunctionGet.FirstObject.GetType().GetFields(FunctionGet.BindingFlagsConditions))
        {
            FunctionSet.SecondObjectFieldInfo =
            FunctionGet.SecondObject.GetType().GetField(FirstObjectFieldInfo.Name, FunctionGet.BindingFlagsConditions);

            FunctionSet.FirstObjectFieldInfoValue = FirstObjectFieldInfo.GetValue(FunctionGet.FirstObject);
            FunctionSet.SecondObjectFieldInfoValue = FunctionSet.SecondObjectFieldInfo.GetValue(FunctionGet.SecondObject);
            if (FirstObjectFieldInfo.FieldType.IsNested)
            {
                FunctionSet.GetObjectsComparison =
                new GetObjectsComparison()
                {
                    FirstObject = FunctionSet.FirstObjectFieldInfoValue
                    ,
                    SecondObject = FunctionSet.SecondObjectFieldInfoValue
                };

                if (!ObjectsComparison(FunctionSet.GetObjectsComparison))
                {
                    FunctionSet.ErrorFound = true;
                    break;
                }
            }
            else if (FunctionSet.FirstObjectFieldInfoValue != FunctionSet.SecondObjectFieldInfoValue)
            {
                FunctionSet.ErrorFound = true;
                break;
            }
        }
    return !FunctionSet.ErrorFound;
}

使用递归原理。
matan justme

1

一种方法是Equals()在涉及的每种类型上重写。例如,您的顶级对象将被覆盖Equals()以调用Equals()所有5个子对象的方法。Equals()假定它们是自定义对象,这些对象也应全部重写,依此类推,直到可以通过仅对顶级对象执行相等检查来比较整个层次结构。


1

使用IEquatable<T>具有方法的接口Equals


1

感谢乔纳森的榜样。我针对所有情况(数组,列表,字典,原始类型)进行了扩展。

这是没有序列化的比较,不需要为比较对象实现任何接口。

        /// <summary>Returns description of difference or empty value if equal</summary>
        public static string Compare(object obj1, object obj2, string path = "")
        {
            string path1 = string.IsNullOrEmpty(path) ? "" : path + ": ";
            if (obj1 == null && obj2 != null)
                return path1 + "null != not null";
            else if (obj2 == null && obj1 != null)
                return path1 + "not null != null";
            else if (obj1 == null && obj2 == null)
                return null;

            if (!obj1.GetType().Equals(obj2.GetType()))
                return "different types: " + obj1.GetType() + " and " + obj2.GetType();

            Type type = obj1.GetType();
            if (path == "")
                path = type.Name;

            if (type.IsPrimitive || typeof(string).Equals(type))
            {
                if (!obj1.Equals(obj2))
                    return path1 + "'" + obj1 + "' != '" + obj2 + "'";
                return null;
            }
            if (type.IsArray)
            {
                Array first = obj1 as Array;
                Array second = obj2 as Array;
                if (first.Length != second.Length)
                    return path1 + "array size differs (" + first.Length + " vs " + second.Length + ")";

                var en = first.GetEnumerator();
                int i = 0;
                while (en.MoveNext())
                {
                    string res = Compare(en.Current, second.GetValue(i), path);
                    if (res != null)
                        return res + " (Index " + i + ")";
                    i++;
                }
            }
            else if (typeof(System.Collections.IEnumerable).IsAssignableFrom(type))
            {
                System.Collections.IEnumerable first = obj1 as System.Collections.IEnumerable;
                System.Collections.IEnumerable second = obj2 as System.Collections.IEnumerable;

                var en = first.GetEnumerator();
                var en2 = second.GetEnumerator();
                int i = 0;
                while (en.MoveNext())
                {
                    if (!en2.MoveNext())
                        return path + ": enumerable size differs";

                    string res = Compare(en.Current, en2.Current, path);
                    if (res != null)
                        return res + " (Index " + i + ")";
                    i++;
                }
            }
            else
            {
                foreach (PropertyInfo pi in type.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public))
                {
                    try
                    {
                        var val = pi.GetValue(obj1);
                        var tval = pi.GetValue(obj2);
                        if (path.EndsWith("." + pi.Name))
                            return null;
                        var pathNew = (path.Length == 0 ? "" : path + ".") + pi.Name;
                        string res = Compare(val, tval, pathNew);
                        if (res != null)
                            return res;
                    }
                    catch (TargetParameterCountException)
                    {
                        //index property
                    }
                }
                foreach (FieldInfo fi in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public))
                {
                    var val = fi.GetValue(obj1);
                    var tval = fi.GetValue(obj2);
                    if (path.EndsWith("." + fi.Name))
                        return null;
                    var pathNew = (path.Length == 0 ? "" : path + ".") + fi.Name;
                    string res = Compare(val, tval, pathNew);
                    if (res != null)
                        return res;
                }
            }
            return null;
        }

为了轻松复制创建的代码存储库


1

您现在可以使用json.net。只需继续安装Nuget即可。

您可以执行以下操作:

    public bool Equals(SamplesItem sampleToCompare)
    {
        string myself = JsonConvert.SerializeObject(this);
        string other = JsonConvert.SerializeObject(sampleToCompare);

        return myself == other;
    }

如果您想变得更奇特,可以为对象创建扩展方法。请注意,这仅比较公共财产。并且,如果您想在进行比较时忽略公共属性,则可以使用[JsonIgnore]属性。


如果您的对象中有列表,而那些对象中有列表,则试图遍历这两个对象将是一场噩梦。如果将两者序列化然后进行比较,则无需处理这种情况。
ashlar64

如果您的复杂对象中包含字典,那么我不相信.net序列化程序可以序列化它。Json序列化器可以。
ashlar64
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.