Comparison of objects
Un article de SmartEiffelWiki, l'encyclopéde libre.
| Sommaire |
Comparaison de deux objets
Le problème de la comparaison de deux objets est un aspect qu'il faut maîtriser assez rapidement car ce problème est très courant et se pose dans de nombreuses applications pour ne pas dire presque toutes les applications.
En Eiffel ainsi que dans la plupart des langages à objets, il existe plusieurs façons de comparer deux objets. Soit l'on souhaite comparer les addresses respectives des deux objets en question, soit l'on souhaite comparer les deux objets selon les informations propres à chacun des deux objets comparés. Dit autrement et selon la terminologie Eiffel, on doit choisir entre l'utilisation de l'opérateur = prédéfini ou bien opter pour la méthode redéfinissable is_equal.
L'objectif du langage Eiffel consistant à éviter autant que possible les fautes d'inattention, il n'est pas possible de comparer n'importe quel type d'expression avec n'importe quel autre type d'expression. La validité d'une comparaison sera détaillée à la suite.
Enfin, et ceci uniquement pour être complet, cette partie traitera également la possibilité d'effectuer une troisième sorte de comparaison, la comparaison en profondeur, comparaison uniquement réservée à une famille très rare d'applications.
Soient deux variables point_a et point_b toutes deux de type POINT. Supposons que ces deux variables sont effectivement initialisées avec autre chose que la valeur Void. Le fragment de code suivant présente une utilisation possible de l'opérateur prédéfini = de comparaison :
if point_a = point_b then
io.put_string("Il s'agit en fait d'un seul et unique POINT !")
else
io.put_string("Il s'agit de deux POINTs à des adresses différentes en mémoire.")
end
La comparaison avec l'opérateur = est la brique de base la plus élémentaire pour la comparaison. Dans le cas d'un type référence, elle correspond à la comparaison simple et directe des deux adresses. Ce qui est effectivement pointé par les deux adresses n'est pas pris en compte. Si nos deux variables point_a et point_b désignent en fait un seul et unique POINT en mémoire, alors le test précédent est vrai. Dès qu'il y a effectivement deux POINTs distincts en mémoire, alors le test est faux même si les deux POINTs semblent parfaitement identiques par exemple lorsqu'on les observe avec le débogueur ou par exemple, tout simplement, lorsqu'on les imprime.
Notons que l'opérateur /= qui correspond à la négation de l'opérateur = existe aussi. L'opérateur /= est lui aussi prédéfini et est un simple raccourci pour éviter d'utiliser la négation not. Ainsi pour tester si une variable référence effectivement un objet, il est d'usage d'écrire :
if point /= Void then
io.put_string("La variable pointe effectivement sur un POINT !")
else
io.put_string("La variable point ne désigne aucun objet.")
end
En ce qui concerne la comparaison de variables dont le type est expanded, l'effet des opérateurs = et /= correspond simplement à la comparaison des valeurs en question. Pour deux variables de type INTEGER, cela revient naturellement à comparer les valeurs en question :
if integer_a = integer_b then
io.put_string("Imprimer integer_a revient à imprimer integer_b !")
else
io.put_string("Les deux valeurs sont bien différentes.")
end
L'essentiel concernant les opérateurs prédéfinis = et /= est d'ores et déjà clairement expliqué, du moins nous l'espérons, grâce au texte qui précède. Si vous faites partie de la catégorie des lecteurs qui découvre les mécanismes de comparaison, continuez la lecture de ce texte en allant directement à la section sur is_equal. Sinon, la section suivante vous donnera toute l'information complémentaire concernant ces deux opérateurs prédéfinis.
Les opérateurs = et /=, en plus d'être des opérateurs prédéfinis, sont des opérateurs non redéfinissables. L'effet des opérateurs = et /= est complètement figé, et ceci à l'intérieur même du compilateur.
En ce qui concerne la comparaison de deux expressions de type référence avec les opérateurs = ou /=, comme cela a déjà été précisé avant, seules les deux adresses sont comparées. Il convient donc maintenant d'indiquer ce qui se passe lorsque les deux expressions comparées sont de type expanded.
Dans le cas d'une comparaison avec = de deux expressions expanded, la comparaison revient simplement à comparer, au premier niveau seulement, bit par bit, les deux zones mémoire correspondant aux deux objets. Naturellement, le type expanded utilisé à gauche de l'opérateur = doit être compatible avec le type expanded utilisé à droite. En fait, sauf pour quelques rares exceptions que nous allons détailler ensuite, une comparaison n'est acceptée que lorsque les deux types expanded sont exactement les mêmes. Il est bien évidemment possible de comparer entre elles deux expressions de type CHARACTER et, par exemple, de comparer entre elles, deux expressions de type INTEGER. Pour éviter toute erreur d'inattention il est interdit de comparer directement un CHARACTER avec un INTEGER. Notons que dans ce dernier cas, c'est au concepteur du programme de faire appel à la méthode de conversion appropriée. Les possibilités sont multiples, soit convertir l'expression INTEGER en CHARACTER, soit inversement, convertir l'expression CHARACTER en INTEGER. Dans les deux cas les méthodes de conversions possibles sont elles-mêmes nombreuses. Plusieurs méthodes de conversions existent par exemple pour passer de CHARACTER à INTEGER. En voici deux exemples parmi de nombreux autres :
if my_character.to_integer = my_integer then
io.put_string("Conversion avec possible extension de signe.")
end
ou encore :
if my_character.decimal_value = my_integer then
io.put_string("Conversion en utilisant la valeur du chiffre correspondant.")
end
Ainsi, il apparaît qu'il n'est absolument pas possible d'automatiser le choix de la conversion qu'il convient d'appliquer avant comparaison. Cela dépend complètement du contexte de la comparaison, du problème traité, et ce choix doit être fait manuellement par le concepteur du programme. La règle stricte, consistant à autoriser la comparaison de deux expanded lorsqu'ils ont exactement le même type est bien la plus simple et la plus sûre. Ainsi, si le compilateur refuse votre expression de comparaison, lisez attentivement le message d'erreur et choisissez soigneusement la conversion la plus appropriée. Ceci étant dit, probablement pour des raisons historiques, deux exceptions à cette règle très stricte subsistent.
Les deux exceptions concernent deux cas particuliers ou aucun doute n'est permis sachant en outre que les objets expanded concernés sont des objets très élémentaires. La première exception à la règle stricte d'identité des deux types expanded concerne le cas de deux objets pris dans l'ensemble {INTEGER_8, INTEGER_16, INTEGER_32, INTEGER, INTEGER_64}. La comparaison avec = est vraie si et seulement si les deux valeurs correspondent exactement à la même valeur entière. En effet, sans perte d'information aucune, il est toujours possible de représenter sur 16 bits un nombre qui est représenté sur 8 bits et ainsi de suite. Cette exception à la règle pure et dure nous permet ainsi d'écrire simplement et sans risque :
if my_integer_8 = my_integer_16 then ...
sachant que c'est comme si l'on avait écrit :
if my_integer_8.to_integer_16 = my_integer_16 then ...
La deuxième et dernière exception est similaire à la précédente et concerne l'ensemble des types expanded suivants : {REAL_32, REAL_64, REAL, REAL_80, REAL_128, REAL_EXTENDED}. Comme dans le cas des entiers et pour l'ensemble des nombres flottants en question, il est toujours possible de convertir un nombre flottant donné sur un plus grand nombre de bits sans aucune perte d'information. Ainsi, une expression de comparaison avec = ou /= peut mélanger deux types pris dans la famille des REAL_*.
Pour finir, notons qu'il est interdit de comparer directement entre eux, un objet de la famille des REAL_* avec un objet de la famille des INTEGER_*. Si vous êtes amené à devoir faire une telle comparaison, ce qui n'est pas illogique, c'est à vous de décider quelle est la méthode de conversion appropriée à votre traitement. Ce n'est pas une règle du langage Eiffel qui peut choisir pour vous.
La méthode redéfinissable is_equal permet de faire une comparaison moins élémentaire qu'avec l'opérateur = prédéfini. Considérons à nouveau notre exemple consistant à comparer deux variables de type POINT mais cette fois-ci en utilisant is_equal. Comme dans le cas précédent, supposons pour l'instant que les deux variables point_a et point_b sont effectivement initialisées avec autre chose que Void :
if point_a.is_equal(point_b) then
io.put_string("Il s'agit soit d'un unique POINT soit de 2 POINTs identiques.")
else
io.put_string("Il s'agit de deux POINTs différents.")
end
Dans une classe donnée, si la méthode is_equal n'a pas été redéfinie par l'utilisateur, le comportement de la méthode is_equal consiste à comparer deux à deux, tous les attributs des deux objets en présence. Pour l'exemple de la classe POINT, cela revient à comparer deux à deux, successivement les deux attributs x puis les deux attributs y. Autrement dit, tout se passe comme si on avait écrit :
if (point_a.x = point_b.x) and (point_a.y = point_b.y) then
io.put_string("Il s'agit soit d'un unique POINT soit de 2 POINTs identiques.")
else
io.put_string("Il s'agit de deux POINTs différents.")
end
Notons, que la comparaison entre attributs utilise non pas is_equal, mais l'opérateur élémentaire = figé. Le schéma qui suit présente une configuration mémoire possible du contenu de la mémoire lors de la comparaison de point_a et de point_b. Notons que dans le cas suivant, les deux POINTs situés à deux endroits de la mémoire sont identiques et que, par conséquent, la comparaison avec is_equal retournerait la valeur True :
Dans l'exemple qui précède, les deux objets que l'on compare sont deux objets de la classe POINT. Des objets très simples. Comme le montre le schéma, un objet de la classe POINT est composé de deux attributs expanded, plus exactement, les attributs x et y qui sont du type REAL, un type de base expanded (i.e. sans pointeur intermédiare). Dans ce cas la définition par défaut de la fonction is_equal convient parfaitement.
Avant de continuer plus avant dans la description de is_equal, signalons dès maintenant que l'utilisation de is_equal est également possible lorsque le type des deux objets à comparer est expanded. Par exemple en ce qui concerne les INTEGERs, le résultat d'une comparaison avec l'opérateur prédéfini = a exactement le même effet si l'on utilise la méthode is_equal. Notons néanmoins qu'il est assez inhabituel d'utiliser is_equal quand les deux opérandes sont expanded, à fortiori s'il s'agit d'un type expanded élémentaire comme INTEGER, CHARACTER, REAL ou encore BOOLEAN. En général, par habitude et surtout par souci de simplicité, on utilise uniquement = et /= pour les types expanded élémentaires.
Dès que les objets sont plus complexes, avec par exemple des attributs qui pointent eux-même vers d'autres objets, il faut faire attention au comportement prédéfini de la fonction is_equal. Considérons par exemple le cas de la comparaison de deux TRIANGLEs comme dans la figure suivante :
Les deux objets de la classe TRIANGLE de la figure précédente sont identiques mais si on procède à leur comparaison en utilisant la méthode is_equal prédéfinie le résultat sera ,False ! En fait, soulignons que triangle_a et triangle_b désignent, même s'ils sont semblables, deux objets disctints en mémoire. En effet, si la fonction is_equal n'a pas été redéfinie dans la classe TRIANGLE, la comparaison de triangle_a et de triangle_b avec is_equal, c'est à dire l'expression :
triangle_a.is_equal(triangle_b)
équivaut à l'expression :
(triangle_a.p1 = triangle_b.p1) and (triangle_a.p2 = triangle_b.p2) and (triangle_a.p3 = triangle_b.p3)
L'expression précédente correspond au comportement prédéfini de is_equal. Afin d'obtenir une fonction de comparaison plus utile, c'est le concepteur de la classe TRIANGLE lui même qui doit penser à changer la définition prédéfinie avec en redéfinissant la fonction is_equal de la classe TRIANGLE comme ceci :
is_equal (other: TRIANGLE): BOOLEAN is
do
Result := p1.is_equal(other.p1) and p2.is_equal(other.p2) and p3.is_equal(other.p3)
end
Notons que la définition automatique d'une bonne fonction de comparaison est très difficilement envisageable. En particulier, il ne suffit pas toujours, comme dans le cas de la classe TRIANGLE, de relancer is_equal entre les attributs plutôt que d'utiliser = (voir par exemple pour s'en convaincre le cas de la classe STRING ou encore le cas de la classe ARRAY). Ainsi, faute de savoir automatiser cette tâche par une règle du langage ou par l'ajout d'une forte dose d'intelligence dans le compilateur, c'est bien au seul concepteur d'une classe que revient cette charge. Le concepteur consciencieux d'une classe doit se préoccuper du bon comportement de la fonction is_equal pour les objets de la classe qu'il est en train de définir. Le comportement par défaut décrit précédemment à été choisit pour sa simplicité, parce qu'il convient assez souvent à une bonne fonction de comparaison et aussi parce qu'il s'implante aisément avec des instruction machine de bas niveau très rapide (instructions de comparaisons de deux blocs de mémoire).
Ne cherchez pas la définition des opérateurs de comparaison = et /= dans le code source Eiffel des classes car ces deux opérateurs sont prédéfinis et leur définition est volontairement figée à l'intérieur même du compilateur. Un des buts du langage Eiffel consistant à limiter les erreurs d'inattention, il ne serait pas raisonnable d'autoriser n'importe quelle comparaison (i.e. d'autoriser tous les mélanges comme par exemple la comparaison d'un CAMION avec une FLEUR pour prendre un cas extrême). Les types statiques des deux expressions que l'on compare doivent respecter certaines contraintes. Bien entendu, et c'est le cas le plus commun, quand les deux expressions sont de même type statique, la comparaison est évidemment autorisée. Il est toujours possible de comparer deux expressions ayant le même type statique à l'aide des opérateurs de comparaison = et /=.
Dès que les deux expressions ne sont pas de même type statique, il faut respecter la règle générale suivante. Une comparaison de la forme :
x = y
ou encore :
x /= y
est valide s'il est possible de faire une affectation dans le sens x vers y ou bien, s'il est possible de faire une affectation dans le sens y vers x. Plus exactement, car x et y ne sont pas forcément des variables, soit le type statique de x est acceptable pour une affectation vers le type statique de y soit le type statique de y est acceptable pour une affectation vers le type statique de x.
La règle de validité précédente contribue à vérifier que le concepteur du programme effectue bien une comparaison raisonable. Par l'expérience, nous avons constaté que cette règle guidée par le bon sens est cruciale pour la détection en amont d'erreurs qui sont souvent des erreurs d'inattention.
Dans quelques cas rares et souvent obscures cette règle peut être jugée trop contraignante. Si d'aventure vous vous trouvez face à une situation ou le compilateur rejette votre comparaison mais que vous pensez vraiment qu'il est souhaitable de comparer malgré tout les deux expressions vous pourrez facilement arrivez à vos fins en utilisant deux variables locales dont le type statique est plus général (utilisez éventuellement pour cela le type ANY).
Pour terminer sur cette règle générale de vérification des comparaisons avec les opérateurs = et /=, notons que la règle qui vient d'être énoncée englobe aussi le cas évoqué précédemment qui concerne la comparaison des expressions de type INTEGER_* ou REAL_*. En effet, il est par exemple possible d'affecter une expression de type INTEGER_16 dans une variable de type INTEGER_32. La comparaison entre une expression de type INTEGER_16 avec une expression de type INTEGER_32 est donc valide.
Seulement pour être exhaustif dans cette partie consacrée à la comparaison d'objets, signalons enfin que la classe ANY offre une possibilité de comparaison en profondeur de deux objets : la fonction is_deep_equal. Les attributs des deux objets à comparer sont eux mêmes comparés deux à deux avec la fonction is_deep_equal et ainsi de suite récursivement. Cette fonction compare récursivement que les deux objets sont structurellement identiques. Notons qu'une circularité dans le graphe des objets est parfois possible comme par exemple dans le cas de la comparaison de deux listes chaînées structurellement circulaires. Pour être considérés identiques au sens is_deep_equal, le graphe des deux objets à comparer doit être superposable.
La définition de la fonction is_deep_equal est figée (frozen) dans la classe ANY. La fonction is_deep_equal doit être utilisée avec précautions car elle dépend totalement de l'implantation des objets qu'elle compare. En fait, abstraction oblige, il est toujours préférable de ne pas utiliser la fonction is_deep_equal.







