簡體   English   中英

比較兩個復雜對象的最佳方法

[英]Best way to compare two complex objects

我有兩個復雜的對象,如Object1Object2 他們有大約 5 個級別的子對象。

我需要最快的方法來說明它們是否相同。

這怎么能在 C# 4.0 中完成呢?

在所有自定義類型上實現IEquatable<T> (通常與覆蓋繼承的Object.EqualsObject.GetHashCode方法結合使用)。 對於復合類型,在包含類型中調用包含類型的Equals方法。 對於包含的集合,請使用SequenceEqual擴展方法,該方法在每個元素上內部調用IEquatable<T>.EqualsObject.Equals 這種方法顯然需要您擴展類型的定義,但其結果比任何涉及序列化的通用解決方案都要快。

編輯:這是一個具有三級嵌套的人為示例。

對於值類型,您通常可以只調用它們的Equals方法。 即使從未明確分配字段或屬性,它們仍將具有默認值。

對於引用類型,您應該首先調用ReferenceEquals ,它會檢查引用相等性——當您碰巧引用同一個對象時,這將提高效率。 它還可以處理兩個引用都為空的情況。 如果該檢查失敗,請確認您的實例的字段或屬性不為空(以避免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()

  • 您可以從不可變的字段計算哈希碼; 要么
  • 您可以確保可變對象的哈希碼在對象包含在依賴其哈希碼的集合中時不會更改。

序列化兩個對象並比較結果字符串

您可以使用擴展方法,遞歸來解決此問題:

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,您始終可以使用反射來比較所有屬性: - 如果它們是值類型,只需比較它們 - 如果它們是引用類型,則遞歸調用該函數以比較其“內部”屬性.

我不是在考慮性能,而是在考慮簡單性。 但是,這取決於對象的確切設計。 根據您的對象形狀(例如,如果屬性之間存在循環依賴關系),它可能會變得復雜。 但是,您可以使用多種解決方案,例如:

另一種選擇是將對象序列化為文本,例如使用 JSON.NET,並比較序列化結果。 (JSON.NET 可以處理屬性之間的循環依賴)。

我不知道最快是指實現它的最快方法還是運行速度快的代碼。 在知道是否需要之前,您不應該進行優化。 過早優化是萬惡之源

序列化兩個對象並通過@JoelFan 比較結果字符串

所以要做到這一點,創建一個像這樣的靜態類並使用擴展來擴展所有對象(這樣你就可以將任何類型的對象、集合等傳遞到方法中)

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 來比較它們。 我用它來檢查對象是否也在集合中。 它真的很好用。

序列化兩個對象,然后計算哈希碼,然后進行比較。

我假設你不是指字面上相同的對象

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

您現在可以使用 json.net。 只需繼續使用 Nuget 並安裝它。

你可以做這樣的事情:

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

    return myself == other;
}

如果你想變得更漂亮,你也許可以為對象創建一個擴展方法。 請注意,這僅比較公共屬性。 如果您想在進行比較時忽略公共屬性,您可以使用[JsonIgnore]屬性。

我會這樣說:

Object1.Equals(Object2)

將是你要找的。 那是如果您想查看對象是否相同,這似乎是您要問的。

如果要檢查所有子對象是否相同,請使用Equals()方法通過循環運行它們。

基於這里已經給出的一些答案,我決定主要支持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.");

感謝喬納森的例子。 我針對所有情況(數組、列表、字典、原始類型)擴展了它。

這是一個沒有序列化的比較,不需要為比較對象實現任何接口。

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

用於輕松復制代碼創建的存儲庫

我在下面找到了用於比較對象的函數。

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

我正在使用它,它對我來說很好用。

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

如果你有一個要求,你想要一個不可變的類。 我的意思是,一旦創建,就不能修改任何屬性。 在這種情況下,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

一種方法是在涉及的每個類型上覆蓋Equals() 例如,您的頂級對象將覆蓋Equals()以調用所有 5 個子對象的Equals()方法。 這些對象也應該全部覆蓋Equals() ,假設它們是自定義對象,依此類推,直到可以通過對頂級對象執行相等檢查來比較整個層次結構。

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

我發現這個名為FastDeepCloner的軟件包非常適合我。
您可以在這里找到它: https : //www.nuget.org/packages/FastDeepCloner/

安裝此NuGet程序包后,您可以通過執行以下操作來創建對象副本:

var cloneObject = FastDeepCloner.DeepCloner.Clone(objectToClone);

通用擴展方法

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM