program story

두 개의 복잡한 개체를 비교하는 가장 좋은 방법

inputbox 2020. 8. 20. 08:17
반응형

두 개의 복잡한 개체를 비교하는 가장 좋은 방법


내가 좋아하는이 개 복잡한 객체를 가지고 Object1Object2. 약 5 단계의 하위 개체가 있습니다.

같은지 아닌지 말할 수있는 가장 빠른 방법이 필요합니다.

C # 4.0에서 어떻게이 작업을 수행 할 수 있습니까?


모든 사용자 지정 형식에 대해 구현 IEquatable<T>(일반적으로 상속 된 메서드 Object.EqualsObject.GetHashCode메서드 재정의와 함께 )합니다. 복합 유형의 경우 포함 유형 Equals내에서 포함 된 유형의 메소드를 호출하십시오 . 포함 된 컬렉션의 경우 SequenceEqual내부적으로 IEquatable<T>.Equals또는 Object.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에 고유 한 실제 사람마다 고유 속성 있다고 가정합니다 . PersonId같지만 다른 Age값을 가진 두 개체를 동일하거나 다른 것으로 간주 해야합니까 ? 위의 대답은 하나가 동등성 뒤에 있다고 가정합니다. 그러나 많은 용도가 있습니다.IEquality<T>이러한 구현이 identity를 제공한다고 가정하는 컬렉션과 같은 인터페이스 . 예를 들어를 채우는 경우 HashSet<T>일반적으로 TryGetValue(T,T)호출에서 인수의 ID 만 공유하는 기존 요소를 반환 할 것으로 예상 할 수 있으며 , 콘텐츠가 완전히 동일한 요소가 아닐 수도 있습니다. 이 개념은 다음에 대한 메모에 의해 시행됩니다 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을 사용하여 모든 속성을 비교할 수 있습니다.-값 유형이면 비교하기 만하면됩니다.-참조 유형 인 경우 함수를 재귀 적으로 호출하여 "내부"속성을 비교합니다. .

나는 성능에 대해 생각하지 않고 단순성에 대해 생각합니다. 그러나 개체의 정확한 디자인에 따라 다릅니다. 객체 모양에 따라 복잡해질 수 있습니다 (예 : 속성간에 순환 종속성이있는 경우). 그러나 다음과 같이 사용할 수있는 몇 가지 솔루션이 있습니다.

또 다른 옵션은 객체를 텍스트로 직렬화하는 것입니다. 예를 들어 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();

이제 단순히 .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;
  }
}

나는 물체를 비교하기 위해 아래 기능을 발견했습니다.

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

이미 여기에 제공된 몇 가지 답변을 바탕으로 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()메서드 를 사용하여 루프를 통해 실행하십시오 .


이를 수행하는 한 가지 방법은 Equals()관련된 각 유형 을 재정의 하는 입니다. 예를 들어 최상위 객체는 5 개의 자식 객체 모두 Equals()Equals()메서드 를 호출하도록 재정의 합니다. 이러한 개체는 모두 Equals()사용자 지정 개체라고 가정하고 재정의해야하며 , 최상위 개체에 대해 동등성 검사를 수행하여 전체 계층 구조를 비교할 수있을 때까지 계속해야합니다.


IEquatable<T>메소드가있는 Interface를 사용합니다 Equals.


Jonathan의 예 덕분입니다. 모든 경우 (배열, 목록, 사전, 기본 유형)에 대해 확장했습니다.

이것은 직렬화가없는 비교이며 비교 된 객체에 대한 인터페이스 구현이 필요하지 않습니다.

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

코드 생성 리포지토리 를 쉽게 복사하기 위해

참고 URL : https://stackoverflow.com/questions/10454519/best-way-to-compare-two-complex-objects

반응형