web-dev-qa-db-fra.com

Utilisation d'une classe et d'une structure comme clé de dictionnaire

Supposons que j'aie la définition de classe et de structure suivante, et que je les utilise chacune comme clé dans un objet dictionnaire:

public class MyClass { }
public struct MyStruct { }

public Dictionary<MyClass, string> ClassDictionary;
public Dictionary<MyStruct, string> StructDictionary;

ClassDictionary = new Dictionary<MyClass, string>();
StructDictionary = new Dictionary<MyStruct, string>();

Pourquoi est-ce que cela fonctionne:

MyClass classA = new MyClass();
MyClass classB = new MyClass();
this.ClassDictionary.Add(classA, "Test");
this.ClassDictionary.Add(classB, "Test");

Mais cela se bloque lors de l'exécution:

MyStruct structA = new MyStruct();
MyStruct structB = new MyStruct();
this.StructDictionary.Add(structA, "Test");
this.StructDictionary.Add(structB, "Test");

Il indique que la clé existe déjà, comme prévu, mais uniquement pour la structure. La classe le traite comme deux entrées distinctes. Je pense que cela a quelque chose à voir avec les données détenues comme référence par rapport à la valeur, mais j'aimerais une explication plus détaillée de la raison.

29
Kyle Baran
  1. new object() == new object() est false , car les types de référence ont l'égalité de référence et les deux instances ne sont pas la même référence

  2. new int() == new int() est true , car les types de valeur ont une égalité de valeur et la valeur de deux entiers par défaut est la même valeur. Notez que si vous avez des types de référence ou des valeurs par défaut qui sont incrémentiels dans votre structure, les valeurs par défaut peuvent ne pas non plus être égales pour les structures.

Vous pouvez remplacer les méthodes Equals et GetHashCode et les opérateurs d'égalité des structures et des classes si vous n'aimez pas le comportement d'égalité par défaut.

De plus, si vous voulez un moyen sûr de définir la valeur du dictionnaire, vous pouvez faire dictionary[key] = value; Qui ajoutera de nouvelles valeurs ou mettra à jour les anciennes avec la même clé.

Mettre à jour

@ 280Z28 a posté n commentaire qui a montré comment cette réponse pouvait être trompeuse, ce que je reconnais et que je veux aborder. Il est important de savoir que:

  1. Par défaut, les types de référence de la méthode Equals(object obj) et de l'opérateur == Appellent object.ReferenceEquals(this, obj) sous le capot.

  2. Les opérateurs et les méthodes d'instance doivent être éventuellement remplacés pour propager le comportement. (par exemple, la modification de l'implémentation Equals n'affectera pas l'implémentation ==, sauf si un appel imbriqué est ajouté explicitement).

  3. Toutes les collections génériques .NET par défaut utilisent une implémentation IEqualityComparer<T> Pour déterminer l'égalité (pas une méthode d'instance). IEqualityComparer<T> Peut (et le fait souvent) appeler la méthode d'instance dans son implémentation, mais ce n'est pas quelque chose sur lequel vous pouvez compter. Il existe deux sources possibles pour l'implémentation IEqualityComparer<T> Utilisée:

    1. Vous pouvez le fournir explicitement dans le constructeur.

    2. Il sera récupéré automatiquement à partir de EqualityComparer<T>.Default (Par défaut). Si vous souhaitez configurer globalement IEqualityComparer<T> Par défaut accessible par EqualityComparer<T>.Default, Vous pouvez utiliser ndefault (sur GitHub).

13
smartcaveman

Dictionary<TKey, TValue> Utilise un IEqualityComparer<TKey> Pour comparer les clés. Si vous ne spécifiez pas explicitement le comparateur lorsque vous construisez le dictionnaire, il utilisera EqualityComparer<TKey>.Default .

Comme ni MyClass ni MyStruct n'implémentent IEquatable<T>, Le comparateur d'égalité par défaut appellera Object.Equals Et Object.GetHashCode Pour comparer les instances. MyClass est dérivé de Object, donc l'implémentation utilisera l'égalité de référence pour la comparaison. MyStruct d'autre part est dérivé de System.ValueType (la classe de base de toutes les structures), donc il utilisera ValueType.Equals pour comparer les instances. La documentation de cette méthode indique ce qui suit:

La méthode ValueType.Equals(Object) remplace Object.Equals(Object) et fournit l'implémentation par défaut de l'égalité des valeurs pour tous les types de valeurs dans le .NET Framework.

Si aucun des champs de l'instance actuelle et obj ne sont des types de référence, la méthode Equals effectue une comparaison octet par octet des deux objets en mémoire. Sinon, il utilise la réflexion pour comparer les champs correspondants de obj et cette instance.

L'exception se produit car IDictionary<TKey, TValue>.Add lance un ArgumentException si "Un élément avec la même clé existe déjà dans le [dictionnaire]." Lors de l'utilisation de structures, la comparaison octet par octet effectuée par ValueType.Equals A pour résultat que les deux appels tentent d'ajouter la même clé.

54
Sam Harwell

Il existe généralement trois bons types de clés de dictionnaire: les identités des objets de classe mutables, les valeurs des objets de classe immuables ou les valeurs des structures. Notez que les structures avec des champs publics exposés sont tout aussi appropriées pour être utilisées comme clés de dictionnaire que celles qui ne le font pas, car la seule façon dont la copie de la structure stockée dans le dictionnaire changera sera si la structure est lue, modifiée et écrite arrière. En revanche, les classes avec des propriétés mutables exposées génèrent généralement des clés de dictionnaire moche, sauf dans le cas où l'on souhaite saisir la identité de l'objet, plutôt que son contenu.

Pour qu'un type soit utilisé comme clé de dictionnaire, soit ses méthodes Equals et GetHashCode doivent avoir la sémantique souhaitée, soit le constructeur de Dictionary doit être donné une IEqualityComparer<T> qui implémente la sémantique souhaitée. Les méthodes par défaut Equals et GetHashCode pour les classes seront basées sur l'identité de l'objet (utile si l'on souhaite saisir l'identité des objets mutables; pas si utile autrement). Les méthodes par défaut Equals et GetHashCode pour les types de valeur s'appuieront généralement sur les méthodes Equals et GetHashCode de leurs membres, mais avec quelques rides:

  • Le code utilisant les méthodes par défaut sur les structures s'exécutera souvent beaucoup plus lentement (parfois un ordre de grandeur) que le code qui utilise des méthodes écrites sur mesure.

  • Les structures qui ne contiennent que des types primitifs effectueront des comparaisons à virgule flottante différemment de celles qui incluent également d'autres types. Par exemple, les valeurs posZero = (1.0/(1.0/0.0)) et negZero = (- 1.0/(1.0/0.0)) seront toutes deux égales, mais si elles sont stockées dans une structure qui ne contient que des primitives, elles compareront inégales. Notez que même si les valeurs qu'il compare sont égales, elles ne sont pas sémantiquement les mêmes, car le calcul de 1.0/posZero produira une infinité positive et 1.0/negZero produira une infinité négative.

Si la performance est loin d'être critique, on peut définir une structure simple [simplement déclarer les champs publics appropriés] et la jeter dans un dictionnaire et la faire se comporter comme une clé basée sur des valeurs. Ce ne sera pas terriblement efficace, mais cela fonctionnera. Les dictionnaires gèrent généralement les objets de classe immuables de manière un peu plus efficace, mais définir et utiliser des objets de classe immuables peut parfois être plus difficile que de définir et d'utiliser des "anciennes structures de données simples".

7
supercat

Beause un struct n'est pas référencé comme un class.

Une structure crée une copie d'elle-même au lieu d'analyser une référence comme une classe.

Par conséquent, si vous essayez ceci:

var a =  new MyStruct(){Prop = "Test"};
var b =  new MyStruct(){Prop = "Test"};

Console.WriteLine(a.Equals(b));

// affichera vrai

Si vous faites de même avec une classe:

var a =  new MyClass(){Prop = "Test"};
var b =  new MyClass(){Prop = "Test"};

Console.WriteLine(a.Equals(b));

// affichera faux! (en supposant que vous n'avez pas implémenté de fonction de comparaison) parce que la référence n'est pas la même

4
Jens Kloster

La clé type de référence (classe) pointe vers une référence distincte; la clé type de valeur (struct) pointe vers des valeurs identiques. Je pense que c'est pourquoi vous obtenez l'exception.

1
Mathieu Guindon