Porównanie właściwości obiektu w języku C # [zamknięte]

111

Oto, co wymyśliłem jako metodę w klasie dziedziczonej przez wiele innych moich klas. Pomysł polega na tym, że pozwala na proste porównanie między właściwościami obiektów tego samego typu.

Teraz to działa - ale w interesie poprawy jakości mojego kodu pomyślałem, że wyrzucę go do sprawdzenia. Jak to może być lepsze / bardziej wydajne / itp.?

/// <summary>
/// Compare property values (as strings)
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public bool PropertiesEqual(object comparisonObject)
{

    Type sourceType = this.GetType();
    Type destinationType = comparisonObject.GetType();

    if (sourceType == destinationType)
    {
        PropertyInfo[] sourceProperties = sourceType.GetProperties();
        foreach (PropertyInfo pi in sourceProperties)
        {
            if ((sourceType.GetProperty(pi.Name).GetValue(this, null) == null && destinationType.GetProperty(pi.Name).GetValue(comparisonObject, null) == null))
            {
                // if both are null, don't try to compare  (throws exception)
            }
            else if (!(sourceType.GetProperty(pi.Name).GetValue(this, null).ToString() == destinationType.GetProperty(pi.Name).GetValue(comparisonObject, null).ToString()))
            {
                // only need one property to be different to fail Equals.
                return false;
            }
        }
    }
    else
    {
        throw new ArgumentException("Comparison object must be of the same type.","comparisonObject");
    }

    return true;
}
Nailitdown
źródło
3
Przy okazji, czy wiesz o tej witrynie SE: codereview.stackexchange.com
wip
Istnieje kilka bibliotek porównujących obiekty: CompareNETObjects , DeepEqual , AutoCompare , ZCompare ...
nawfal
... i mnóstwo generycznych implementatorów porównujących równość, z których niektóre to: MemberwiseEqualityComparer , Equ , SemanticComparison , EqualityComparer , DeepEqualityComparer , Equality , Equals.Fody . Ta ostatnia grupa może mieć ograniczony zakres i elastyczność co do tego, co mogą osiągnąć.
nawfal
Głosuję za zamknięciem tego pytania jako
niezwiązanego

Odpowiedzi:

160

Szukałem fragmentu kodu, który zrobiłby coś podobnego, aby pomóc w pisaniu testów jednostkowych. Oto, czego ostatecznie użyłem.

public static bool PublicInstancePropertiesEqual<T>(T self, T to, params string[] ignore) where T : class 
  {
     if (self != null && to != null)
     {
        Type type = typeof(T);
        List<string> ignoreList = new List<string>(ignore);
        foreach (System.Reflection.PropertyInfo pi in type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
        {
           if (!ignoreList.Contains(pi.Name))
           {
              object selfValue = type.GetProperty(pi.Name).GetValue(self, null);
              object toValue = type.GetProperty(pi.Name).GetValue(to, null);

              if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue)))
              {
                 return false;
              }
           }
        }
        return true;
     }
     return self == to;
  }

EDYTOWAĆ:

Ten sam kod jak powyżej, ale używa metod LINQ i Extension:

public static bool PublicInstancePropertiesEqual<T>(this T self, T to, params string[] ignore) where T : class
{
    if (self != null && to != null)
    {
        var type = typeof(T);
        var ignoreList = new List<string>(ignore);
        var unequalProperties =
            from pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
            where !ignoreList.Contains(pi.Name) && pi.GetUnderlyingType().IsSimpleType() && pi.GetIndexParameters().Length == 0
            let selfValue = type.GetProperty(pi.Name).GetValue(self, null)
            let toValue = type.GetProperty(pi.Name).GetValue(to, null)
            where selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue))
            select selfValue;
        return !unequalProperties.Any();
    }
    return self == to;
}

public static class TypeExtensions
   {
      /// <summary>
      /// Determine whether a type is simple (String, Decimal, DateTime, etc) 
      /// or complex (i.e. custom class with public properties and methods).
      /// </summary>
      /// <see cref="http://stackoverflow.com/questions/2442534/how-to-test-if-type-is-primitive"/>
      public static bool IsSimpleType(
         this Type type)
      {
         return
            type.IsValueType ||
            type.IsPrimitive ||
            new[]
            {
               typeof(String),
               typeof(Decimal),
               typeof(DateTime),
               typeof(DateTimeOffset),
               typeof(TimeSpan),
               typeof(Guid)
            }.Contains(type) ||
            (Convert.GetTypeCode(type) != TypeCode.Object);
      }

      public static Type GetUnderlyingType(this MemberInfo member)
      {
         switch (member.MemberType)
         {
            case MemberTypes.Event:
               return ((EventInfo)member).EventHandlerType;
            case MemberTypes.Field:
               return ((FieldInfo)member).FieldType;
            case MemberTypes.Method:
               return ((MethodInfo)member).ReturnType;
            case MemberTypes.Property:
               return ((PropertyInfo)member).PropertyType;
            default:
               throw new ArgumentException
               (
                  "Input MemberInfo must be if type EventInfo, FieldInfo, MethodInfo, or PropertyInfo"
               );
         }
      }
   }
Taras Alenin
źródło
Big T - dość stary, ale z pewnością służy
świetnemu
1
To dobrze, ale stwierdziłem, że nie działa z bardziej złożonymi obiektami. Na przykład mam obiekt z kilkoma napisami (porównuje je dobrze), ale ten obiekt ma również listę innego obiektu, którego nie porównuje poprawnie, więc muszę to jakoś powtórzyć.
Ryan Thomas
1
Musiałem dodać do kryteriów w pierwszym, gdzie dwa dodatkowe kryteria, ponieważ musisz wykluczyć indeksowane właściwości, które zgłaszają wyjątek w innym przypadku. Oto kryteria tego błędu: pi.GetIndexParameters (). Length == 0. Drugie kryterium rozwiązania problemu podanego przez @RyanThomas to: pi.GetUnderlyingType (). IsSimpleType (). Jak zobaczysz, IsSimpleType jest rozszerzeniem, które nie istnieje dla klasy Type. Zmodyfikowałem odpowiedź, dodając wszystkie te warunki i rozszerzenie.
Samuel
64

AKTUALIZACJA: Najnowsza wersja Compare-Net-Objects znajduje się w serwisie GitHub , zawiera pakiet NuGet i samouczek . Można to nazwać jak

//This is the comparison class
CompareLogic compareLogic = new CompareLogic();

ComparisonResult result = compareLogic.Compare(person1, person2);

//These will be different, write out the differences
if (!result.AreEqual)
    Console.WriteLine(result.DifferencesString);

Lub jeśli chcesz zmienić jakąś konfigurację, użyj

CompareLogic basicComparison = new CompareLogic() 
{ Config = new ComparisonConfig()
   { MaxDifferences = propertyCount 
     //add other configurations
   }
};

Pełna lista konfigurowalnych parametrów znajduje się w pliku CompareConfig.cs

Oryginalna odpowiedź:

Ograniczenia, które widzę w Twoim kodzie:

  • Największą z nich jest to, że nie wykonuje głębokiego porównania obiektów.

  • Nie wykonuje porównania elementu po elemencie w przypadku, gdy właściwości są listami lub zawierają listy jako elementy (może to mieć n-poziom).

  • Nie bierze pod uwagę, że nie należy porównywać niektórych typów właściwości (np. Właściwość Func używana do celów filtrowania, jak ta w klasie PagedCollectionView).

  • Nie śledzi, jakie właściwości faktycznie były różne (więc możesz pokazać w swoich asercjach).

Szukałem dzisiaj jakiegoś rozwiązania do celów testów jednostkowych, aby przeprowadzić głębokie porównanie właściwości według właściwości i ostatecznie skorzystałem z: http://comparenetobjects.codeplex.com .

Jest to darmowa biblioteka z tylko jedną klasą, której możesz po prostu użyć w następujący sposób:

var compareObjects = new CompareObjects()
{
    CompareChildren = true, //this turns deep compare one, otherwise it's shallow
    CompareFields = false,
    CompareReadOnly = true,
    ComparePrivateFields = false,
    ComparePrivateProperties = false,
    CompareProperties = true,
    MaxDifferences = 1,
    ElementsToIgnore = new List<string>() { "Filter" }
};

Assert.IsTrue(
    compareObjects.Compare(objectA, objectB), 
    compareObjects.DifferencesString
);

Można go również łatwo ponownie skompilować dla Silverlight. Po prostu skopiuj jedną klasę do projektu Silverlight i usuń jeden lub dwa wiersze kodu dla porównań, które nie są dostępne w Silverlight, na przykład porównania prywatnych członków.

Liviu Trifoi
źródło
2
Liviu, zauważyłem twój komentarz dotyczący niezgodności klasy z Silverlight. Właśnie zmieniłem go, aby był zgodny z Silverlight i Windows Phone 7. Pobierz najnowsze. Zobacz zestaw zmian 74131
Greg Finzer
To wygląda obiecująco. Spróbuję
DJ Burb,
Dziękuję za wspaniały przykład! Ponadto IgnoreObjectTypesustawienie może być przydatne, gdy istnieją różne typy.
Sergey Brunov
Wersja 2.0 ma wersję Portable Class Library zgodną z Silverlight 5+, Windows Phone 8+, WinRT 8+, Xamarin IOS i Xamarin Droid
Greg Finzer
DifferencesStringzostał usunięty w klasie CompareObjects. Ale teraz możesz to uzyskać z var r = compareObjects.Compare(objectA, objectB); Assert.IsTrue(r.AreEqual, r.DifferencesString);
CompareResult
6

Myślę, że najlepiej byłoby postępować zgodnie ze wzorem dla Override Object # Equals ()
Aby uzyskać lepszy opis: Przeczytaj efektywny C # Billa Wagnera - Myślę, że punkt 9

public override Equals(object obOther)
{
  if (null == obOther)
    return false;
  if (object.ReferenceEquals(this, obOther)
    return true;
  if (this.GetType() != obOther.GetType())
    return false;
  # private method to compare members.
  return CompareMembers(this, obOther as ThisClass);
}
  • Również w metodach sprawdzających równość należy zwrócić wartość true lub false. albo są równe, albo nie są ... zamiast rzucać wyjątek, zwraca fałsz.
  • Rozważyłbym zastąpienie Object # Equals.
  • Mimo że musieliście to wziąć pod uwagę, używanie Reflection do porównywania właściwości jest rzekomo powolne (nie mam liczb, które to potwierdzają). Jest to domyślne zachowanie dla valueType # Equals w C # i zaleca się zastąpienie Equals dla typów wartości i wykonanie mądrego porównania elementów członkowskich pod kątem wydajności. (Wcześniej czytałem to szybko, ponieważ masz kolekcję niestandardowych obiektów właściwości ... moja wina).

Aktualizacja - grudzień 2011:

  • Oczywiście, jeśli typ ma już produkcję Equals (), potrzebujesz innego podejścia.
  • Jeśli używasz tego do porównywania niezmiennych struktur danych wyłącznie do celów testowych, nie powinieneś dodawać Equals do klas produkcyjnych (ktoś może zawęzić testy, kierując implementacją Equals lub możesz zapobiec utworzeniu implementacji Equals wymaganej do produkcji) .
Gishu
źródło
Wystąpiły problemy z nadpisywaniem .Równoważności (), ponieważ próbuję zaimplementować to w klasie bazowej, która jest dziedziczona ... ponieważ nie znam kluczy dla klasy, przeciwko której zostanie uruchomiony, nie mogę zaimplementuj przyzwoite przesłonięcie dla GetHasCode () (wymagane, gdy zastąpisz Equals ()).
nailitdown
Wymóg jest taki, że jeśli objA.Equals (objB) to objA.GetHashCode () == objB.GetHashCode (). GetHashCode nie powinien być zależny od zmiennego stanu / danych klasy ... Nie rozumiem, co masz na myśli, mówiąc o kluczach dla tej klasy. Wygląda na to, że można to rozwiązać. Czy typ podstawowy nie ma „kluczy”?
Gishu,
6

Jeśli wydajność nie ma znaczenia, możesz je serializować i porównać wyniki:

var serializer = new XmlSerializer(typeof(TheObjectType));
StringWriter serialized1 = new StringWriter(), serialized2 = new StringWriter();
serializer.Serialize(serialized1, obj1);
serializer.Serialize(serialized2, obj2);
bool areEqual = serialized1.ToString() == serialized2.ToString();
Edward Brey
źródło
4
próbowałem tego chwilę temu, zastanawiałbyś się, ile obiektów nie można serializować ...
Offler
5

Myślę, że odpowiedź Big T była całkiem dobra, ale brakowało głębokiego porównania, więc trochę go poprawiłem:

using System.Collections.Generic;
using System.Reflection;

/// <summary>Comparison class.</summary>
public static class Compare
{
    /// <summary>Compare the public instance properties. Uses deep comparison.</summary>
    /// <param name="self">The reference object.</param>
    /// <param name="to">The object to compare.</param>
    /// <param name="ignore">Ignore property with name.</param>
    /// <typeparam name="T">Type of objects.</typeparam>
    /// <returns><see cref="bool">True</see> if both objects are equal, else <see cref="bool">false</see>.</returns>
    public static bool PublicInstancePropertiesEqual<T>(T self, T to, params string[] ignore) where T : class
    {
        if (self != null && to != null)
        {
            var type = self.GetType();
            var ignoreList = new List<string>(ignore);
            foreach (var pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                if (ignoreList.Contains(pi.Name))
                {
                    continue;
                }

                var selfValue = type.GetProperty(pi.Name).GetValue(self, null);
                var toValue = type.GetProperty(pi.Name).GetValue(to, null);

                if (pi.PropertyType.IsClass && !pi.PropertyType.Module.ScopeName.Equals("CommonLanguageRuntimeLibrary"))
                {
                    // Check of "CommonLanguageRuntimeLibrary" is needed because string is also a class
                    if (PublicInstancePropertiesEqual(selfValue, toValue, ignore))
                    {
                        continue;
                    }

                    return false;
                }

                if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue)))
                {
                    return false;
                }
            }

            return true;
        }

        return self == to;
    }
}
Greg
źródło
4

Dodałbym następujący wiersz do metody PublicInstancePropertiesEqual, aby uniknąć błędów kopiowania i wklejania:

Assert.AreNotSame(self, to);
thanei
źródło
2

Czy nadpisujesz .ToString () na wszystkich obiektach, które są we właściwościach? W przeciwnym razie to drugie porównanie mogłoby zwrócić wartość null.

Również w tym drugim porównaniu jestem na granicy co do konstrukcji! (A == B) w porównaniu z (A! = B), pod względem czytelności za sześć miesięcy / dwa lata od teraz. Sama linia jest dość szeroka, co jest w porządku, jeśli masz szeroki monitor, ale może nie drukować się dobrze. (czubek)

Czy wszystkie obiekty zawsze używają takich właściwości, że ten kod będzie działał? Czy mogą istnieć jakieś wewnętrzne, niezastrzeżone dane, które mogą się różnić w zależności od obiektu, ale wszystkie ujawnione dane są takie same? Myślę o niektórych danych, które mogą się zmieniać w czasie, na przykład dwa generatory liczb losowych, które zdarzają się trafiać w tę samą liczbę w jednym punkcie, ale zamierzają wygenerować dwie różne sekwencje informacji lub po prostu dane, które nie zostaną ujawnione poprzez interfejs właściwości.

mmr
źródło
dobre punkty -! = ... zgodził się, punkt przyjęty. ToString () było próbą obejścia .GetValue zwracającego obiekt (stąd porównanie zawsze jest fałszywe, ponieważ jest to porównanie referencyjne). Czy jest lepszy sposób?
nailitdown
Jeśli GetValue zwraca obiekt, czy możesz powtórzyć tę funkcję ponownie? tj. wywołać PropertiesEqual na zwróconych obiektach?
mmr
1

Jeśli porównujesz tylko obiekty tego samego typu lub dalej w łańcuchu dziedziczenia, dlaczego nie określić parametru jako typu podstawowego, a nie obiektu?

Wykonaj również testy zerowe parametru.

Ponadto użyłbym `` var '' tylko po to, aby kod był bardziej czytelny (jeśli jest to kod C # 3)

Ponadto, jeśli obiekt ma typy referencyjne jako właściwości, to po prostu wywołujesz na nich ToString (), co tak naprawdę nie porównuje wartości. Jeśli ToString nie zostanie nadpisane, po prostu zwróci nazwę typu jako ciąg, który może zwrócić fałszywie dodatnie wartości.

DarkwingDuck
źródło
dobra uwaga na temat typów referencyjnych - w moim przypadku to nie ma znaczenia, ale jest duża szansa, że ​​tak.
nailitdown
1

Pierwszą rzeczą, którą zasugerowałbym, byłoby podzielenie rzeczywistego porównania, aby było nieco bardziej czytelne (wyjąłem również ToString () - czy to jest potrzebne?):

else {
    object originalProperty = sourceType.GetProperty(pi.Name).GetValue(this, null);
    object comparisonProperty = destinationType.GetProperty(pi.Name).GetValue(comparisonObject, null);

    if (originalProperty != comparisonProperty)
        return false;

Następną sugestią byłoby maksymalne zminimalizowanie użycia odbicia - jest to naprawdę powolne. To znaczy naprawdę wolno. Jeśli masz zamiar to zrobić, sugerowałbym buforowanie odwołań do właściwości. Nie jestem dokładnie zaznajomiony z interfejsem API Reflection, więc jeśli jest trochę niesprawny, po prostu dostosuj go, aby go kompilować:

// elsewhere
Dictionary<object, Property[]> lookupDictionary = new Dictionary<object, Property[]>;

Property[] objectProperties = null;
if (lookupDictionary.ContainsKey(sourceType)) {
  objectProperties = lookupProperties[sourceType];
} else {
  // build array of Property references
  PropertyInfo[] sourcePropertyInfos = sourceType.GetProperties();
  Property[] sourceProperties = new Property[sourcePropertyInfos.length];
  for (int i=0; i < sourcePropertyInfos.length; i++) {
    sourceProperties[i] = sourceType.GetProperty(pi.Name);
  }
  // add to cache
  objectProperties = sourceProperties;
  lookupDictionary[object] = sourceProperties;
}

// loop through and compare against the instances

Muszę jednak powiedzieć, że zgadzam się z innymi plakatami. To pachnie leniwie i nieefektywnie. Zamiast tego powinieneś zaimplementować IComparable :-).

tsimon
źródło
Właśnie patrzyłem na IComparable, ale wydawało się, że służy do sortowania i porządkowania… czy jest to naprawdę przydatne do porównywania równości dwóch obiektów?
nailitdown
Absolutnie, ponieważ .Equals (obiekt o) jest zdefiniowany jako this.CompareTo (o) == 0. Tak więc equals używa ComparesTo () do określenia równości. Będzie to znacznie skuteczniejsze (i standardowa praktyka) niż refleksja.
tsimon
Mogę się mylić, zakładając, że Equals jest implementowane (lub powinno być zaimplementowane) w odniesieniu do CompareTo (). Powinieneś rozważyć nadpisanie Equals, jak opisano tutaj: stackoverflow.com/questions/104158/ ...
tsimon
1

tutaj zmieniono jeden, aby traktować null = null jako równe

 private bool PublicInstancePropertiesEqual<T>(T self, T to, params string[] ignore) where T : class
        {
            if (self != null && to != null)
            {
                Type type = typeof(T);
                List<string> ignoreList = new List<string>(ignore);
                foreach (PropertyInfo pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
                {
                    if (!ignoreList.Contains(pi.Name))
                    {
                        object selfValue = type.GetProperty(pi.Name).GetValue(self, null);
                        object toValue = type.GetProperty(pi.Name).GetValue(to, null);
                        if (selfValue != null)
                        {
                            if (!selfValue.Equals(toValue))
                                return false;
                        }
                        else if (toValue != null)
                            return false;
                    }
                }
                return true;
            }
            return self == to;
        }
Hossein
źródło
Co by było, gdybym miał głęboki wykres obiektów, jaki jest najlepszy sposób użycia powyżej, aby zwrócić listę starych i nowych właściwości, które zostały zmienione?
Rod
1

Skończyło się na tym:

    public static string ToStringNullSafe(this object obj)
    {
        return obj != null ? obj.ToString() : String.Empty;
    }
    public static bool Compare<T>(T a, T b)
    {
        int count = a.GetType().GetProperties().Count();
        string aa, bb;
        for (int i = 0; i < count; i++)
        {
            aa = a.GetType().GetProperties()[i].GetValue(a, null).ToStringNullSafe();
            bb = b.GetType().GetProperties()[i].GetValue(b, null).ToStringNullSafe();
            if (aa != bb)
            {
                return false;
            }
        }
        return true;
    }

Stosowanie:

    if (Compare<ObjectType>(a, b))

Aktualizacja

Jeśli chcesz zignorować niektóre właściwości według nazwy:

    public static string ToStringNullSafe(this object obj)
    {
        return obj != null ? obj.ToString() : String.Empty;
    }
    public static bool Compare<T>(T a, T b, params string[] ignore)
    {
        int count = a.GetType().GetProperties().Count();
        string aa, bb;
        for (int i = 0; i < count; i++)
        {
            aa = a.GetType().GetProperties()[i].GetValue(a, null).ToStringNullSafe();
            bb = b.GetType().GetProperties()[i].GetValue(b, null).ToStringNullSafe();
            if (aa != bb && ignore.Where(x => x == a.GetType().GetProperties()[i].Name).Count() == 0)
            {
                return false;
            }
        }
        return true;
    }

Stosowanie:

    if (MyFunction.Compare<ObjType>(a, b, "Id","AnotherProp"))
BjarkeCK
źródło
1

Możesz zoptymalizować swój kod, wywołując GetProperties tylko raz dla każdego typu:

public static string ToStringNullSafe(this object obj)
{
    return obj != null ? obj.ToString() : String.Empty;
}
public static bool Compare<T>(T a, T b, params string[] ignore)
{
    var aProps = a.GetType().GetProperties();
    var bProps = b.GetType().GetProperties();
    int count = aProps.Count();
    string aa, bb;
    for (int i = 0; i < count; i++)
    {
        aa = aProps[i].GetValue(a, null).ToStringNullSafe();
        bb = bProps[i].GetValue(b, null).ToStringNullSafe();
        if (aa != bb && ignore.Where(x => x == aProps[i].Name).Count() == 0)
        {
            return false;
        }
    }
    return true;
}
Moti Elbilya
źródło
1

Dla kompletności chciałbym dodać odniesienie do http://www.cyotek.com/blog/comparing-the-properties-of-two-objects-via-reflection Ma bardziej kompletną logikę niż większość innych odpowiedzi na tej stronie.

Jednak wolę Porównaj-Net-Przedmioty biblioteka https://github.com/GregFinzer/Compare-Net-Objects (określone przez Liviu Trifoi „s odpowiedzi )
Biblioteka posiada pakiet Nuget http://www.nuget.org/packages/ CompareNETObjects i wiele opcji do skonfigurowania.

Michael Freidgeim
źródło
1

Upewnij się, że przedmioty nie są null.

Posiadanie obj1i obj2:

if(obj1 == null )
{
   return false;
}
return obj1.Equals( obj2 );
Ric Tokyo
źródło
co jeśli obie są zerowe? czy nie są więc równi?
mmr
dobra uwaga na wartości null, w moim przypadku użycie .Equals () wydaje się nie działać, dlatego wymyśliłem to rozwiązanie
nailitdown
cóż, przypadek, dla którego testuję, to dwa obiekty, jeden nowo utworzony, jeden z sesji. porównanie tych dwóch z .Equals () zwraca false, mimo że oba mają identyczne wartości właściwości
nailitdown
0

Działa to nawet wtedy, gdy obiekty są różne. możesz dostosować metody w klasie narzędzi, może chcesz również porównać prywatne właściwości ...

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

class ObjectA
{
    public string PropertyA { get; set; }
    public string PropertyB { get; set; }
    public string PropertyC { get; set; }
    public DateTime PropertyD { get; set; }

    public string FieldA;
    public DateTime FieldB;
}

class ObjectB
{
    public string PropertyA { get; set; }
    public string PropertyB { get; set; }
    public string PropertyC { get; set; }
    public DateTime PropertyD { get; set; }


    public string FieldA;
    public DateTime FieldB;


}

class Program
{
    static void Main(string[] args)
    {
        // create two objects with same properties
        ObjectA a = new ObjectA() { PropertyA = "test", PropertyB = "test2", PropertyC = "test3" };
        ObjectB b = new ObjectB() { PropertyA = "test", PropertyB = "test2", PropertyC = "test3" };

        // add fields to those objects
        a.FieldA = "hello";
        b.FieldA = "Something differnt";

        if (a.ComparePropertiesTo(b))
        {
            Console.WriteLine("objects have the same properties");
        }
        else
        {
            Console.WriteLine("objects have diferent properties!");
        }


        if (a.CompareFieldsTo(b))
        {
            Console.WriteLine("objects have the same Fields");
        }
        else
        {
            Console.WriteLine("objects have diferent Fields!");
        }

        Console.Read();
    }
}

public static class Utilities
{
    public static bool ComparePropertiesTo(this Object a, Object b)
    {
        System.Reflection.PropertyInfo[] properties = a.GetType().GetProperties(); // get all the properties of object a

        foreach (var property in properties)
        {
            var propertyName = property.Name;

            var aValue = a.GetType().GetProperty(propertyName).GetValue(a, null);
            object bValue;

            try // try to get the same property from object b. maybe that property does
                // not exist! 
            {
                bValue = b.GetType().GetProperty(propertyName).GetValue(b, null);
            }
            catch
            {
                return false;
            }

            if (aValue == null && bValue == null)
                continue;

            if (aValue == null && bValue != null)
                return false;

            if (aValue != null && bValue == null)
               return false;

            // if properties do not match return false
            if (aValue.GetHashCode() != bValue.GetHashCode())
            {
                return false;
            }
        }

        return true;
    }



    public static bool CompareFieldsTo(this Object a, Object b)
    {
        System.Reflection.FieldInfo[] fields = a.GetType().GetFields(); // get all the properties of object a

        foreach (var field in fields)
        {
            var fieldName = field.Name;

            var aValue = a.GetType().GetField(fieldName).GetValue(a);

            object bValue;

            try // try to get the same property from object b. maybe that property does
            // not exist! 
            {
                bValue = b.GetType().GetField(fieldName).GetValue(b);
            }
            catch
            {
                return false;
            }

            if (aValue == null && bValue == null)
               continue;

            if (aValue == null && bValue != null)
               return false;

            if (aValue != null && bValue == null)
               return false;


            // if properties do not match return false
            if (aValue.GetHashCode() != bValue.GetHashCode())
            {
                return false;
            }
        }

        return true;
    }


}
Tono Nam
źródło
Ten kod nie jest w 100% skuteczny. nie działa w niektórych sytuacjach, na przykład jeśli zawiera właściwość typu object.
Tono Nam
0

Aktualizacja odpowiedzi Liviu powyżej - CompareObjects.DifferencesString został wycofany.

Działa to dobrze w teście jednostkowym:

CompareLogic compareLogic = new CompareLogic();
ComparisonResult result = compareLogic.Compare(object1, object2);
Assert.IsTrue(result.AreEqual);
Daniel de Zwaan
źródło
1
To świetnie, że naprawiłeś depracację, ale myślę, że ta odpowiedź powinna być komentarzem w odpowiedzi Liviu. Szczególnie dlatego, że w Twoim przykładowym kodzie (w porównaniu do Liviu) brakuje parametrów CompareLogic (które z pewnością są ważne), a także komunikatu assert (który był przestarzały). Twierdzenie można naprawić:Assert.IsTrue(result.AreEqual, result.DifferencesString);
Mariano Desanze
0

Ta metoda pobierze propertiesklasę i porówna wartości dla każdej z nich property. Jeśli którakolwiek z wartości jest inna, będzie return false, w przeciwnym razie będzie return true.

public 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 (Object1 == null || object2 == null)
        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;
}

Stosowanie:

bool isEqual = Compare<Employee>(Object1, Object2)

Nadawca
źródło
0

Aby rozwinąć odpowiedź @nawfal: s, używam tego do testowania obiektów różnych typów w moich testach jednostkowych w celu porównania równych nazw właściwości. W moim przypadku jednostka bazy danych i DTO.

Używane w ten sposób w moim teście;

Assert.IsTrue(resultDto.PublicInstancePropertiesEqual(expectedEntity));



public static bool PublicInstancePropertiesEqual<T, Z>(this T self, Z to, params string[] ignore) where T : class
{
    if (self != null && to != null)
    {
        var type = typeof(T);
        var type2 = typeof(Z);
        var ignoreList = new List<string>(ignore);
        var unequalProperties =
           from pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
           where !ignoreList.Contains(pi.Name)
           let selfValue = type.GetProperty(pi.Name).GetValue(self, null)
           let toValue = type2.GetProperty(pi.Name).GetValue(to, null)
           where selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue))
           select selfValue;
           return !unequalProperties.Any();
    }
    return self == null && to == null;
}
Sgedda
źródło
0

czasami nie chcesz porównywać wszystkich właściwości publicznych i chcesz porównać tylko ich podzbiór, więc w tym przypadku możesz po prostu przenieść logikę, aby porównać żądaną listę właściwości z klasą abstrakcyjną

public abstract class ValueObject<T> where T : ValueObject<T>
{
    protected abstract IEnumerable<object> GetAttributesToIncludeInEqualityCheck();

    public override bool Equals(object other)
    {
        return Equals(other as T);
    }

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

        return GetAttributesToIncludeInEqualityCheck()
            .SequenceEqual(other.GetAttributesToIncludeInEqualityCheck());
    }

    public static bool operator ==(ValueObject<T> left, ValueObject<T> right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(ValueObject<T> left, ValueObject<T> right)
    {
        return !(left == right);
    }

    public override int GetHashCode()
    {
        int hash = 17;
        foreach (var obj in this.GetAttributesToIncludeInEqualityCheck())
            hash = hash * 31 + (obj == null ? 0 : obj.GetHashCode());

        return hash;
    }
}

i użyj tej klasy abstrakcyjnej później do porównania obiektów

public class Meters : ValueObject<Meters>
{
    ...

    protected decimal DistanceInMeters { get; private set; }

    ...

    protected override IEnumerable<object> GetAttributesToIncludeInEqualityCheck()
    {
        return new List<Object> { DistanceInMeters };
    }
}
Mr. Pumpkin
źródło
0

moje rozwiązanie zainspirowane powyższą odpowiedzią Arasa Alenina, w którym dodałem jeden poziom porównania obiektów i niestandardowy obiekt dla wyników porównania. Interesuje mnie również uzyskanie nazwy właściwości z nazwą obiektu:

    public static IEnumerable<ObjectPropertyChanged> GetPublicSimplePropertiesChanged<T>(this T previous, T proposedChange,
     string[] namesOfPropertiesToBeIgnored) where T : class
    {
        return GetPublicGenericPropertiesChanged(previous, proposedChange, namesOfPropertiesToBeIgnored, true, null, null);
    }

    public static IReadOnlyList<ObjectPropertyChanged> GetPublicGenericPropertiesChanged<T>(this T previous, T proposedChange,
        string[] namesOfPropertiesToBeIgnored) where T : class
    {
        return GetPublicGenericPropertiesChanged(previous, proposedChange, namesOfPropertiesToBeIgnored, false, null, null);
    }

    /// <summary>
    /// Gets the names of the public properties which values differs between first and second objects.
    /// Considers 'simple' properties AND for complex properties without index, get the simple properties of the children objects.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="previous">The previous object.</param>
    /// <param name="proposedChange">The second object which should be the new one.</param>
    /// <param name="namesOfPropertiesToBeIgnored">The names of the properties to be ignored.</param>
    /// <param name="simpleTypeOnly">if set to <c>true</c> consider simple types only.</param>
    /// <param name="parentTypeString">The parent type string. Meant only for recursive call with simpleTypeOnly set to <c>true</c>.</param>
    /// <param name="secondType">when calling recursively, the current type of T must be clearly defined here, as T will be more generic (using base class).</param>
    /// <returns>
    /// the names of the properties
    /// </returns>
    private static IReadOnlyList<ObjectPropertyChanged> GetPublicGenericPropertiesChanged<T>(this T previous, T proposedChange,
        string[] namesOfPropertiesToBeIgnored, bool simpleTypeOnly, string parentTypeString, Type secondType) where T : class
    {
        List<ObjectPropertyChanged> propertiesChanged = new List<ObjectPropertyChanged>();

        if (previous != null && proposedChange != null)
        {
            var type = secondType == null ? typeof(T) : secondType;
            string typeStr = parentTypeString + type.Name + ".";
            var ignoreList = namesOfPropertiesToBeIgnored.CreateList();
            IEnumerable<IEnumerable<ObjectPropertyChanged>> genericPropertiesChanged =
                from pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                where !ignoreList.Contains(pi.Name) && pi.GetIndexParameters().Length == 0 
                    && (!simpleTypeOnly || simpleTypeOnly && pi.PropertyType.IsSimpleType())
                let firstValue = type.GetProperty(pi.Name).GetValue(previous, null)
                let secondValue = type.GetProperty(pi.Name).GetValue(proposedChange, null)
                where firstValue != secondValue && (firstValue == null || !firstValue.Equals(secondValue))
                let subPropertiesChanged = simpleTypeOnly || pi.PropertyType.IsSimpleType()
                    ? null
                    : GetPublicGenericPropertiesChanged(firstValue, secondValue, namesOfPropertiesToBeIgnored, true, typeStr, pi.PropertyType)
                let objectPropertiesChanged = subPropertiesChanged != null && subPropertiesChanged.Count() > 0
                    ? subPropertiesChanged
                    : (new ObjectPropertyChanged(proposedChange.ToString(), typeStr + pi.Name, firstValue.ToStringOrNull(), secondValue.ToStringOrNull())).CreateList()
                select objectPropertiesChanged;

            if (genericPropertiesChanged != null)
            {   // get items from sub lists
                genericPropertiesChanged.ForEach(a => propertiesChanged.AddRange(a));
            }
        }
        return propertiesChanged;
    }

Używanie następującej klasy do przechowywania wyników porównania

[System.Serializable]
public class ObjectPropertyChanged
{
    public ObjectPropertyChanged(string objectId, string propertyName, string previousValue, string changedValue)
    {
        ObjectId = objectId;
        PropertyName = propertyName;
        PreviousValue = previousValue;
        ProposedChangedValue = changedValue;
    }

    public string ObjectId { get; set; }

    public string PropertyName { get; set; }

    public string PreviousValue { get; set; }

    public string ProposedChangedValue { get; set; }
}

I przykładowy test jednostkowy:

    [TestMethod()]
    public void GetPublicGenericPropertiesChangedTest1()
    {
        // Define objects to test
        Function func1 = new Function { Id = 1, Description = "func1" };
        Function func2 = new Function { Id = 2, Description = "func2" };
        FunctionAssignment funcAss1 = new FunctionAssignment
        {
            Function = func1,
            Level = 1
        };
        FunctionAssignment funcAss2 = new FunctionAssignment
        {
            Function = func2,
            Level = 2
        };

        // Main test: read properties changed
        var propertiesChanged = Utils.GetPublicGenericPropertiesChanged(funcAss1, funcAss2, null);

        Assert.IsNotNull(propertiesChanged);
        Assert.IsTrue(propertiesChanged.Count == 3);
        Assert.IsTrue(propertiesChanged[0].PropertyName == "FunctionAssignment.Function.Description");
        Assert.IsTrue(propertiesChanged[1].PropertyName == "FunctionAssignment.Function.Id");
        Assert.IsTrue(propertiesChanged[2].PropertyName == "FunctionAssignment.Level");
    }
EricBDev
źródło