Comprendre les différences entre l'opérateur d'égalité, Equals et ReferenceEquals dans les comparaisons
Introduction
Comparer deux objets paraît être une opération simple. Et pourtant, le langage C# propose plusieurs mécanismes de comparaison, dont le comportement peut varier selon le type manipulé, et la manière dont l’égalité a été implémentée. Les éléments suivants peuvent notamment être utilisés :
-
L’opérateur
== -
La méthode
Equalsproposée par la classeObject. -
La méthode statique
ReferenceEqualsproposée par la classeObject.
Ces mécanismes de comparaison n'ont pas le même comportement. Certains vérifient si deux variables désignent la même instance, tandis que d’autres déterminent si deux objets doivent être considérés comme égaux en fonction de la valeur de leurs propriétés. Dans cet article, nous étudierons leurs différences et apprendrons à définir correctement l’égalité entre des objets.
Égalité de référence et égalité de valeurs
Dans un premier temps deux formes d’égalité doivent être distinguées :
-
L’égalité de référence, qui permet de vérifier si deux variables désignent exactement la même instance en mémoire.
-
L’égalité de valeurs, aussi appelée égalité logique, qui permet de déterminer si deux objets différents doivent être considérés comme égaux, parce qu’ils représentent la même donnée.
Prenons la classe Customer suivante :
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Customer(int id, string name)
{
Id = id;
Name = name;
}
}
Créons deux instances contenant les mêmes valeurs :
Bien que leurs propriétés aient des valeurs qui sont identiques, customer1 et customer2 sont deux objets distincts.
Comparer des objets avec l’opérateur ==
Pour les types référence, l’opérateur == compare par défaut les références des objets :
Le résultat obtenu est :
Les deux variables ne désignent pas la même instance, même si les objets contiennent les mêmes valeurs. En revanche, si une deuxième variable référence le même objet, la comparaison retourne la valeur true :
Résultat :
Le comportement de l’opérateur == peut toutefois être redéfini par une classe. Il ne faut donc pas en déduire qu’il compare systématiquement les références.
Comparer les références avec la méthode ReferenceEquals
La méthode statique ReferenceEquals de la classe Object détermine si deux variables référencent la même instance :
Console.WriteLine(ReferenceEquals(customer1, customer2));
Console.WriteLine(ReferenceEquals(customer1, customer3));
Résultats :
Contrairement à l’opérateur ==, le comportement de la méthode ReferenceEquals ne peut pas être redéfini. Cette méthode répond uniquement à la question suivante : "les deux variables référencent-elles le même objet ?". Elle ne tient pas compte des états des objets.
Cas particulier des types valeur
La méthode ReferenceEquals n’est pas adaptée à la comparaison des types valeur. Prenons l’exemple suivant :
Le résultat est :
Les deux valeurs sont placées dans des objets distincts au cours d’une opération appelée boxing. La méthode compare alors les références de ces deux objets temporaires.
Pour comparer des types valeur, il convient d’utiliser == lorsqu’il est disponible ou la méthode Equals.
Comparer des objets avec la méthode Equals
La méthode Equals implémentée dans la classe Object permet de définir une égalité logique entre les objets. Avec notre classe Customer, cette instruction :
Retourne :
La classe Customer ne redéfinit pas encore cette méthode. Elle hérite donc de l’implémentation située dans la classe Object, qui compare les références pour les classes. Pour obtenir une égalité basée sur l’identité du client, nous pouvons redéfinir la méthode Equals.
Redéfinir la méthode Equals
Supposons que deux clients soient considérés comme égaux lorsqu’ils ont le même identifiant :
public class Customer
{
public int Id { get; init; }
public string Name { get; init; }
public Customer(int id, string name)
{
Id = id;
Name = name;
}
public override bool Equals(object? obj)
{
return obj is Customer customer && Id == customer.Id;
}
}
La comparaison retourne maintenant la valeur true :
var customer1 = new Customer(1, "James");
var customer2 = new Customer(1, "James RAVAILLE");
Console.WriteLine(customer1.Equals(customer2));
Même si les noms sont différents, les objets représentent le même client selon la règle d’égalité implémentée dans la méthode Equals.
Redéfinir également la méthode GetHashCode
Lorsqu’une classe redéfinit la méthode Equals, il est important de redéfinir la méthode GetHashCode, aussi définie comme étant virtuelle dans la classe Object, car deux objets considérés comme égaux doivent obligatoirement avoir le même hashcode :
Cette cohérence est indispensable lorsque les objets sont utilisés dans des collections reposant sur un hachage, telles que Dictionary<TKey, TValue>, HashSet<T> ou les méthodes Linq comme Distinct. L’implémentation complète devient :
public sealed class Customer
{
public int Id { get; init; }
public string Name { get; init; }
public Customer(int id, string name)
{
Id = id;
Name = name;
}
public override bool Equals(object? obj)
{
return obj is Customer customer
&& Id == customer.Id;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
}
Les propriétés participant au calcul du hashcode ne devraient pas être modifiées lorsque les objets sont utilisés en tant que clés d’un dictionnaire ou comme élément d’un HashSet.
Implémenter l'interface IEquatable<T>
L’interface IEquatable<T> permet de proposer une comparaison fortement typée :
public sealed class Customer : IEquatable<Customer>
{
public int Id { get; init; }
public string Name { get; init; }
public Customer(int id, string name)
{
Id = id;
Name = name;
}
public bool Equals(Customer? other)
{
return other is not null && Id == other.Id;
}
public override bool Equals(object? obj)
{
return obj is Customer customer && Equals(customer);
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
}
Cette interface évite notamment de passer systématiquement par une référence de type object. Elle est pertinente pour les types utilisés fréquemment dans des collections génériques.
Redéfinir les opérateurs == et !=
La méthode Equals et l’opérateur == restent indépendants. Dans l’implémentation précédente, la méthode Equals compare les identifiants, mais l'opérateur == continue de comparer les références. Pour rendre leur comportement cohérent, nous pouvons redéfinir les opérateurs == et != :
public sealed class Customer : IEquatable<Customer>
{
public int Id { get; init; }
public string Name { get; init; }
public Customer(int id, string name)
{
Id = id;
Name = name;
}
public bool Equals(Customer? other)
{
return other is not null && Id == other.Id;
}
public override bool Equals(object? obj)
{
return obj is Customer customer && Equals(customer);
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
public static bool operator ==(Customer? left, Customer? right)
{
if (ReferenceEquals(left, right))
return true;
if (left is null || right is null)
return false;
return left.Equals(right);
}
public static bool operator !=(Customer? left, Customer? right)
{
return !(left == right);
}
}
Les comparaisons suivantes retournent désormais la valeur true, car la règle d’égalité implémentée repose uniquement sur la propriété Id, et ne tient compte de la propriété Name :
var customer1 = new Customer(1, "James");
var customer2 = new Customer(1, "James RAVAILLE");
Console.WriteLine(customer1.Equals(customer2));
Console.WriteLine(customer1 == customer2);
Et la méthode ReferenceEquals retourne la valeur false, car il s’agit bien de deux instances différentes :
Le comportement particulier des chaînes de caractères
Le type string est un type référence, mais son opérateur == et sa méthode Equals comparent le contenu des chaînes. Créons explicitement deux instances distinctes contenant le même texte :
Comparons-les :
Console.WriteLine(text1 == text2);
Console.WriteLine(text1.Equals(text2));
Console.WriteLine(ReferenceEquals(text1, text2));
Résultats :
L'opérateur == et la méthode Equals retournent la valeur true, car les deux chaînes contiennent les mêmes caractères. La méthode ReferenceEquals retourne la valeur false, car elles ont été créées comme deux instances distinctes. Et si nous écrivons :
Les trois comparaisons précédentes retournent la valeur true. L'opérateur == et la méthode Equals indiquent que les chaînes ont le même contenu. La méthode ReferenceEquals indique que les variables text1 et text2 désignent la même instance. La méthode ReferenceEquals ne compare donc toujours pas le contenu des chaînes de caractères ; elle vérifie leur identité. C’est toute la différence entre égalité des valeurs et identité des objets.
La méthode statique Object.Equals
La classe Object propose également une méthode statique permettant de comparer deux objets :
Cette méthode présente notamment l’avantage de gérer les valeurs null :
Customer? customer1 = null;
Customer? customer2 = null;
Console.WriteLine(object.Equals(customer1, customer2));
Résultat :
Son comportement peut être résumé ainsi :
-
Si les deux références sont
null, alors elle retourne la valeur true. -
Si une seule référence est
null, alors elle retourne la valeur false. -
Si les deux références désignent la même instance, elle retourne directement
true. -
Sinon, elle appelle la méthode d’instance
Equalsdu premier objet et passe le second objet en paramètre.
Conclusion
L’opérateur ==, la méthode d’instance Equals et la méthode statique ReferenceEquals ne sont pas interchangeables.
La méthode ReferenceEquals vérifie si deux variables désignent la même instance. La méthode Equals représente l’égalité logique définie par le type (étant polymorphe, elle peut être redéfinie). L’opérateur == compare les références ou les valeurs selon les types des variables.
Lorsqu’une classe doit proposer une égalité basée sur ses données métier, il est nécessaire de désigner les propriétés participant à cette égalité et de maintenir une cohérence entre les méthodes polymorphes Equals, GetHashCode, et éventuellement les opérateurs == et !=.