เปรียบเทียบคุณสมบัติของวัตถุสองรายการเพื่อค้นหาความแตกต่าง


155

ฉันมีวัตถุสองชนิดที่เหมือนกันและฉันต้องการวนลูปผ่านคุณสมบัติสาธารณะในแต่ละรายการและแจ้งเตือนผู้ใช้เกี่ยวกับคุณสมบัติที่ไม่ตรงกัน

เป็นไปได้ไหมที่จะทำสิ่งนี้โดยไม่ทราบว่าคุณสมบัติของวัตถุมีอะไรบ้าง?


ฉันคิดว่าสิ่งนี้จะช่วยคุณ ... พูดซ้ำถึงคุณสมบัติและค่านิยม
Damien Doumer

ฉันคิดว่านี่จะช่วยคุณได้ ย้ำผ่านคุณสมบัติและค่าของวัตถุ
Damien Doumer

คำตอบ:


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 -> List (of School) -> List (of Classes) -> List (ของนักเรียน)
Peter PitLock

@PeterPitLock: คุณอาจต้องการการจัดการที่แตกต่างกันสำหรับคอลเลกชัน - เพียงแค่การเปรียบเทียบคุณสมบัติในรายการจะไม่ทำงานได้ดี
Jon Skeet

2
ขอบคุณจอนฉันมี MasterObject (MO) และ LightweightMasterObject (LWMO) ซึ่งเป็นรุ่น MasterObject ที่ปล้นได้ แต่ทั้งคู่มีคอลเล็กชัน - ฉันพยายามดูว่าฉันสามารถใช้รหัสที่มีการเรียกซ้ำได้หรือไม่ - LWMO ว่างเปล่า เมื่อเริ่มต้น แต่เมื่อเข้าไปในแต่ละคอลเลคชั่นใน MO และคุณสมบัติของมัน - ค่าของ LWMO ที่สอดคล้องกันถูกตั้งค่า - การใช้งานนี้จะช่วยให้สามารถเรียกซ้ำบนโค้ดที่ให้มาได้หรือไม่?
Peter PitLock

@PeterPitLock: ดูเหมือนว่าคุณควรจะถามคำถามใหม่ ณ จุดนี้โดยทั่วไป - คำถามที่ตอบนี้ไม่เพียงพอกับความต้องการของคุณ
Jon Skeet

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

ฉันคิดว่าเขาหมายถึงวัตถุสองชนิดที่เหมือนกันซึ่งค่าไม่ตรงกัน
ฟรี

@ JaredPar: การกระจายไม่ทำงาน PropertyInfo วัตถุแน่นอนไม่เหมือนกันเว้นแต่ประเภทที่ตัวเองเป็น ...
Mehrdad Afshari

@ Mehrdad ฉันเป็นเพียงตัวอย่างพื้นฐานสำหรับชื่อ ฉันรอ OP เพื่อให้ความกระจ่างแก่สิ่งที่พวกเขามองหาก่อนที่ฉันจะทำให้มันชัดเจนยิ่งขึ้น
JaredPar

1
@ JaredPar: ฉันเข้าใจ แต่นั่นใช้ไม่ได้กับชื่อจริงๆ แม้ว่ามันอาจสื่อสารความคิด แต่มันก็ทำให้เข้าใจผิดเล็กน้อย ลำดับจะไม่เท่ากัน ฉันขอแนะนำให้เพิ่ม a.Select(...)
Mehrdad Afshari

ขอโทษด้วยที่จะอธิบายอย่างชัดเจนฉันหมายถึงสถานที่ที่ค่าในคุณสมบัติที่แตกต่างกัน ขอบคุณ
Gavin

7

ฉันรู้ว่านี่อาจเป็น overkill แต่นี่คือคลาส 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! นี่คือการเขียนซ้ำของโซลูชันจาก Jon Skeet แต่ด้วยรูปแบบที่กะทัดรัดและทันสมัยยิ่งขึ้น นอกจากนี้ควรสร้าง IL ที่มีประสิทธิภาพมากกว่าเล็กน้อย

มันเป็นอะไรแบบนี้

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 ฉันชอบคำตอบของจอนเพราะนี่เป็นเว็บไซต์ช่วยเหลือและการจัดรูปแบบของเขาชัดเจนขึ้น แต่สำหรับคำตอบที่สูงกว่านี้ก็ดีเช่นกัน
Jim Yarbro

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);
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.