Comparer les propriétés de deux objets pour trouver des différences?
155
J'ai deux objets du même type et je veux parcourir les propriétés publiques de chacun d'eux et alerter l'utilisateur sur les propriétés qui ne correspondent pas.
Est-il possible de faire cela sans connaître les propriétés que contient l'objet?
Oui, avec réflexion - en supposant que chaque type de propriété est Equalscorrectement implémenté . Une alternative serait d'utiliser ReflectiveEqualsrécursivement pour tous sauf certains types connus, mais cela devient délicat.
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;}
Serait-il possible d'utiliser la récursivité avec cette méthode et de comparer toutes les collections que l'objet peut avoir? par exemple Object1 -> List (of School) -> List (of Classes) -> List (of Students)
Peter PitLock
@PeterPitLock: Eh bien, vous voudriez probablement une gestion différente pour les collections - la simple comparaison des propriétés sur des listes ne fonctionnerait pas bien.
Jon Skeet
2
Merci jon, j'ai un MasterObject (MO) et un LightweightMasterObject (LWMO) qui n'est qu'une version allégée du MasterObject - mais les deux ont des collections - J'essaie de voir si je peux utiliser le code fourni avec la récursivité - Le LWMO est vide au démarrage, mais lors de la traversée de chaque collection sur le MO et ses propriétés - la valeur de LWMO correspondante est définie - cette implémentation permettrait peut-être une récursivité sur le code fourni?
Peter PitLock
@PeterPitLock: Il semble que vous devriez poser une nouvelle question à ce stade, en gros - la question à laquelle elle répondait n'est pas suffisamment proche de vos besoins.
Jon Skeet
42
Bien sûr, vous pouvez avec réflexion. Voici le code pour récupérer les propriétés d'un type donné.
var info =typeof(SomeType).GetProperties();
Si vous pouvez donner plus d'informations sur ce que vous comparez sur les propriétés, nous pouvons rassembler un algorithme de base différent. Ce code pour l'instance diffère sur les noms
Je pense qu'il voulait dire deux objets du même type où les valeurs ne correspondent pas.
BGratuit le
@JaredPar: Diffing ne fonctionne pas. Les objets PropertyInfo ne sont certainement pas identiques sauf si le type lui-même est ...
Mehrdad Afshari
@Mehrdad, le mien n'était qu'un exemple basique de noms. J'attendais que l'OP clarifie ce qu'ils recherchaient avant de le préciser.
JaredPar
1
@JaredPar: Je comprends, mais cela ne fonctionne pas vraiment pour les noms. Bien que cela puisse communiquer l'idée, c'est un peu trompeur. La séquence ne sera pas égale de toute façon. Je suggère d'ajouter un.Select(...)
Mehrdad Afshari
désolé, juste pour clarifier, je voulais dire où les valeurs dans les propriétés sont différentes. Merci
Gavin
7
Je sais que c'est probablement exagéré, mais voici ma classe ObjectComparer que j'utilise dans ce but précis:
/// <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;}}
J'adore cette réponse mais j'aurais aimé voir un exemple d'utilisation des classes. Je vais certainement l'utiliser pour un projet sur
lequel
7
Le vrai problème: comment faire la différence entre deux ensembles?
Le moyen le plus rapide que j'ai trouvé est de convertir d'abord les ensembles en dictionnaires, puis de les différencier. Voici une approche générique:
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;}
Ensuite, vous pouvez faire quelque chose comme ceci:
Comparaison de deux objets du même type à l'aide de LINQ et de Reflection. NB! Il s'agit essentiellement d'une réécriture de la solution de Jon Skeet, mais avec une syntaxe plus compacte et moderne. Il devrait également générer une IL légèrement plus efficace.
Ca fait plutot comme ca:
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();}
la récursion serait-elle utile? remplacer la ligne where !Equals(serverValue, localValue)parfirstType.IsValueType ? !Equals(serverValue, localValue) : !ReflectiveEquals(serverValue, localValue)
drzaus
3
Peut-être plus moderne, mais pas plus compact. Vous venez de vous débarrasser de tout un tas d'espaces et de rendre la lecture plus difficile.
Eliezer Steinbock
EliezerSteinbock ce n'est guère le cas. Bien qu'il se soit débarrassé des espaces et qu'il ait rendu la lecture plus difficile, ce n'est pas JUSTE ce qu'il a fait. L'instruction LINQ s'y compile différemment de l'instruction foreach dans la réponse de @ jon-skeet. Je préfère la réponse de Jon car il s'agit d'un site d'aide, et sa mise en forme est plus claire, mais pour une réponse plus avancée, celle-ci est également bonne.
Jim Yarbro
4
Si «plus moderne» équivaut à «plus difficile à lire», alors nous allons dans la mauvaise direction.
Comme beaucoup l'ont mentionné l'approche récursive, c'est la fonction avec laquelle vous pouvez passer le nom recherché et la propriété pour commencer à:
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);}
Réponses:
Oui, avec réflexion - en supposant que chaque type de propriété est
Equals
correctement implémenté . Une alternative serait d'utiliserReflectiveEquals
récursivement pour tous sauf certains types connus, mais cela devient délicat.la source
Bien sûr, vous pouvez avec réflexion. Voici le code pour récupérer les propriétés d'un type donné.
Si vous pouvez donner plus d'informations sur ce que vous comparez sur les propriétés, nous pouvons rassembler un algorithme de base différent. Ce code pour l'instance diffère sur les noms
la source
.Select(...)
Je sais que c'est probablement exagéré, mais voici ma classe ObjectComparer que j'utilise dans ce but précis:
la source
Le vrai problème: comment faire la différence entre deux ensembles?
Le moyen le plus rapide que j'ai trouvé est de convertir d'abord les ensembles en dictionnaires, puis de les différencier. Voici une approche générique:
Ensuite, vous pouvez faire quelque chose comme ceci:
la source
Oui. Utilisez la réflexion . Avec Reflection, vous pouvez faire des choses comme:
Et puis vous pouvez utiliser les classes PropertyInfo résultantes pour comparer toutes sortes de choses.
la source
Comparaison de deux objets du même type à l'aide de LINQ et de Reflection. NB! Il s'agit essentiellement d'une réécriture de la solution de Jon Skeet, mais avec une syntaxe plus compacte et moderne. Il devrait également générer une IL légèrement plus efficace.
Ca fait plutot comme ca:
la source
where !Equals(serverValue, localValue)
parfirstType.IsValueType ? !Equals(serverValue, localValue) : !ReflectiveEquals(serverValue, localValue)
Type.GetProperties listera chacune des propriétés d'un type donné. Utilisez ensuite PropertyInfo.GetValue pour vérifier les valeurs.
la source
Comme beaucoup l'ont mentionné l'approche récursive, c'est la fonction avec laquelle vous pouvez passer le nom recherché et la propriété pour commencer à:
la source