¿Comparar las propiedades de dos objetos para encontrar diferencias?
155
Tengo dos objetos del mismo tipo y quiero recorrer las propiedades públicas de cada uno de ellos y alertar al usuario sobre qué propiedades no coinciden.
¿Es posible hacer esto sin saber qué propiedades contiene el objeto?
Sí, con reflexión, suponiendo que cada tipo de propiedad se implemente Equalsadecuadamente. Una alternativa sería usar ReflectiveEqualsrecursivamente para todos menos algunos tipos conocidos, pero eso se vuelve complicado.
publicboolReflectiveEquals(object first,object second){if(first ==null&& second ==null){returntrue;}if(first ==null|| second ==null){returnfalse;}Type firstType = first.GetType();if(second.GetType()!= firstType){returnfalse;// 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)){returnfalse;}}}returntrue;}
¿Sería posible utilizar la recursividad con este método y comparar todas las colecciones que pueda tener el objeto? Ejemplo: Object1 -> List (of School) -> List (of Classes) -> List (of Students)
Peter PitLock
@PeterPitLock: Bueno, es probable que desee un manejo diferente para las colecciones; solo comparar propiedades en listas no funcionaría bien.
Jon Skeet
2
Gracias, tengo un MasterObject (MO) y un LightweightMasterObject (LWMO), que es solo una versión simplificada del MasterObject, pero ambos tienen colecciones. Estoy tratando de ver si puedo usar el código provisto con recursión. El LWMO está vacío. cuando se inicia, pero al atravesar cada colección en el MO y sus propiedades (se establece el valor de LWMO correspondiente), ¿esta implementación permitiría la recurrencia en el código proporcionado tal vez?
Peter PitLock
@PeterPitLock: Parece que debería hacer una nueva pregunta en este momento, básicamente, la pregunta que estaba respondiendo no está lo suficientemente cerca de sus requisitos.
Jon Skeet
42
Claro que puedes con la reflexión. Aquí está el código para tomar las propiedades de un tipo dado.
var info =typeof(SomeType).GetProperties();
Si puede dar más información sobre lo que está comparando sobre las propiedades, podemos reunir un algoritmo básico diferente. Este código para intstance diferirá en los nombres
Creo que se refería a dos objetos del mismo tipo donde los valores no coinciden.
BFree
@JaredPar: La diferenciación no funciona. PropertyInfo objetos ciertamente no son idénticos a menos que el tipo en sí es ...
Mehrdad Afshari
@Mehrdad, el mío fue solo un ejemplo básico de nombres. Estaba esperando en el OP para dar claridad a lo que estaban buscando antes de hacerlo más específico.
JaredPar 05 de
1
@JaredPar: Entiendo, pero eso realmente no funciona para los nombres. Si bien puede comunicar la idea, es un poco engañoso. La secuencia no será igual de todos modos. Sugiero agregar un.Select(...)
Mehrdad Afshari
lo siento, solo para aclarar quise decir dónde los valores en las propiedades son diferentes. Gracias
Gavin
7
Sé que esto probablemente sea excesivo, pero aquí está mi clase ObjectComparer que uso para este mismo propósito:
/// <summary>/// Utility class for comparing objects./// </summary>publicstaticclassObjectComparer{/// <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>publicstaticboolGetDifferentProperties<T>( T object1 , T object2 ,outList<PropertyInfo> propertyInfoList )where T :class{returnGetDifferentProperties<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>publicstaticboolGetDifferentProperties<T>( T object1 , T object2 ,List<PropertyInfo> ignoredProperties ,outList<PropertyInfo> propertyInfoList )where T :class{
propertyInfoList =newList<PropertyInfo>();// If either object is null, we can't compare anythingif( object1 ==null|| object2 ==null){returnfalse;}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 ignoredList<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 valueif( 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 continueif( 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 targetif( object1PropValue ==null|| object2PropValue ==null){
propertyInfoList.Add( object1Prop );}else{// Use the native CompareTo methodMethodInfo 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 propertyif( nativeCompare !=null){// Return the native CompareTo resultbool equal =(0==(int)( nativeCompare.Invoke( object1PropValue ,newobject[]{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>publicstaticboolHasSamePropertyValues<T>( T object1 , T object2 )where T :class{returnHasSamePropertyValues<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>publicstaticboolHasSamePropertyValues<T>( T object1 , T object2 ,List<PropertyInfo> ignoredProperties )where T :class{// If either object is null, we can't compare anythingif( object1 ==null|| object2 ==null){returnfalse;}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 ignoredList<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 valueif( 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 continueif( 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 targetif( object1PropValue ==null|| object2PropValue ==null){returnfalse;}// Use the native CompareTo methodMethodInfo 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 propertyif( nativeCompare !=null){// Return the native CompareTo resultbool equal =(0==(int)( nativeCompare.Invoke( object1PropValue ,newobject[]{object2PropValue})));if(!equal ){returnfalse;}}}}returntrue;}/// <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>privatestaticList<PropertyInfo>RemoveProperties(IEnumerable<PropertyInfo> allProperties ,IEnumerable<PropertyInfo> propertiesToRemove ){List<PropertyInfo> innerPropertyList =newList<PropertyInfo>();// Add all properties to a list for easy manipulationforeach(PropertyInfo prop in allProperties ){
innerPropertyList.Add( prop );}// Sanity checkif( propertiesToRemove !=null){// Iterate through the properties to ignore and remove them from the list of // all properties, if they existforeach(PropertyInfo ignoredProp in propertiesToRemove ){if( innerPropertyList.Contains( ignoredProp )){
innerPropertyList.Remove( ignoredProp );}}}return innerPropertyList;}}
Me encanta esta respuesta, pero me hubiera gustado ver un ejemplo de uso de las clases. Definitivamente voy a usar esto para un proyecto en el que estoy trabajando
emmojo 05 de
7
El verdadero problema: ¿Cómo obtener la diferencia de dos conjuntos?
La forma más rápida que he encontrado es convertir primero los conjuntos a diccionarios, luego difuminarlos. Aquí hay un enfoque genérico:
staticIEnumerable<T>DictionaryDiff<K, T>(Dictionary<K, T> d1,Dictionary<K, T> d2){returnfrom x in d1 where!d2.ContainsKey(x.Key)select x.Value;}
Comparar dos objetos del mismo tipo usando LINQ y Reflection. ¡NÓTESE BIEN! Esto es básicamente una reescritura de la solución de Jon Skeet, pero con una sintaxis más compacta y moderna. También debería generar un poco más de IL efectiva.
Es algo parecido a esto:
publicboolReflectiveEquals(LocalHdTicket serverTicket,LocalHdTicket localTicket){if(serverTicket ==null&& localTicket ==null)returntrue;if(serverTicket ==null|| localTicket ==null)returnfalse;var firstType = serverTicket.GetType();// Handle type mismatch anyway you please:if(localTicket.GetType()!= firstType)thrownewException("Trying to compare two different object types!");return!(from propertyInfo in firstType.GetProperties()where propertyInfo.CanReadlet serverValue = propertyInfo.GetValue(serverTicket,null)let localValue = propertyInfo.GetValue(localTicket,null)where!Equals(serverValue, localValue)select serverValue).Any();}
¿Sería útil la recursión? reemplace la línea where !Equals(serverValue, localValue)confirstType.IsValueType ? !Equals(serverValue, localValue) : !ReflectiveEquals(serverValue, localValue)
drzaus
3
Puede ser más moderno, pero no más compacto. Acabas de deshacerte de un montón de espacios en blanco y lo hiciste más difícil de leer.
Eliezer Steinbock
EliezerSteinbock ese no es el caso. Si bien eliminó el espacio en blanco y lo hizo más difícil de leer, eso no es SOLO lo que hizo. La declaración LINQ allí se compila de manera diferente a la declaración foreach en la respuesta de @ jon-skeet. Prefiero la respuesta de Jon porque este es un sitio de ayuda y su formato es más claro, pero para una respuesta más avanzada, esta también es buena.
Jim Yarbro
44
Si "más moderno" equivale a "más difícil de leer", entonces nos estamos moviendo en la dirección equivocada.
Como muchos mencionaron el enfoque recursivo, esta es la función que puede pasar el nombre buscado y la propiedad para comenzar:
publicstaticvoid 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 functionforeach(PropertyInfo prop in rootObject.GetType().GetProperties()){
loopAttributes(prop, targetAttribute, rootObject);}
Respuestas:
Sí, con reflexión, suponiendo que cada tipo de propiedad se implemente
Equals
adecuadamente. Una alternativa sería usarReflectiveEquals
recursivamente para todos menos algunos tipos conocidos, pero eso se vuelve complicado.fuente
Claro que puedes con la reflexión. Aquí está el código para tomar las propiedades de un tipo dado.
Si puede dar más información sobre lo que está comparando sobre las propiedades, podemos reunir un algoritmo básico diferente. Este código para intstance diferirá en los nombres
fuente
.Select(...)
Sé que esto probablemente sea excesivo, pero aquí está mi clase ObjectComparer que uso para este mismo propósito:
fuente
El verdadero problema: ¿Cómo obtener la diferencia de dos conjuntos?
La forma más rápida que he encontrado es convertir primero los conjuntos a diccionarios, luego difuminarlos. Aquí hay un enfoque genérico:
Entonces puedes hacer algo como esto:
fuente
Si. Utiliza la reflexión . Con Reflection, puedes hacer cosas como:
Y luego puede usar las clases PropertyInfo resultantes para comparar todo tipo de cosas.
fuente
Comparar dos objetos del mismo tipo usando LINQ y Reflection. ¡NÓTESE BIEN! Esto es básicamente una reescritura de la solución de Jon Skeet, pero con una sintaxis más compacta y moderna. También debería generar un poco más de IL efectiva.
Es algo parecido a esto:
fuente
where !Equals(serverValue, localValue)
confirstType.IsValueType ? !Equals(serverValue, localValue) : !ReflectiveEquals(serverValue, localValue)
Type.GetProperties enumerará cada una de las propiedades de un tipo determinado. Luego use PropertyInfo.GetValue para verificar los valores.
fuente
Como muchos mencionaron el enfoque recursivo, esta es la función que puede pasar el nombre buscado y la propiedad para comenzar:
fuente