Порівняйте властивості двох об’єктів, щоб знайти відмінності?


155

У мене є два об'єкти одного типу, і я хочу переглядати загальнодоступні властивості на кожному з них і попередити користувача про те, які властивості не відповідають.

Чи можливо це зробити, не знаючи, які властивості об'єкт містить?



Відповіді:


212

Так, з відображенням - припускаючи, що кожен тип властивості реалізується Equalsналежним чином. Альтернативою було б використовувати ReflectiveEqualsрекурсивно для всіх, крім відомих типів, але це стає складним.

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

Чи можна було б використовувати рекурсію за допомогою цього методу та порівняти всі колекції, які може мати об’єкт? наприклад, Object1 -> Список (для школи) -> Список (для класів) -> Список (для учнів)
Peter PitLock

@PeterPitLock: Ну, ви, мабуть, хочете по-різному обробляти колекції - просто порівняння властивостей у списках не буде добре.
Джон Скіт

2
Дякую, Джон, у мене є MasterObject (MO) та LightweightMasterObject (LWMO), який є лише знятою версією MasterObject - але в обох є колекції - Я намагаюся побачити, чи можу я використовувати код, наданий при рекурсії - LWMO порожній коли починається, але при проходженні через кожну колекцію MO та його властивостей - встановлено відповідне значення LWMO - чи дозволить ця реалізація рекурсувати на наданий код?
Пітер ПітЛок

@PeterPitLock: Схоже, вам слід задавати нове запитання на даний момент, в основному - питання, на яке відповіли, недостатньо відповідає вашим вимогам.
Джон Скіт

42

Звичайно, ви можете з роздумом. Ось код для захоплення властивостей певного типу.

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

Якщо ви можете дати більше інформації про порівняння властивостей, ми можемо зібрати основний алгоритм, що відрізняється. Цей код для предметності відрізнятиметься від імен

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

Я думаю, що він мав на увазі два об'єкти одного типу, де значення не збігаються.
BFree

@JaredPar: Різниця не працює. Об'єкти PropertyInfo, безумовно, не є ідентичними, якщо тільки тип не є ...
Mehrdad Afshari

@Mehrdad, мій був лише основним прикладом для імен. Я чекав на ОП, щоб зрозуміти, що вони шукали, перш ніж я зробив це більш конкретним.
JaredPar

1
@JaredPar: Я розумію, але це не дуже працює для імен. Хоча це може повідомити ідею, це трохи вводить в оману. Послідовність все одно не буде рівною. Пропоную додати.Select(...)
Мехрдад Афшарі

Вибачте, просто для уточнення я мав на увазі, де значення у властивостях різні. Спасибі
Гевін

7

Я знаю, що це, мабуть, надмірно, але ось мій клас ObjectComparer, який я використовую саме для цієї мети:

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

Я дуже люблю цю відповідь, але хотілося б, щоб я бачив приклад використання класів. Я обов'язково буду використовувати це для проекту, над яким я працюю
emmojo

7

Справжня проблема: як отримати різницю двох множин?

Найшвидший спосіб, який я знайшов, - це перетворити набори спочатку в словники, а потім розімкнути їх. Ось загальний підхід:

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

Тоді ви можете зробити щось подібне:

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

5

Так. Використовуйте відбиття . За допомогою Reflection ви можете робити такі речі, як:

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

І тоді ви можете використовувати отримані класи PropertyInfo для порівняння всіх речей.


4

Порівняння двох об'єктів одного типу за допомогою LINQ та Reflection. NB! Це в основному переписання рішення з Джона Скіта, але з більш компактним і сучасним синтаксисом. Він також повинен генерувати дещо більш ефективний ІЛ.

Це щось подібне:

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

2
була б корисна рекурсія? замініть рядок where !Equals(serverValue, localValue)наfirstType.IsValueType ? !Equals(serverValue, localValue) : !ReflectiveEquals(serverValue, localValue)
drzaus

3
Можливо, буде більш сучасним, але не більш компактним. Ви просто позбулися цілого ряду пробілів і ускладнили читання.
Eliezer Steinbock

EliezerSteinbock навряд чи так. Поки він позбувся пробілу, і він чинив його важче для читання, це НЕ ПЕРЕШЕ, що він зробив. Заява LINQ там складається інакше, ніж твердження foreach у відповіді від @ jon-skeet. Я вважаю за краще відповідь Джона, оскільки це сайт довідки, і його форматування є більш зрозумілим, але для більш розширеної відповіді це добре також.
Джим Ярбро

4
Якщо "більш сучасний" прирівнюється до "важче читати", то ми рухаємось у неправильному напрямку.
bwegs


1

Як багато хто згадував рекурсивний підхід, це функція, з якої ви можете передати шукане ім'я та властивість для початку:

    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);
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.