Note
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier les répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de changer de répertoire.
Conseil / Astuce
Envisagez d'abord d'utiliser des enregistrements. Les enregistrements implémentent automatiquement l’égalité des valeurs avec du code minimal, ce qui en fait l’approche recommandée pour la plupart des types axés sur les données. Si vous avez besoin d’une logique d’égalité de valeur personnalisée ou que vous ne pouvez pas utiliser d’enregistrements, suivez les étapes d’implémentation manuelles ci-dessous.
Quand vous définissez une classe ou un struct, vous décidez s’il est judicieux de créer une définition personnalisée de l’égalité des valeurs (ou équivalence) pour le type. En règle générale, vous implémentez l’égalité des valeurs lorsque vous prévoyez d’ajouter des objets de ce type à une collection, ou lorsque leur objectif principal est de stocker un ensemble de champs ou de propriétés. Vous pouvez baser votre définition de l’égalité des valeurs sur une comparaison de tous les champs et propriétés du type, ou vous pouvez la baser sur un sous-ensemble.
Dans les deux cas, et tant dans les classes que dans les structs, votre implémentation doit suivre les cinq garanties d’équivalence (pour les règles suivantes, supposons que x, y et z ne sont pas null) :
La propriété réflexive :
x.Equals(x)renvoietrue.La propriété symétrique :
x.Equals(y)renvoie la même valeur quey.Equals(x).La propriété transitive : si
(x.Equals(y) && y.Equals(z))renvoietrue, alorsx.Equals(z)renvoietrue.Les invocations successives de
x.Equals(y)renvoient la même valeur tant que les objets référencés par x et y ne sont pas modifiés.Toute valeur non null n’est pas égale à null. Toutefois,
x.Equals(y)lève une exception lorsquexa la valeur null. Cela enfreint les règles 1 ou 2, selon l’argument deEquals.
Tout struct que vous définissez a déjà une implémentation par défaut de l’égalité des valeurs dont il hérite de la substitution System.ValueType de la méthode Object.Equals(Object). Cette implémentation utilise la réflexion pour examiner tous les champs et propriétés du type. Bien que cette implémentation produise des résultats corrects, elle est relativement lente par rapport à une implémentation personnalisée que vous écrivez spécifiquement pour le type.
Les détails d’implémentation pour l’égalité des valeurs sont différents pour les classes et les structs. Toutefois, les classes et les structs nécessitent tous deux les mêmes étapes de base pour l’implémentation de l’égalité :
Substituez la méthode virtuelleObject.Equals(Object). Cela fournit un comportement d’égalité polymorphe, permettant à vos objets d’être comparés correctement lorsqu’ils sont traités comme des
objectréférences. Il garantit un comportement approprié dans les collections et lors de l’utilisation de polymorphisme. Dans la plupart des cas, votre implémentation debool Equals( object obj )doit simplement appeler la méthodeEqualspropre au type qui est l’implémentation de l’interface System.IEquatable<T>. (Voir l’étape 2.)Implémentez l’interface System.IEquatable<T> en fournissant une méthode
Equalspropre au type. Cela permet de vérifier l’égalité de type sans boxer, ce qui améliore les performances. Il évite également la conversion inutile et active la vérification du type au moment de la compilation. C’est ici qu’est effectuée la comparaison d’équivalence. Par exemple, vous pouvez décider de définir l’égalité en comparant seulement un ou deux champs de votre type. Ne levez pas d’exceptions à partir deEquals. Pour les classes associées par héritage :cette méthode doit examiner uniquement les champs qui sont déclarés dans la classe. Elle doit appeler
base.Equalspour examiner les champs qui sont dans la classe de base. (N’appelez pasbase.Equalssi le type hérite directement de Object, car Object l’implémentation de Object.Equals(Object) effectue une vérification de l’égalité des références)Deux variables doivent être considérées comme égales uniquement si les types d’exécution des variables comparées sont identiques. Vérifiez également que
IEquatablel’implémentation de laEqualsméthode pour le type d’exécution est utilisée si les types d’exécution et de compilation d’une variable sont différents. Une stratégie pour s’assurer que les types d’exécution sont toujours comparés correctement consiste à implémenterIEquatableuniquement danssealedles classes. Pour plus d’informations, consultez l’exemple de classe plus loin dans cet article.
Facultatif, mais recommandé : Surchargez les opérateurs == et ! =. Cela fournit une syntaxe cohérente et intuitive pour les comparaisons d’égalité, qui répond aux attentes des utilisateurs des types intégrés. Il garantit que
obj1 == obj2etobj1.Equals(obj2)se comportent de la même façon.Substituez Object.GetHashCode pour que deux objets ayant une égalité des valeurs produisent le même code de hachage. Cela est nécessaire pour un comportement correct dans les collections basées sur le hachage comme
Dictionary<TKey,TValue>etHashSet<T>. Les objets égaux doivent avoir des codes de hachage égaux, ou ces collections ne fonctionnent pas correctement.Facultatif : Pour prendre en charge les définitions de « supérieur à » ou « inférieur à », implémentez l’interface IComparable<T> de votre type, et surchargez également les opérateurs <= et >=. Cela active les opérations de tri et fournit une relation de classement complète pour votre type, utile lors de l’ajout d’objets à des collections triées ou lors du tri de tableaux ou de listes.
Exemple d’enregistrement
L’exemple suivant montre comment les enregistrements implémentent automatiquement l’égalité des valeurs avec un code minimal. Le premier enregistrement TwoDPoint est un type d’enregistrement simple qui implémente automatiquement l’égalité des valeurs. Le deuxième enregistrement ThreeDPoint montre que les enregistrements peuvent être dérivés d’autres enregistrements et conservent toujours un comportement d’égalité des valeurs approprié :
namespace ValueEqualityRecord;
public record TwoDPoint(int X, int Y);
public record ThreeDPoint(int X, int Y, int Z) : TwoDPoint(X, Y);
class Program
{
static void Main(string[] args)
{
// Create some points
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
TwoDPoint pointC = new TwoDPoint(5, 6);
ThreeDPoint point3D_A = new ThreeDPoint(3, 4, 5);
ThreeDPoint point3D_B = new ThreeDPoint(3, 4, 5);
ThreeDPoint point3D_C = new ThreeDPoint(3, 4, 7);
Console.WriteLine("=== Value Equality with Records ===");
// Value equality works automatically
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}"); // True
Console.WriteLine($"pointA == pointB = {pointA == pointB}"); // True
Console.WriteLine($"pointA.Equals(pointC) = {pointA.Equals(pointC)}"); // False
Console.WriteLine($"pointA == pointC = {pointA == pointC}"); // False
Console.WriteLine("\n=== Hash Codes ===");
// Equal objects have equal hash codes automatically
Console.WriteLine($"pointA.GetHashCode() = {pointA.GetHashCode()}");
Console.WriteLine($"pointB.GetHashCode() = {pointB.GetHashCode()}");
Console.WriteLine($"pointC.GetHashCode() = {pointC.GetHashCode()}");
Console.WriteLine("\n=== Inheritance with Records ===");
// Inheritance works correctly with value equality
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
Console.WriteLine($"point3D_A == point3D_B = {point3D_A == point3D_B}"); // True
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
// Different types are not equal (unlike problematic class example)
Console.WriteLine($"pointA.Equals(point3D_A) = {pointA.Equals(point3D_A)}"); // False
Console.WriteLine("\n=== Collections ===");
// Works seamlessly with collections
var pointSet = new HashSet<TwoDPoint> { pointA, pointB, pointC };
Console.WriteLine($"Set contains {pointSet.Count} unique points"); // 2 unique points
var pointDict = new Dictionary<TwoDPoint, string>
{
{ pointA, "First point" },
{ pointC, "Different point" }
};
// Demonstrate that equivalent points work as the same key
var duplicatePoint = new TwoDPoint(3, 4);
Console.WriteLine($"Dictionary contains key for {duplicatePoint}: {pointDict.ContainsKey(duplicatePoint)}"); // True
Console.WriteLine($"Dictionary contains {pointDict.Count} entries"); // 2 entries
Console.WriteLine("\n=== String Representation ===");
// Automatic ToString implementation
Console.WriteLine($"pointA.ToString() = {pointA}");
Console.WriteLine($"point3D_A.ToString() = {point3D_A}");
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Expected Output:
=== Value Equality with Records ===
pointA.Equals(pointB) = True
pointA == pointB = True
pointA.Equals(pointC) = False
pointA == pointC = False
=== Hash Codes ===
pointA.GetHashCode() = -1400834708
pointB.GetHashCode() = -1400834708
pointC.GetHashCode() = -148136000
=== Inheritance with Records ===
point3D_A.Equals(point3D_B) = True
point3D_A == point3D_B = True
point3D_A.Equals(point3D_C) = False
pointA.Equals(point3D_A) = False
=== Collections ===
Set contains 2 unique points
Dictionary contains key for TwoDPoint { X = 3, Y = 4 }: True
Dictionary contains 2 entries
=== String Representation ===
pointA.ToString() = TwoDPoint { X = 3, Y = 4 }
point3D_A.ToString() = ThreeDPoint { X = 3, Y = 4, Z = 5 }
*/
Les enregistrements offrent plusieurs avantages pour l’égalité des valeurs :
- Implémentation automatique : les enregistrements implémentent automatiquement et remplacent System.IEquatable<T>, Object.Equals, ainsi que les opérateurs Object.GetHashCode
==/. -
Comportement d’héritage correct : les enregistrements implémentent à
IEquatable<T>l’aide de méthodes virtuelles qui vérifient le type d’exécution des deux opérandes, ce qui garantit un comportement correct dans les hiérarchies d’héritage et les scénarios polymorphes. - Immuabilité par défaut : les enregistrements encouragent la conception immuable, qui fonctionne bien avec la sémantique d’égalité des valeurs.
- Syntaxe concise : les paramètres positionnels offrent un moyen compact de définir des types de données.
- Meilleures performances : l’implémentation d’égalité générée par le compilateur est optimisée et n’utilise pas de réflexion comme l’implémentation de struct par défaut.
Utilisez des enregistrements lorsque votre objectif principal est de stocker des données et que vous avez besoin d’une sémantique d’égalité des valeurs.
Enregistrements avec des membres qui utilisent l’égalité de référence
Lorsque les enregistrements contiennent des membres qui utilisent l’égalité de référence, le comportement d’égalité automatique des valeurs des enregistrements ne fonctionne pas comme prévu. Cela s’applique aux collections telles que System.Collections.Generic.List<T>, les tableaux et d’autres types de référence qui n’implémentent pas l’égalité basée sur la valeur (à l’exception notable de , qui implémente l’égalité des System.Stringvaleurs).
Important
Bien que les enregistrements fournissent une excellente égalité de valeur pour les types de données de base, ils ne résolvent pas automatiquement l’égalité des valeurs pour les membres qui utilisent l’égalité de référence. Si un enregistrement contient un System.Collections.Generic.List<T>, System.Arrayou d’autres types de référence qui n’implémentent pas l’égalité des valeurs, deux instances d’enregistrement avec du contenu identique dans ces membres ne sont toujours pas égales, car les membres utilisent l’égalité de référence.
public record PersonWithHobbies(string Name, List<string> Hobbies);
var person1 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
var person2 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
Console.WriteLine(person1.Equals(person2)); // False - different List instances!
Cela est dû au fait que les enregistrements utilisent la Object.Equals méthode de chaque membre et que les types de collection utilisent généralement l’égalité de référence plutôt que de comparer leur contenu.
Voici le problème :
// Records with reference-equality members don't work as expected
public record PersonWithHobbies(string Name, List<string> Hobbies);
Voici comment cela se comporte lorsque vous exécutez le code :
Console.WriteLine("=== Records with Collections - The Problem ===");
// Problem: Records with mutable collections use reference equality for the collection
var person1 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
var person2 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
Console.WriteLine($"person1: {person1}");
Console.WriteLine($"person2: {person2}");
Console.WriteLine($"person1.Equals(person2): {person1.Equals(person2)}"); // False! Different List instances
Console.WriteLine($"Lists have same content: {person1.Hobbies.SequenceEqual(person2.Hobbies)}"); // True
Console.WriteLine();
Solutions pour les enregistrements avec des membres d’égalité de référence
Implémentation personnalisée System.IEquatable<T>: remplacez l’égalité générée par le compilateur par une version codée manuellement qui fournit une comparaison basée sur le contenu pour les membres d’égalité de référence. Pour les collections, implémentez la comparaison d’éléments par élément à l’aide Enumerable.SequenceEqual de méthodes similaires ou similaires.
Utilisez des types valeur dans la mesure du possible : déterminez si vos données peuvent être représentées avec des types valeur ou des structures immuables qui prennent naturellement en charge l’égalité des valeurs, comme System.Numerics.Vector<T> ou Plane.
Utilisez des types avec égalité basée sur la valeur : pour les regroupements, envisagez d’utiliser des types qui implémentent l’égalité basée sur la valeur ou implémentent des types de collection personnalisés qui remplacent Object.Equals pour fournir une comparaison basée sur le contenu, par System.Collections.Immutable.ImmutableArray<T>System.Collections.Immutable.ImmutableList<T>exemple .
Conception avec égalité de référence à l’esprit : acceptez que certains membres utilisent l’égalité des références et concevoir votre logique d’application en conséquence, en vous assurant que vous réutilisez les mêmes instances lorsque l’égalité est importante.
Voici un exemple d’implémentation de l’égalité personnalisée pour les enregistrements avec des collections :
// A potential solution using IEquatable<T> with custom equality
public record PersonWithHobbiesFixed(string Name, List<string> Hobbies) : IEquatable<PersonWithHobbiesFixed>
{
public virtual bool Equals(PersonWithHobbiesFixed? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
// Use SequenceEqual for List comparison
return Name == other.Name && Hobbies.SequenceEqual(other.Hobbies);
}
public override int GetHashCode()
{
// Create hash based on content, not reference
var hashCode = new HashCode();
hashCode.Add(Name);
foreach (var hobby in Hobbies)
{
hashCode.Add(hobby);
}
return hashCode.ToHashCode();
}
}
Cette implémentation personnalisée fonctionne correctement :
Console.WriteLine("=== Solution 1: Custom IEquatable Implementation ===");
var personFixed1 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
var personFixed2 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
Console.WriteLine($"personFixed1: {personFixed1}");
Console.WriteLine($"personFixed2: {personFixed2}");
Console.WriteLine($"personFixed1.Equals(personFixed2): {personFixed1.Equals(personFixed2)}"); // True! Custom equality
Console.WriteLine();
Le même problème affecte les tableaux et d’autres types de collection :
// These also use reference equality - the issue persists
public record PersonWithHobbiesArray(string Name, string[] Hobbies);
public record PersonWithHobbiesImmutable(string Name, IReadOnlyList<string> Hobbies);
Les tableaux utilisent également l’égalité des références, produisant les mêmes résultats inattendus :
Console.WriteLine("=== Arrays Also Use Reference Equality ===");
var personArray1 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
var personArray2 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
Console.WriteLine($"personArray1: {personArray1}");
Console.WriteLine($"personArray2: {personArray2}");
Console.WriteLine($"personArray1.Equals(personArray2): {personArray1.Equals(personArray2)}"); // False! Arrays use reference equality too
Console.WriteLine($"Arrays have same content: {personArray1.Hobbies.SequenceEqual(personArray2.Hobbies)}"); // True
Console.WriteLine();
Même les collections en lecture seule présentent ce comportement d’égalité de référence :
Console.WriteLine("=== Same Issue with IReadOnlyList ===");
var personImmutable1 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
var personImmutable2 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
Console.WriteLine($"personImmutable1: {personImmutable1}");
Console.WriteLine($"personImmutable2: {personImmutable2}");
Console.WriteLine($"personImmutable1.Equals(personImmutable2): {personImmutable1.Equals(personImmutable2)}"); // False! Reference equality
Console.WriteLine($"Content is the same: {personImmutable1.Hobbies.SequenceEqual(personImmutable2.Hobbies)}"); // True
Console.WriteLine();
L’insight clé est que les enregistrements résolvent le problème d’égalité structurelle , mais ne modifient pas le comportement d’égalité sémantique des types qu’ils contiennent.
Exemple de classe
L’exemple suivant montre comment implémenter l’égalité des valeurs dans une classe (type référence). Cette approche manuelle est nécessaire lorsque vous ne pouvez pas utiliser d’enregistrements ou avoir besoin d’une logique d’égalité personnalisée :
namespace ValueEqualityClass;
class TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.X = x;
this.Y = y;
}
public override bool Equals(object obj) => this.Equals(obj as TwoDPoint);
public bool Equals(TwoDPoint p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// If run-time types are not exactly the same, return false.
if (this.GetType() != p.GetType())
{
return false;
}
// Return true if the fields match.
// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (X == p.X) && (Y == p.Y);
}
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
{
if (lhs is null)
{
if (rhs is null)
{
return true;
}
// Only the left side is null.
return false;
}
// Equals handles case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
public int Z { get; private set; }
public ThreeDPoint(int x, int y, int z)
: base(x, y)
{
if ((z < 1) || (z > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.Z = z;
}
public override bool Equals(object obj) => this.Equals(obj as ThreeDPoint);
public bool Equals(ThreeDPoint p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// Check properties that this class declares.
if (Z == p.Z)
{
// Let base class check its own fields
// and do the run-time type comparison.
return base.Equals((TwoDPoint)p);
}
else
{
return false;
}
}
public override int GetHashCode() => (X, Y, Z).GetHashCode();
public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
{
if (lhs is null)
{
if (rhs is null)
{
// null == null = true.
return true;
}
// Only the left side is null.
return false;
}
// Equals handles the case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs);
}
class Program
{
static void Main(string[] args)
{
ThreeDPoint pointA = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointB = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointC = null;
int i = 5;
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}");
Console.WriteLine($"pointA == pointB = {pointA == pointB}");
Console.WriteLine($"null comparison = {pointA.Equals(pointC)}");
Console.WriteLine($"Compare to some other type = {pointA.Equals(i)}");
TwoDPoint pointD = null;
TwoDPoint pointE = null;
Console.WriteLine($"Two null TwoDPoints are equal: {pointD == pointE}");
pointE = new TwoDPoint(3, 4);
Console.WriteLine($"(pointE == pointA) = {pointE == pointA}");
Console.WriteLine($"(pointA == pointE) = {pointA == pointE}");
Console.WriteLine($"(pointA != pointE) = {pointA != pointE}");
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new ThreeDPoint(3, 4, 5));
Console.WriteLine($"pointE.Equals(list[0]): {pointE.Equals(list[0])}");
// Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
null comparison = False
Compare to some other type = False
Two null TwoDPoints are equal: True
(pointE == pointA) = False
(pointA == pointE) = False
(pointA != pointE) = True
pointE.Equals(list[0]): False
*/
Sur les classes (types référence), l’implémentation par défaut des deux méthodes Object.Equals(Object) effectue une comparaison d’égalité de référence, et non une vérification de l’égalité des valeurs. Quand un implémenteur substitue la méthode virtuelle, l’objectif est de lui donner une sémantique d’égalité des valeurs.
Les opérateurs == et != peuvent être utilisés avec des classes, même si la classe ne les surcharge pas. Toutefois, le comportement par défaut consiste à effectuer une vérification de l’égalité de référence. Dans une classe, si vous surchargez la méthode Equals, vous devez surcharger les opérateurs == et !=, mais cela n’est pas obligatoire.
Important
L’exemple de code précédent peut ne pas traiter tous les scénarios d’héritage comme vous l’attendez. Prenez le code suivant :
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True
Ce code signale que p1 est égal à p2 malgré la différence de valeurs z. La différence est ignorée, car le compilateur choisit l’implémentation TwoDPoint de IEquatable en fonction du type de compilation. Il s’agit d’un problème fondamental lié à l’égalité polymorphe dans les hiérarchies d’héritage.
Égalité polymorphe
Lors de l’implémentation de l’égalité des valeurs dans les hiérarchies d’héritage avec des classes, l’approche standard indiquée dans l’exemple de classe peut entraîner un comportement incorrect lorsque les objets sont utilisés polymorphes. Le problème se produit parce que System.IEquatable<T> les implémentations sont choisies en fonction du type de compilation, et non du type d’exécution.
Problème avec les implémentations standard
Tenez compte de ce scénario problématique :
TwoDPoint p1 = new ThreeDPoint(1, 2, 3); // Declared as TwoDPoint
TwoDPoint p2 = new ThreeDPoint(1, 2, 4); // Declared as TwoDPoint
Console.WriteLine(p1.Equals(p2)); // True - but should be False!
La comparaison retourne True parce que le compilateur sélectionne TwoDPoint.Equals(TwoDPoint) en fonction du type déclaré, en ignorant les Z différences de coordonnées.
La clé pour corriger l’égalité polymorphe garantit que toutes les comparaisons d’égalité utilisent la méthode virtuelle Object.Equals , qui peut vérifier correctement les types d’exécution et gérer l’héritage. Pour ce faire, vous pouvez utiliser une implémentation d’interface explicite pour System.IEquatable<T> ces délégués à la méthode virtuelle :
La classe de base illustre les modèles clés :
// Safe polymorphic equality implementation using explicit interface implementation
class TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.X = x;
this.Y = y;
}
public override bool Equals(object? obj) => Equals(obj as TwoDPoint);
// Explicit interface implementation prevents compile-time type issues
bool IEquatable<TwoDPoint>.Equals(TwoDPoint? p) => Equals((object?)p);
protected virtual bool Equals(TwoDPoint? p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// If run-time types are not exactly the same, return false.
if (this.GetType() != p.GetType())
{
return false;
}
// Return true if the fields match.
// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (X == p.X) && (Y == p.Y);
}
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint? lhs, TwoDPoint? rhs)
{
if (lhs is null)
{
if (rhs is null)
{
return true;
}
// Only the left side is null.
return false;
}
// Equals handles case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(TwoDPoint? lhs, TwoDPoint? rhs) => !(lhs == rhs);
}
La classe dérivée étend correctement la logique d’égalité :
// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
public int Z { get; private set; }
public ThreeDPoint(int x, int y, int z)
: base(x, y)
{
if ((z < 1) || (z > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.Z = z;
}
public override bool Equals(object? obj) => Equals(obj as ThreeDPoint);
// Explicit interface implementation prevents compile-time type issues
bool IEquatable<ThreeDPoint>.Equals(ThreeDPoint? p) => Equals((object?)p);
protected override bool Equals(TwoDPoint? p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// Runtime type check happens in the base method
if (p is ThreeDPoint threeD)
{
// Check properties that this class declares.
if (Z != threeD.Z)
{
return false;
}
return base.Equals(p);
}
return false;
}
public override int GetHashCode() => (X, Y, Z).GetHashCode();
public static bool operator ==(ThreeDPoint? lhs, ThreeDPoint? rhs)
{
if (lhs is null)
{
if (rhs is null)
{
// null == null = true.
return true;
}
// Only the left side is null.
return false;
}
// Equals handles the case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(ThreeDPoint? lhs, ThreeDPoint? rhs) => !(lhs == rhs);
}
Voici comment cette implémentation gère les scénarios polymorphes problématiques :
Console.WriteLine("=== Safe Polymorphic Equality ===");
// Test polymorphic scenarios that were problematic before
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
TwoDPoint p3 = new ThreeDPoint(1, 2, 3);
TwoDPoint p4 = new TwoDPoint(1, 2);
Console.WriteLine("Testing polymorphic equality (declared as TwoDPoint):");
Console.WriteLine($"p1 = ThreeDPoint(1, 2, 3) as TwoDPoint");
Console.WriteLine($"p2 = ThreeDPoint(1, 2, 4) as TwoDPoint");
Console.WriteLine($"p3 = ThreeDPoint(1, 2, 3) as TwoDPoint");
Console.WriteLine($"p4 = TwoDPoint(1, 2)");
Console.WriteLine();
Console.WriteLine($"p1.Equals(p2) = {p1.Equals(p2)}"); // False - different Z values
Console.WriteLine($"p1.Equals(p3) = {p1.Equals(p3)}"); // True - same values
Console.WriteLine($"p1.Equals(p4) = {p1.Equals(p4)}"); // False - different types
Console.WriteLine($"p4.Equals(p1) = {p4.Equals(p1)}"); // False - different types
Console.WriteLine();
L’implémentation gère également correctement les comparaisons de types directs :
// Test direct type comparisons
var point3D_A = new ThreeDPoint(3, 4, 5);
var point3D_B = new ThreeDPoint(3, 4, 5);
var point3D_C = new ThreeDPoint(3, 4, 7);
var point2D_A = new TwoDPoint(3, 4);
Console.WriteLine("Testing direct type comparisons:");
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
Console.WriteLine($"point3D_A.Equals(point2D_A) = {point3D_A.Equals(point2D_A)}"); // False
Console.WriteLine($"point2D_A.Equals(point3D_A) = {point2D_A.Equals(point3D_A)}"); // False
Console.WriteLine();
L’implémentation de l’égalité fonctionne également correctement avec les collections :
// Test with collections
Console.WriteLine("Testing with collections:");
var hashSet = new HashSet<TwoDPoint> { p1, p2, p3, p4 };
Console.WriteLine($"HashSet contains {hashSet.Count} unique points"); // Should be 3: one ThreeDPoint(1,2,3), one ThreeDPoint(1,2,4), one TwoDPoint(1,2)
var dictionary = new Dictionary<TwoDPoint, string>
{
{ p1, "First 3D point" },
{ p2, "Second 3D point" },
{ p4, "2D point" }
};
Console.WriteLine($"Dictionary contains {dictionary.Count} entries");
Console.WriteLine($"Dictionary lookup for equivalent point: {dictionary.ContainsKey(new ThreeDPoint(1, 2, 3))}"); // True
Le code précédent illustre les éléments clés pour implémenter l’égalité basée sur les valeurs :
-
Remplacement virtuel
Equals(object?): la logique d’égalité principale se produit dans la méthode virtuelleObject.Equals, qui est appelée quel que soit le type de compilation. -
Vérification du type d’exécution : l’utilisation
this.GetType() != p.GetType()garantit que les objets de différents types ne sont jamais considérés comme égaux. - Implémentation d’interface explicite : l’implémentationSystem.IEquatable<T> délègue à la méthode virtuelle, empêchant les problèmes de sélection de type au moment de la compilation.
-
Méthode d’assistance virtuelle protégée : la
protected virtual Equals(TwoDPoint? p)méthode permet aux classes dérivées de remplacer la logique d’égalité tout en conservant la sécurité des types.
Utilisez ce modèle dans les situations suivantes :
- Vous avez des hiérarchies d’héritage où l’égalité des valeurs est importante
- Les objets peuvent être utilisés polymorphement (déclarés en tant que type de base, instanciés comme type dérivé)
- Vous avez besoin de types de référence avec la sémantique d’égalité des valeurs
L’approche recommandée consiste à utiliser record des types pour implémenter l’égalité basée sur la valeur. Cette approche nécessite une implémentation plus complexe que l’approche standard et nécessite des tests approfondis de scénarios polymorphes pour garantir la justesse.
Exemple de struct
L’exemple suivant montre comment implémenter l’égalité des valeurs dans un struct (type valeur). Bien que les structs aient l’égalité des valeurs par défaut, une implémentation personnalisée peut améliorer les performances :
namespace ValueEqualityStruct
{
struct TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
: this()
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
X = x;
Y = y;
}
public override bool Equals(object? obj) => obj is TwoDPoint other && this.Equals(other);
public bool Equals(TwoDPoint p) => X == p.X && Y == p.Y;
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) => lhs.Equals(rhs);
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
class Program
{
static void Main(string[] args)
{
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
int i = 5;
// True:
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}");
// True:
Console.WriteLine($"pointA == pointB = {pointA == pointB}");
// True:
Console.WriteLine($"object.Equals(pointA, pointB) = {object.Equals(pointA, pointB)}");
// False:
Console.WriteLine($"pointA.Equals(null) = {pointA.Equals(null)}");
// False:
Console.WriteLine($"(pointA == null) = {pointA == null}");
// True:
Console.WriteLine($"(pointA != null) = {pointA != null}");
// False:
Console.WriteLine($"pointA.Equals(i) = {pointA.Equals(i)}");
// CS0019:
// Console.WriteLine($"pointA == i = {pointA == i}");
// Compare unboxed to boxed.
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new TwoDPoint(3, 4));
// True:
Console.WriteLine($"pointA.Equals(list[0]): {pointA.Equals(list[0])}");
// Compare nullable to nullable and to non-nullable.
TwoDPoint? pointC = null;
TwoDPoint? pointD = null;
// False:
Console.WriteLine($"pointA == (pointC = null) = {pointA == pointC}");
// True:
Console.WriteLine($"pointC == pointD = {pointC == pointD}");
TwoDPoint temp = new TwoDPoint(3, 4);
pointC = temp;
// True:
Console.WriteLine($"pointA == (pointC = 3,4) = {pointA == pointC}");
pointD = temp;
// True:
Console.WriteLine($"pointD == (pointC = 3,4) = {pointD == pointC}");
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
Object.Equals(pointA, pointB) = True
pointA.Equals(null) = False
(pointA == null) = False
(pointA != null) = True
pointA.Equals(i) = False
pointE.Equals(list[0]): True
pointA == (pointC = null) = False
pointC == pointD = True
pointA == (pointC = 3,4) = True
pointD == (pointC = 3,4) = True
*/
}
Pour les structs, l’implémentation par défaut de Object.Equals(Object) (qui est la version substituée dans System.ValueType) effectue une vérification de l’égalité des valeurs à l’aide de la réflexion pour comparer les valeurs de chaque champ dans le type. Bien que cette implémentation produise des résultats corrects, elle est relativement lente par rapport à une implémentation personnalisée que vous écrivez spécifiquement pour le type.
Lorsque vous remplacez la méthode virtuelle Equals dans un struct, l’objectif est de fournir un moyen plus efficace d’effectuer la vérification de l’égalité des valeurs et éventuellement de baser la comparaison sur certains sous-ensembles des champs ou propriétés du struct.
Les opérateurs == et != ne peuvent pas fonctionner sur un struct, sauf si le struct les surcharge explicitement.