Eigenschaften von zwei Objekten vergleichen, um Unterschiede zu finden?

155

Ich habe zwei Objekte des gleichen Typs und möchte die öffentlichen Eigenschaften für jedes Objekt durchlaufen und den Benutzer darüber informieren, welche Eigenschaften nicht übereinstimmen.

Ist dies möglich, ohne zu wissen, welche Eigenschaften das Objekt enthält?

Gavin
quelle
Ich denke, das wird Ihnen helfen ... Durch Eigenschaften und Werte iterieren
Damien Doumer
Ich denke, das wird dir helfen. Durchlaufen Sie die Eigenschaften und Werte eines Objekts .
Damien Doumer

Antworten:

212

Ja, mit Reflexion - vorausgesetzt, jeder Eigenschaftstyp wird Equalsentsprechend implementiert . Eine Alternative wäre die ReflectiveEqualsrekursive Verwendung für alle außer einigen bekannten Typen, aber das wird schwierig.

public bool ReflectiveEquals(object first, object second)
{
    if (first == null && second == null)
    {
        return true;
    }
    if (first == null || second == null)
    {
        return false;
    }
    Type firstType = first.GetType();
    if (second.GetType() != firstType)
    {
        return false; // Or throw an exception
    }
    // This will only use public properties. Is that enough?
    foreach (PropertyInfo propertyInfo in firstType.GetProperties())
    {
        if (propertyInfo.CanRead)
        {
            object firstValue = propertyInfo.GetValue(first, null);
            object secondValue = propertyInfo.GetValue(second, null);
            if (!object.Equals(firstValue, secondValue))
            {
                return false;
            }
        }
    }
    return true;
}
Jon Skeet
quelle
Wäre es möglich, mit dieser Methode eine Rekursion zu verwenden und alle Sammlungen zu vergleichen, über die das Objekt möglicherweise verfügt? zB Objekt1 -> Liste (der Schule) -> Liste (der Klassen) -> Liste (der Schüler)
Peter PitLock
@PeterPitLock: Nun, Sie möchten wahrscheinlich eine andere Behandlung für Sammlungen - nur das Vergleichen von Eigenschaften in Listen würde nicht gut funktionieren.
Jon Skeet
2
Danke Jon, ich habe ein MasterObject (MO) und ein LightweightMasterObject (LWMO), die nur eine abgespeckte Version des MasterObject sind - aber beide haben Sammlungen - Ich versuche zu sehen, ob ich den mit Rekursion gelieferten Code verwenden kann - Das LWMO ist leer Wenn gestartet, aber beim Durchlaufen jeder Sammlung auf dem MO und seinen Eigenschaften - der entsprechende LWMO-Wert wird festgelegt - würde diese Implementierung möglicherweise eine Rekursion des bereitgestellten Codes ermöglichen?
Peter PitLock
@PeterPitLock: Es klingt so, als ob Sie an dieser Stelle eine neue Frage stellen sollten - die Frage, die hier beantwortet wurde, entspricht nicht Ihren Anforderungen.
Jon Skeet
42

Sicher können Sie mit Reflexion. Hier ist der Code zum Abrufen der Eigenschaften eines bestimmten Typs.

var info = typeof(SomeType).GetProperties();

Wenn Sie mehr Informationen darüber geben können, was Sie über die Eigenschaften vergleichen, können wir einen grundlegenden unterschiedlichen Algorithmus zusammenstellen. Dieser Code für die Instanz unterscheidet sich in den Namen

public bool AreDifferent(Type t1, Type t2) {
  var list1 = t1.GetProperties().OrderBy(x => x.Name).Select(x => x.Name);
  var list2 = t2.GetProperties().OrderBy(x => x.Name).Select(x => x.Name);
  return list1.SequenceEqual(list2);
}
JaredPar
quelle
Ich denke, er meinte zwei Objekte des gleichen Typs, bei denen die Werte nicht übereinstimmen.
BFree
@JaredPar: Diffing funktioniert nicht. PropertyInfo-Objekte sind sicherlich nicht identisch, es sei denn, der Typ selbst ist ...
Mehrdad Afshari
@Mehrdad, meins war nur ein einfaches Beispiel für Namen. Ich wartete auf das OP, um Klarheit darüber zu schaffen, wonach sie suchten, bevor ich es genauer formulierte.
JaredPar
1
@JaredPar: Ich verstehe, aber das funktioniert bei Namen nicht wirklich. Während es die Idee kommunizieren könnte, ist es ein bisschen irreführend. Die Reihenfolge wird sowieso nicht gleich sein. Ich schlage vor, ein.Select(...)
Mehrdad Afshari
Entschuldigung, nur um zu verdeutlichen, meinte ich, wo die Werte in den Eigenschaften unterschiedlich sind. Vielen Dank
Gavin
7

Ich weiß, dass dies wahrscheinlich übertrieben ist, aber hier ist meine ObjectComparer-Klasse, die ich genau für diesen Zweck verwende:

/// <summary>
/// Utility class for comparing objects.
/// </summary>
public static class ObjectComparer
{
    /// <summary>
    /// Compares the public properties of any 2 objects and determines if the properties of each
    /// all contain the same value.
    /// <para> 
    /// In cases where object1 and object2 are of different Types (both being derived from Type T) 
    /// we will cast both objects down to the base Type T to ensure the property comparison is only 
    /// completed on COMMON properties.
    /// (ex. Type T is Foo, object1 is GoodFoo and object2 is BadFoo -- both being inherited from Foo --
    /// both objects will be cast to Foo for comparison)
    /// </para>
    /// </summary>
    /// <typeparam name="T">Any class with public properties.</typeparam>
    /// <param name="object1">Object to compare to object2.</param>
    /// <param name="object2">Object to compare to object1.</param>
    /// <param name="propertyInfoList">A List of <see cref="PropertyInfo"/> objects that contain data on the properties
    /// from object1 that are not equal to the corresponding properties of object2.</param>
    /// <returns>A boolean value indicating whether or not the properties of each object match.</returns>
    public static bool GetDifferentProperties<T> ( T object1 , T object2 , out List<PropertyInfo> propertyInfoList )
        where T : class
    {
        return GetDifferentProperties<T>( object1 , object2 , null , out propertyInfoList );
    }

    /// <summary>
    /// Compares the public properties of any 2 objects and determines if the properties of each
    /// all contain the same value.
    /// <para> 
    /// In cases where object1 and object2 are of different Types (both being derived from Type T) 
    /// we will cast both objects down to the base Type T to ensure the property comparison is only 
    /// completed on COMMON properties.
    /// (ex. Type T is Foo, object1 is GoodFoo and object2 is BadFoo -- both being inherited from Foo --
    /// both objects will be cast to Foo for comparison)
    /// </para>
    /// </summary>
    /// <typeparam name="T">Any class with public properties.</typeparam>
    /// <param name="object1">Object to compare to object2.</param>
    /// <param name="object2">Object to compare to object1.</param>
    /// <param name="ignoredProperties">A list of <see cref="PropertyInfo"/> objects
    /// to ignore when completing the comparison.</param>
    /// <param name="propertyInfoList">A List of <see cref="PropertyInfo"/> objects that contain data on the properties
    /// from object1 that are not equal to the corresponding properties of object2.</param>
    /// <returns>A boolean value indicating whether or not the properties of each object match.</returns>
    public static bool GetDifferentProperties<T> ( T object1 , T object2 , List<PropertyInfo> ignoredProperties , out List<PropertyInfo> propertyInfoList )
        where T : class
    {
        propertyInfoList = new List<PropertyInfo>();

        // If either object is null, we can't compare anything
        if ( object1 == null || object2 == null )
        {
            return false;
        }

        Type object1Type = object1.GetType();
        Type object2Type = object2.GetType();

        // In cases where object1 and object2 are of different Types (both being derived from Type T) 
        // we will cast both objects down to the base Type T to ensure the property comparison is only 
        // completed on COMMON properties.
        // (ex. Type T is Foo, object1 is GoodFoo and object2 is BadFoo -- both being inherited from Foo --
        // both objects will be cast to Foo for comparison)
        if ( object1Type != object2Type )
        {
            object1Type = typeof ( T );
            object2Type = typeof ( T );
        }

        // Remove any properties to be ignored
        List<PropertyInfo> comparisonProps =
            RemoveProperties( object1Type.GetProperties() , ignoredProperties );

        foreach ( PropertyInfo object1Prop in comparisonProps )
        {
            Type propertyType = null;
            object object1PropValue = null;
            object object2PropValue = null;

            // Rule out an attempt to check against a property which requires
            // an index, such as one accessed via this[]
            if ( object1Prop.GetIndexParameters().GetLength( 0 ) == 0 )
            {
                // Get the value of each property
                object1PropValue = object1Prop.GetValue( object1 , null );
                object2PropValue = object2Type.GetProperty( object1Prop.Name ).GetValue( object2 , null );

                // As we are comparing 2 objects of the same type we know
                // that they both have the same properties, so grab the
                // first non-null value
                if ( object1PropValue != null )
                    propertyType = object1PropValue.GetType().GetInterface( "IComparable" );

                if ( propertyType == null )
                    if ( object2PropValue != null )
                        propertyType = object2PropValue.GetType().GetInterface( "IComparable" );
            }

            // If both objects have null values or were indexed properties, don't continue
            if ( propertyType != null )
            {
                // If one property value is null and the other is not null, 
                // they aren't equal; this is done here as a native CompareTo
                // won't work with a null value as the target
                if ( object1PropValue == null || object2PropValue == null )
                {
                    propertyInfoList.Add( object1Prop );
                }
                else
                {
                    // Use the native CompareTo method
                    MethodInfo nativeCompare = propertyType.GetMethod( "CompareTo" );

                    // Sanity Check:
                    // If we don't have a native CompareTo OR both values are null, we can't compare;
                    // hence, we can't confirm the values differ... just go to the next property
                    if ( nativeCompare != null )
                    {
                        // Return the native CompareTo result
                        bool equal = ( 0 == (int) ( nativeCompare.Invoke( object1PropValue , new object[] {object2PropValue} ) ) );

                        if ( !equal )
                        {
                            propertyInfoList.Add( object1Prop );
                        }
                    }
                }
            }
        }
        return propertyInfoList.Count == 0;
    }

    /// <summary>
    /// Compares the public properties of any 2 objects and determines if the properties of each
    /// all contain the same value.
    /// <para> 
    /// In cases where object1 and object2 are of different Types (both being derived from Type T) 
    /// we will cast both objects down to the base Type T to ensure the property comparison is only 
    /// completed on COMMON properties.
    /// (ex. Type T is Foo, object1 is GoodFoo and object2 is BadFoo -- both being inherited from Foo --
    /// both objects will be cast to Foo for comparison)
    /// </para>
    /// </summary>
    /// <typeparam name="T">Any class with public properties.</typeparam>
    /// <param name="object1">Object to compare to object2.</param>
    /// <param name="object2">Object to compare to object1.</param>
    /// <returns>A boolean value indicating whether or not the properties of each object match.</returns>
    public static bool HasSamePropertyValues<T> ( T object1 , T object2 )
        where T : class
    {
        return HasSamePropertyValues<T>( object1 , object2 , null );
    }

    /// <summary>
    /// Compares the public properties of any 2 objects and determines if the properties of each
    /// all contain the same value.
    /// <para> 
    /// In cases where object1 and object2 are of different Types (both being derived from Type T) 
    /// we will cast both objects down to the base Type T to ensure the property comparison is only 
    /// completed on COMMON properties.
    /// (ex. Type T is Foo, object1 is GoodFoo and object2 is BadFoo -- both being inherited from Foo --
    /// both objects will be cast to Foo for comparison)
    /// </para>
    /// </summary>
    /// <typeparam name="T">Any class with public properties.</typeparam>
    /// <param name="object1">Object to compare to object2.</param>
    /// <param name="object2">Object to compare to object1.</param>
    /// <param name="ignoredProperties">A list of <see cref="PropertyInfo"/> objects
    /// to ignore when completing the comparison.</param>
    /// <returns>A boolean value indicating whether or not the properties of each object match.</returns>
    public static bool HasSamePropertyValues<T> ( T object1 , T object2 , List<PropertyInfo> ignoredProperties )
        where T : class
    {

        // If either object is null, we can't compare anything
        if ( object1 == null || object2 == null )
        {
            return false;
        }

        Type object1Type = object1.GetType();
        Type object2Type = object2.GetType();

        // In cases where object1 and object2 are of different Types (both being derived from Type T) 
        // we will cast both objects down to the base Type T to ensure the property comparison is only 
        // completed on COMMON properties.
        // (ex. Type T is Foo, object1 is GoodFoo and object2 is BadFoo -- both being inherited from Foo --
        // both objects will be cast to Foo for comparison)
        if ( object1Type != object2Type )
        {
            object1Type = typeof ( T );
            object2Type = typeof ( T );
        }

        // Remove any properties to be ignored
        List<PropertyInfo> comparisonProps =
            RemoveProperties( object1Type.GetProperties() , ignoredProperties );

        foreach ( PropertyInfo object1Prop in comparisonProps )
        {
            Type propertyType = null;
            object object1PropValue = null;
            object object2PropValue = null;

            // Rule out an attempt to check against a property which requires
            // an index, such as one accessed via this[]
            if ( object1Prop.GetIndexParameters().GetLength( 0 ) == 0 )
            {
                // Get the value of each property
                object1PropValue = object1Prop.GetValue( object1 , null );
                object2PropValue = object2Type.GetProperty( object1Prop.Name ).GetValue( object2 , null );

                // As we are comparing 2 objects of the same type we know
                // that they both have the same properties, so grab the
                // first non-null value
                if ( object1PropValue != null )
                    propertyType = object1PropValue.GetType().GetInterface( "IComparable" );

                if ( propertyType == null )
                    if ( object2PropValue != null )
                        propertyType = object2PropValue.GetType().GetInterface( "IComparable" );
            }

            // If both objects have null values or were indexed properties, don't continue
            if ( propertyType != null )
            {
                // If one property value is null and the other is not null, 
                // they aren't equal; this is done here as a native CompareTo
                // won't work with a null value as the target
                if ( object1PropValue == null || object2PropValue == null )
                {
                    return false;
                }

                // Use the native CompareTo method
                MethodInfo nativeCompare = propertyType.GetMethod( "CompareTo" );

                // Sanity Check:
                // If we don't have a native CompareTo OR both values are null, we can't compare;
                // hence, we can't confirm the values differ... just go to the next property
                if ( nativeCompare != null )
                {
                    // Return the native CompareTo result
                    bool equal = ( 0 == (int) ( nativeCompare.Invoke( object1PropValue , new object[] {object2PropValue} ) ) );

                    if ( !equal )
                    {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    /// <summary>
    /// Removes any <see cref="PropertyInfo"/> object in the supplied List of 
    /// properties from the supplied Array of properties.
    /// </summary>
    /// <param name="allProperties">Array containing master list of 
    /// <see cref="PropertyInfo"/> objects.</param>
    /// <param name="propertiesToRemove">List of <see cref="PropertyInfo"/> objects to
    /// remove from the supplied array of properties.</param>
    /// <returns>A List of <see cref="PropertyInfo"/> objects.</returns>
    private static List<PropertyInfo> RemoveProperties (
        IEnumerable<PropertyInfo> allProperties , IEnumerable<PropertyInfo> propertiesToRemove )
    {
        List<PropertyInfo> innerPropertyList = new List<PropertyInfo>();

        // Add all properties to a list for easy manipulation
        foreach ( PropertyInfo prop in allProperties )
        {
            innerPropertyList.Add( prop );
        }

        // Sanity check
        if ( propertiesToRemove != null )
        {
            // Iterate through the properties to ignore and remove them from the list of 
            // all properties, if they exist
            foreach ( PropertyInfo ignoredProp in propertiesToRemove )
            {
                if ( innerPropertyList.Contains( ignoredProp ) )
                {
                    innerPropertyList.Remove( ignoredProp );
                }
            }
        }

        return innerPropertyList;
    }
}
Steve Dignan
quelle
Ich liebe diese Antwort so sehr, aber ich hätte gerne ein Beispiel für die Verwendung der Klassen gesehen. Ich werde dies definitiv für ein Projekt verwenden, an dem ich arbeite
Emmojo
7

Das eigentliche Problem: Wie erhält man die Differenz zweier Sätze?

Der schnellste Weg, den ich gefunden habe, besteht darin, die Mengen zuerst in Wörterbücher umzuwandeln und dann zu unterscheiden. Hier ist ein allgemeiner Ansatz:

static IEnumerable<T> DictionaryDiff<K, T>(Dictionary<K, T> d1, Dictionary<K, T> d2)
{
    return from x in d1 where !d2.ContainsKey(x.Key) select x.Value;
}

Dann können Sie so etwas tun:

static public IEnumerable<PropertyInfo> PropertyDiff(Type t1, Type t2)
{
    var d1 = t1.GetProperties().ToDictionary(x => x.Name);
    var d2 = t2.GetProperties().ToDictionary(x => x.Name);
    return DictionaryDiff(d1, d2);
}
Strahl
quelle
5

Ja. Verwenden Sie Reflexion . Mit Reflection können Sie Dinge tun wie:

//given object of some type
object myObjectFromSomewhere;
Type myObjOriginalType = myObjectFromSomewhere.GetType();
PropertyInfo[] myProps = myObjOriginalType.GetProperties();

Und dann können Sie die resultierenden PropertyInfo-Klassen verwenden, um alle möglichen Dinge zu vergleichen.

GWLlosa
quelle
4

Vergleichen zweier Objekte desselben Typs mit LINQ und Reflection. NB! Dies ist im Grunde eine Neufassung der Lösung von Jon Skeet, jedoch mit einer kompakteren und moderneren Syntax. Es sollte auch eine etwas effektivere IL erzeugen.

Es geht ungefähr so:

public bool ReflectiveEquals(LocalHdTicket serverTicket, LocalHdTicket localTicket)
  {
     if (serverTicket == null && localTicket == null) return true;
     if (serverTicket == null || localTicket == null) return false;

     var firstType = serverTicket.GetType();
     // Handle type mismatch anyway you please:
     if(localTicket.GetType() != firstType) throw new Exception("Trying to compare two different object types!");

     return !(from propertyInfo in firstType.GetProperties() 
              where propertyInfo.CanRead 
              let serverValue = propertyInfo.GetValue(serverTicket, null) 
              let localValue = propertyInfo.GetValue(localTicket, null) 
              where !Equals(serverValue, localValue) 
              select serverValue).Any();
  }

quelle
2
Wäre Rekursion nützlich? Ersetzen Sie die Zeile where !Equals(serverValue, localValue)durchfirstType.IsValueType ? !Equals(serverValue, localValue) : !ReflectiveEquals(serverValue, localValue)
drzaus
3
Könnte moderner sein, aber nicht kompakter. Sie haben gerade eine ganze Reihe von Leerzeichen entfernt und das Lesen erschwert.
Eliezer Steinbock
EliezerSteinbock das ist kaum der Fall. Obwohl er Leerzeichen loswurde und das Lesen erschwerte, war das nicht NUR das, was er getan hat. Die dortige LINQ-Anweisung wird anders kompiliert als die foreach-Anweisung in der Antwort von @ jon-skeet. Ich bevorzuge Jons Antwort, da dies eine Hilfeseite ist und seine Formatierung klarer ist, aber für eine fortgeschrittenere Antwort ist diese auch gut.
Jim Yarbro
4
Wenn "moderner" gleich "schwerer zu lesen" ist, bewegen wir uns in die falsche Richtung.
bwegs
1

Wie viele den rekursiven Ansatz erwähnt haben, können Sie mit dieser Funktion den gesuchten Namen und die Eigenschaft zunächst an Folgendes übergeben:

    public static void loopAttributes(PropertyInfo prop, string targetAttribute, object tempObject)
    {
        foreach (PropertyInfo nestedProp in prop.PropertyType.GetProperties())
        {
            if(nestedProp.Name == targetAttribute)
            {
                //found the matching attribute
            }
            loopAttributes(nestedProp, targetAttribute, prop.GetValue(tempObject);
        }
    }

//in the main function
foreach (PropertyInfo prop in rootObject.GetType().GetProperties())
{
    loopAttributes(prop, targetAttribute, rootObject);
}
simon w
quelle