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

本文关键字:对象 最佳 方法 复杂 两个 比较 | 更新日期: 2023-09-27 18:33:23

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

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

如何在 C# 4.0 中做到这一点?

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

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

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

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

对于引用类型,您应该首先调用 ReferenceEquals ,它检查引用相等性 - 当您碰巧引用同一对象时,这将提高效率。它还将处理两个引用都为 null 的情况。如果检查失败,请确认实例的字段或属性不为 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 属性,该属性对于不同的现实世界人物是唯一的。具有相同PersonIdAge值不同的两个对象应被视为相等还是不同?上面的答案假设一个人在等价之后。但是,IEquality<T>接口的许多用法(如集合(都假定此类实现提供标识。例如,如果您正在填充一个HashSet<T>,您通常期望TryGetValue(T,T)调用返回仅共享参数标识的现有元素,而不一定是内容完全相同的等效元素。这个概念由GetHashCode上的注释强制执行:

通常,对于可变引用类型,只有在以下情况下才应重写GetHashCode()

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

序列化两个对象并比较生成的字符串

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

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

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

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

  • 比较 .NET 对象

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

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

序列化两个对象并按@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();

现在您可以简单地使用 .等于比较它们。我用它来检查对象是否也在集合中。它工作得很好。

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

你可以做这样的事情:

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

如果你想变得更漂亮,你也许可以为对象创建一个扩展方法。 请注意,这仅比较公共属性。 如果你想在进行比较时忽略公共属性,你可以使用 [JsonIgnore] 属性。

序列化这两个对象,然后计算哈希代码,然后进行比较。

如果你

有一个要求,你想要一个不可变的类。我的意思是,一旦创建任何属性,就无法对其进行修改。在这种情况下,C# 9 具有称为记录的功能。

您可以按值和类型轻松比较记录,因为它们是相等的。

public record Person
{
    public string LastName { get; }
    public string FirstName { get; }
    public Person(string first, string last) => (FirstName, LastName) = (first, last);
}
var person1 = new Person("Bill", "Wagner");
var person2 = new Person("Bill", "Wagner");
Console.WriteLine(person1 == person2); // true

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

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

基于这里已经给出的一些答案,我决定主要支持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.");
我会

这么说:

Object1.Equals(Object2)

将是你要找的。也就是说,如果你想看看物体是否相同,这就是你似乎在问的。

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

我发现下面的函数用于比较对象。

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

我正在使用它,它对我来说工作正常。

感谢乔纳森的例子。我为所有情况(数组、列表、字典、基元类型(扩展了它。

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

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

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

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

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

使用具有

方法Equals IEquatable<T>接口。

泛型扩展方法

public static class GenericExtensions
{
    public static bool DeepCompare<T>(this T objA, T objB)
    {
        if (typeof(T).IsValueType)
            return objA.Equals(objB);
        if (ReferenceEquals(objA, objB))
            return true;
        if ((objA == null) || (objB == null))
            return false;
        if (typeof(T) is IEnumerable)
        {
            var enumerableA = (IEnumerable<T>) objA;
            var enumerableB = (IEnumerable<T>) objB;
            if (enumerableA.Count() != enumerableB.Count())
                return false;
            using (var enumeratorA = enumerableA.GetEnumerator())
            using (var enumeratorB = enumerableB.GetEnumerator())
            {
                while (true)
                {
                    bool moveNextA = enumeratorA.MoveNext();
                    bool moveNextB = enumeratorB.MoveNext();
                    if (!moveNextA || !moveNextB)
                        break;
                    var currentA = enumeratorA.Current;
                    var currentB = enumeratorB.Current;
                    if (!currentA.DeepCompare<T>(currentB))
                        return false;
                }
                return true;
            }
        }
        foreach (var property in objA.GetType().GetProperties())
        {
            var valueA = property.GetValue(objA);
            var valueB = property.GetValue(objB);
            if (!valueA.DeepCompare(valueB))
                return false;
        }
        return true;
    }
}

返回每个属性更新:

    public IEnumerable<string> GetPropsUpdated(T oldModel, T newModel)
    {
        var diff = new List<string>();
        foreach (var prop in oldModel.GetType().GetProperties())
        {
            var oldValue = prop.GetValue(oldModel);
            var newValue = prop.GetValue(newModel);
            if (oldValue == null && newValue == null)
                continue;
            if (oldValue == null && newValue != null 
                || oldValue != null && newValue == null)
            {
                diff.Add(prop.Name);
                continue;
            }
            var oldPropHashed = oldValue.GetHashCode();
            var newPropHashed = newValue.GetHashCode();
            if (!oldPropHashed.Equals(newPropHashed))
                diff.Add(prop.Name);
        }
        return diff;
    }