So sánh thuộc tính của hai đối tượng để tìm sự khác biệt?


155

Tôi có hai đối tượng cùng loại và tôi muốn lặp qua các thuộc tính công khai trên mỗi đối tượng đó và cảnh báo cho người dùng về những thuộc tính nào không khớp.

Có thể làm điều này mà không biết các thuộc tính mà đối tượng chứa?


Tôi nghĩ rằng điều này sẽ giúp bạn ... Lặp lại thông qua các thuộc tính và giá trị
Damien Doumer

Câu trả lời:


212

Có, với sự phản ánh - giả sử mỗi loại tài sản thực hiện Equalsmột cách thích hợp. Một cách khác là sử dụng ReflectiveEqualsđệ quy cho tất cả trừ một số loại đã biết, nhưng điều đó trở nên khó khăn.

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

Có thể sử dụng đệ quy với phương thức này và so sánh tất cả các bộ sưu tập mà đối tượng có thể có? ví dụ: Object1 -> List (of School) -> List (of Classes) -> List (of Student)
Peter PitLock 15/2/2016

@PeterPitLock: Có lẽ bạn sẽ muốn xử lý khác nhau cho các bộ sưu tập - chỉ cần so sánh các thuộc tính trong danh sách sẽ không hoạt động tốt.
Jon Skeet

2
Cảm ơn jon, tôi có MasterObject (MO) và LightMasterObject (LWMO), đây chỉ là phiên bản rút gọn của MasterObject - nhưng cả hai đều có các bộ sưu tập - Tôi đang cố gắng xem liệu tôi có thể sử dụng mã được cung cấp với đệ quy không khi bắt đầu, nhưng khi đi qua từng bộ sưu tập trên MO và các thuộc tính của nó - giá trị của LWMO tương ứng được đặt - việc triển khai này có cho phép đệ quy trên mã được cung cấp không?
Peter PitLock

@PeterPitLock: Có vẻ như bạn nên hỏi một câu hỏi mới vào thời điểm này, về cơ bản - câu hỏi mà câu trả lời này không đủ gần với yêu cầu của bạn.
Jon Skeet

42

Chắc chắn bạn có thể với sự phản ánh. Đây là mã để lấy các thuộc tính của một loại nhất định.

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

Nếu bạn có thể cung cấp thêm thông tin về những gì bạn đang so sánh về các thuộc tính, chúng ta có thể có được một thuật toán khác nhau cơ bản. Mã này cho intstance sẽ khác về tên

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

Tôi nghĩ rằng anh ta có nghĩa là hai đối tượng cùng loại trong đó các giá trị không khớp.
BFree

@JaredPar: Diffing không hoạt động. Các đối tượng PropertyInfo chắc chắn không giống nhau trừ khi chính loại đó là ...
Mehrdad Afshari

@Mehrdad, của tôi chỉ là một ví dụ cơ bản cho tên. Tôi đã chờ đợi trên OP để làm rõ những gì họ đang tìm kiếm trước khi tôi làm cho nó cụ thể hơn.
JaredPar

1
@JaredPar: Tôi hiểu, nhưng điều đó không thực sự hiệu quả với tên. Trong khi nó có thể truyền đạt ý tưởng, nó hơi sai lệch. Trình tự sẽ không bằng nhau. Tôi đề nghị thêm một.Select(...)
Mehrdad Afshari

xin lỗi, chỉ để làm rõ ý tôi là nơi các giá trị trong các thuộc tính khác nhau. Cảm ơn
Gavin

7

Tôi biết điều này có thể là quá mức cần thiết, nhưng đây là lớp ObjectComparer của tôi, tôi sử dụng cho mục đích này:

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

Tôi thích câu trả lời này rất nhiều nhưng tôi muốn thấy một ví dụ về cách sử dụng các lớp. Tôi chắc chắn sẽ sử dụng nó cho một dự án tôi đang làm
emmojo

7

Vấn đề thực sự: Làm thế nào để có được sự khác biệt của hai bộ?

Cách nhanh nhất tôi tìm thấy là chuyển đổi các bộ thành từ điển trước, sau đó khác. Đây là một cách tiếp cận chung:

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

Sau đó, bạn có thể làm một cái gì đó như thế này:

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

Đúng. Sử dụng Phản xạ . Với Reflection, bạn có thể làm những việc như:

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

Và sau đó, bạn có thể sử dụng các lớp PropertyInfo kết quả để so sánh tất cả các cách thức của mọi thứ.


4

So sánh hai đối tượng cùng loại bằng LINQ và Reflection. Lưu ý! Về cơ bản, đây là một bản viết lại của giải pháp từ Jon Skeet, nhưng với một cú pháp nhỏ gọn và hiện đại hơn. Nó cũng sẽ tạo ra IL hiệu quả hơn một chút.

Nó đi một cái gì đó như thế này:

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
đệ quy sẽ hữu ích? thay thế dòng where !Equals(serverValue, localValue)bằngfirstType.IsValueType ? !Equals(serverValue, localValue) : !ReflectiveEquals(serverValue, localValue)
drzaus

3
Có thể hiện đại hơn, nhưng không gọn hơn. Bạn vừa thoát khỏi một loạt các khoảng trắng và làm cho nó khó đọc hơn.
Eliezer Steinbock

EliezerSteinbock hầu như không phải vậy. Trong khi anh ta đã thoát khỏi khoảng trắng và anh ta đã làm cho nó khó đọc hơn, đó không phải là CHỈ LÀ những gì anh ta đã làm. Câu lệnh LINQ ở đó biên dịch khác với câu lệnh foreach trong câu trả lời từ @ jon-skeet. Tôi thích câu trả lời của Jon vì đây là trang trợ giúp và định dạng của anh ấy rõ ràng hơn, nhưng đối với câu trả lời nâng cao hơn, thì câu trả lời này cũng tốt.
Jim Yarbro

4
Nếu "hiện đại hơn" tương đương với "khó đọc hơn" thì chúng ta đang đi sai hướng.
bwegs


1

Như nhiều người đã đề cập đến cách tiếp cận đệ quy, đây là chức năng bạn có thể chuyển tên được tìm kiếm và thuộc tính để bắt đầu:

    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);
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.