Aller au contenu

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 Equals proposée par la classe Object.

  • La méthode statique ReferenceEquals proposé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 :

var customer1 = new Customer(1, "James");
var customer2 = new Customer(1, "James");

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 :

Console.WriteLine(customer1 == customer2);

Le résultat obtenu est :

False

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 :

var customer3 = customer1;

Console.WriteLine(customer1 == customer3);

Résultat :

True

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 :

False
True

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 :

int number1 = 10;
int number2 = 10;

Console.WriteLine(ReferenceEquals(number1, number2));

Le résultat est :

False

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 :

Console.WriteLine(customer1.Equals(customer2));

Retourne :

False

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 :

public override int GetHashCode()
{
    return Id.GetHashCode();
}

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 :

Console.WriteLine(ReferenceEquals(customer1, customer2));

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 :

string text1 = new(['h', 'e', 'l', 'l', 'o']);

string text2 = new(['h', 'e', 'l', 'l', 'o']);

Comparons-les :

Console.WriteLine(text1 == text2);

Console.WriteLine(text1.Equals(text2));

Console.WriteLine(ReferenceEquals(text1, text2));

Résultats :

True
True
False

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 :

string text1 = "hello";

string text2 = "hello";

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 :

object.Equals(customer1, customer2);

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 :

True

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 Equals du 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 !=.