Le bug héréditaire
Par amethyste le Jan 16, 2005 | Dans focus | 6 retours »
Vous savez tous que C# permet la surcharge de certains opérateurs et en particulier ==.
Vous êtes vous, par contre, demandé pourquoi Microsoft fournit un exemple pour l'opérateur + (plus), mais pas ==? Même Eric Gunnerson dans son livre ne s'y risque pas!
La raison (selon moi) est qu'en fait on ne devrait pas surcharger == en C#.
Alors avant de faire des réponses vengeresses mettant en doute mes capacités mentales, laissez moi expliquer quel est le sens précis de cette affirmation.
Les exemples de surcharge sont nombreux dans les forums.
Ils ont toutefois (presque) tous un point commun:
Ils ne fonctionnent pas!
Si, si, ils ne fonctionnent pas.
Voici l'exemple canonique trouvé sur le site C# Help:
public class Score : IComparable
{
int value;
public Score (int score) {
value = score;
}
public static bool operator == (Score x, Score y) {
return x.value == y.value;
}
public static bool operator != (Score x, Score y) {
return x.value != y.value;
}
public int CompareTo (object o) {
return value - ((Score)o).value;
}
}
Essayez d'exécuter le code suivant apparemment bien innocent:
Score a = null;
if (a == null)
{
// on arrive jamais ici
}
Vous obtenez un NullReferenceException!
La ligne qui pose problème est évidemment:
return x.value == y.value;
Même des auteurs pourtant de bonne famille reprennent religieusement ce bug. Au hasard, Patrick Smacchia nous propose page 357 de son livre Pratique de .NET et C# l'exemple suivant:
public class Distance
{
public double m_Mesure = 0.0;
public Distance(double d)
{
m_Mesure = d;
}
public override bool Equals(object obj)
{
Distance D = (Distance) obj;
return m_Mesure == D.m_Mesure;
}
public static bool operator == (Distance d1,Distance d2)
{
return d1.Equals(d2);
}
public static bool operator != (Distance d1,Distance d2)
{
return ! d1.Equals(d2);
}
public override int GetHashCode()
{
return this.ToString().GetHashCode();
}
}
Qui échoue pour exactement les même raisons.
Evidemment on pourrait mettre un try..catch.
Mais c'est une solution extrêmement mauvaise tant au niveau du design que des performances.
Après être tombé sur ce bug dans une de mes applis j'ai fais pas mal de recherches sur Internet et je peux vous assurer qu'au moins 99% des exemples cités ne fonctionnent pas pour cette raison précise. Personne ne teste ou quoi?
99% ce n'est pas 100%. Qui est le 1% restant?
Le voici, je l'ai trouvé!
http://blogs.msdn.com/santoshz/archive/2004/06/01/145542.aspx
Vous trouverez en prime une discussion sur la façon d'optimiser Equals().
Pour faire simple. Si on veut éviter la levée de l'exception on doit tester si les paramètres de part et d'autre de == ou != sont nulls.
Le problème est de faire cela sans déclencher une boucle infinie. Essayez sur un des exemples précédents pour vous convaincre que ce n'est pas si trivial.
Le truc, si je peux dire, est de faire le test avec la méthode statique Equals de Object. Par exemple on pourrait écrire:
return Object.Equals(d1,d2);
NOTE:
On ne teste jamais trop disais-je!
Justement pris d'un doute j'ai revérifié mon code de test et découvert une erreur dedans qui invalide une partie de ma conclusion!!!
Contrairement à ce que j'avais initialement cru, réécrire l'exemple de Patrick Smacchia ainsi marche parfaitement bien:
On peut tester avec:
Console.WriteLine(b == c);
Console.WriteLine(b == null);
Console.WriteLine(Object.Equals(b,null));
Ceci étant, soyez prudent tout de même avec les surcharges d'opérateurs. Et lisez aussi ce blog de Bertrand Leroy qui pointe sur une autre raison, quoique subtile, qui fait que certains codes trouvés dans les forums ne sont pas corrects.
D'une façon générale, surcharger == n'est probablement pas une bonne idée si on a besoin d'un code fiable et à l'épreuve de toute maintenance future. Je garde au moins cela de ma conclusion.
Autrement, comme le fait remarquer Léon Andrianarivony en commentaire, il n'y a pas ce genre de problème avec les ValueType, par exemple les structures.
public class Distance
{
public double m_Mesure = 0.0;
public Distance(double d)
{
m_Mesure = d;
}
public override bool Equals(object obj)
{
Distance D = (Distance) obj;
return m_Mesure == D.m_Mesure;
}
public static bool operator == (Distance d1,Distance d2)
{
return Object.Equals(d1,d2);
}
public static bool operator != (Distance d1,Distance d2)
{
return ! Object.Equals(d1,d2);
}
public override int GetHashCode()
{
return this.ToString().GetHashCode();
}
}
Mais attention est-ce vraiment correct?
Après tout ce que teste Equals, c'est le fait que deux références sont égales. Or relisez bien le code de Patrick Smacchia.
La définition de l'égalité entre deux distances est: la mesure de la distance est la même.
Ce qui n'est pas la même chose. Equals() teste l'égalité de deux références tandis que nous souhaitons tester l'égalité de deux valeurs.
Le problème pour nous est que les valeurs ne peuvent être null d'une part et qu'en outre, la classe portant ces valeurs peut par contre être nulle en étant pas instanciée.
Le résultat est qu'il n'est alors pas très clair de savoir ce que l'on teste en écrivant:
if (x == null)
Vous imaginez facilement les bugs extravagants que peut impliquer un manque de précision sur ce que réalise un opérateur aussi basique.
La surcharge des opérateurs tels ==, != pose donc rapidement un problème de logique et de complexité.
Je crois qu'en pratique il est préférable de ne pas les surcharger, mais à la place créer une méthode d'instance qui teste l'égalité, l'inégalité....
Cette fois les choses sont claires. On utilise == pour tester l'égalité de deux références et la nouvelle méthode pour tester l'égalité des deux valeurs.
On revient alors à un comportement plus standard.
C'est d'ailleurs ce que fait MSIL en interne.
public override bool Equals(object obj)
{
Distance D = (Distance) obj;
return m_Mesure == D.m_Mesure;
}
public static bool operator == (Distance d1,Distance d2)
{
return d1.Equals(d2);
}
Moralité de cette histoire:
On ne teste jamais trop. (Conseil d'expert!)
17: 15/01/2005
Améthyste
6 commentaires
1. Avec un type ValueType, il n'y a pas de problème !
2. Avec un type "Class", il faut tjs passer par Type.ReferenceEquals(x, y) pour la surchage. Si elle renvoie true, il ne faut plus aller plus loin ...
Il y a en prime un quizz dans lequel il n'y a rien à gagner!
ce bug sera corrigé pour Pratique2
c vrai que le sujet est délicat entre
les opérateurs == !=
le méthode Equals +ReferenceEquals
les IComparer IComparable + version générique
la comparaison par valeur de hachage
sur ce coup là il aurait pu mieux 'unifier' tout ca
Je partage l'avis : "On ne teste jamais trop. (Conseil d'expert!)"
Il vaudrait mieux faire:
public override bool Equals(object obj)
{
Distance D = obj as Distance;
if (D == null) return false;
return m_Mesure == D.m_Mesure;
}
Non?
entre temps j'ai trouvé un deuxième auteur qui sait faire des surcharges qui marchent de ==:
Jeffrey Richter dans son livre sur C#.
Amethyste
Laisser un commentaire
| « Petit retour sur la sécurité | Classe pour faciliter les tests unitaires » |