Ayons cette classe C # (ce serait presque la même chose en Java)
public class MyClass {
public string A {get; set;}
public string B {get; set;}
public override bool Equals(object obj) {
var item = obj as MyClass;
if (item == null || this.A == null || item.A == null)
{
return false;
}
return this.A.equals(item.A);
}
public override int GetHashCode() {
return A != null ? A.GetHashCode() : 0;
}
}
Comme vous pouvez le voir, l'égalité de deux instances de MyClass
dépend A
uniquement de. Il peut donc y avoir deux instances égales, mais détenant différentes informations dans leur B
propriété.
Dans une bibliothèque de collection standard de nombreux langages (y compris C # et Java, bien sûr), il y a un Set
( HashSet
en C #), qui une collection, qui peut contenir au plus un élément de chaque ensemble d'instances égales.
On peut ajouter des éléments, supprimer des éléments et vérifier si l'ensemble contient un élément. Mais pourquoi est-il impossible d'obtenir un élément particulier de l'ensemble?
HashSet<MyClass> mset = new HashSet<MyClass>();
mset.Add(new MyClass {A = "Hello", B = "Bye"});
//I can do this
if (mset.Contains(new MyClass {A = "Hello", B = "See you"})) {
//something
}
//But I cannot do this, because Get does not exist!!!
MyClass item = mset.Get(new MyClass {A = "Hello", B = "See you"});
Console.WriteLine(item.B); //should print Bye
La seule façon de récupérer mon article est d'itérer sur toute la collection et de vérifier l'égalité de tous les articles. Cependant, cela prend du O(n)
temps au lieu de O(1)
!
Jusqu'à présent, je n'ai trouvé aucune langue qui prend en charge get à partir d'un ensemble. Tous les langages "courants" que je connais (Java, C #, Python, Scala, Haskell ...) semblent être conçus de la même manière: vous pouvez ajouter des éléments, mais vous ne pouvez pas les récupérer. Y a-t-il une bonne raison pour laquelle toutes ces langues ne prennent pas en charge quelque chose d'aussi simple et évidemment utile? Ils ne peuvent pas tout simplement se tromper, non? Y a-t-il des langues qui le prennent en charge? Peut-être que récupérer un élément particulier d'un ensemble est faux, mais pourquoi?
Il y a quelques questions liées au SO:
/programming/7283338/getting-an-element-from-a-set
/programming/7760364/how-to-retrieve-actual-item-from-hashsett
std::set
prend en charge la récupération d'objets, donc tous les langages "courants" ne sont pas comme vous le décrivez.Set<E>
implémentations sont justeMap<E,Boolean>
à l'intérieur.a == b
toujours vraie) au cas oùthis.A == null
. Leif (item == null || this.A == null || item.A == null)
test est "exagéré" et vérifie beaucoup, peut-être afin de créer artificiellement du code "de haute qualité". Je vois ce genre de "dépassement" et d'être trop correct tout le temps sur la révision du code.Réponses:
Le problème ici n'est pas qu'il
HashSet
manque uneGet
méthode, c'est que votre code n'a aucun sens du point de vue duHashSet
type.Cette
Get
méthode est effectivement "obtenez-moi cette valeur, s'il vous plaît", à laquelle les gens du framework .NET répondraient raisonnablement, "hein? Vous avez déjà cette valeur<confused face />
".Si vous souhaitez stocker des éléments, puis les récupérer en fonction de la correspondance avec une autre valeur légèrement différente, utilisez-la
Dictionary<String, MyClass>
comme vous pouvez alors:Eh bien oui, mais c'est parce que ça
MyClass
dément avec le principe du moindre étonnement (POLA). Avec cette fonctionnalité d'égalité encapsulée, il est tout à fait raisonnable de supposer que le code suivant est valide:Pour éviter cela,
MyClass
doit être clairement documenté quant à sa forme étrange d'égalité. Cela fait, il n'est plus encapsulé et changer le fonctionnement de l'égalité romprait le principe ouvert / fermé. Ergo, il ne devrait pas changer etDictionary<String, MyClass>
est donc une bonne solution pour cette exigence étrange.la source
Dictionary<MyClass, MyClass>
car il récupérera alors la valeur en fonction d'une clé qui utiliseMyClass.Equals
.Dictionary<MyClass, MyClass>
fourni avec un appropriéIEqualityComparer<MyClass>
, et retirer la relation d'équivalence deMyClass
Pourquoi fautMyClass
-il connaître cette relation sur ses instances?...reasonable to assume...
. Tout cela peut être vrai dans 99% des cas, mais la possibilité de récupérer un élément d'un ensemble peut toujours être utile. Le code du monde réel ne peut pas toujours adhérer aux principes de POLA, etc. Par exemple, si vous dédupliquez des chaînes sans tenir compte de la casse, vous souhaiterez peut-être obtenir l'élément "maître".Dictionary<string, string>
est une solution de contournement, mais cela coûte de la perf.Vous avez déjà l'élément qui est "dans" l'ensemble - vous l'avez passé comme clé.
"Mais ce n'est pas le cas que j'ai appelé Ajouter avec" - Oui, mais vous avez spécifiquement affirmé qu'ils étaient égaux.
A
Set
est également un cas particulier de aMap
|Dictionary
, avec void comme type de valeur (enfin les méthodes inutiles ne sont pas définies, mais cela n'a pas d'importance).La structure de données que vous recherchez est un
Dictionary<X, MyClass>
endroit où enX
quelque sorte obtient le As des MyClasses.Le type de dictionnaire C # est agréable à cet égard, car il vous permet de fournir un IEqualityComparer pour les clés.
Pour l'exemple donné, j'aurais ce qui suit:
Utilisé ainsi:
la source
Dictionary<String, String>
.Comparer
etDictionary<MyClass, MyClass>
est une solution pragmatique. En Java, la même chose peut être obtenue parTreeSet
ouTreeMap
plus personnaliséeComparator
.Votre problème est que vous avez deux concepts contradictoires d'égalité:
Si vous souhaitez utiliser la relation d'égalité réelle dans votre ensemble, le problème de la récupération d'un élément particulier de l'ensemble ne se pose pas - pour vérifier si un objet est dans l'ensemble, vous avez déjà cet objet. Il n'est donc jamais nécessaire de récupérer une instance particulière d'un ensemble, en supposant que vous utilisez la relation d'égalité correcte.
Nous pourrions également faire valoir qu'un ensemble est un type de données abstrait qui est défini uniquement par la relation
S contains x
oux is-element-of S
(«fonction caractéristique»). Si vous voulez d'autres opérations, vous ne recherchez pas réellement un ensemble.Ce qui se produit assez souvent - mais ce n'est pas un ensemble - est que nous regroupons tous les objets dans des classes d'équivalence distinctes . Les objets de chaque classe ou sous-ensemble sont uniquement équivalents, pas égaux. Nous pouvons représenter chaque classe d'équivalence à travers n'importe quel membre de ce sous-ensemble, et il devient alors souhaitable de récupérer cet élément représentant. Ce serait un mappage de la classe d'équivalence à l'élément représentatif.
En C #, un dictionnaire peut utiliser une relation d'égalité explicite, je pense. Sinon, une telle relation peut être implémentée en écrivant une classe wrapper rapide. Pseudocode:
la source
Parce que ce n'est pas à cela que servent les ensembles.
Permettez-moi de reformuler l'exemple.
Si remplacer "HashSet" par "Collection", "objets" par "Valeurs" et "propriété A" par "Clé", la phrase devient:
Ce qui est décrit est un dictionnaire. La véritable question posée est "Pourquoi ne puis-je pas traiter HashSet comme un dictionnaire?"
La réponse est qu'ils ne sont pas utilisés pour la même chose. La raison d'utiliser un ensemble est de garantir l'unicité de son contenu individuel, sinon vous pouvez simplement utiliser une liste ou un tableau. Le comportement décrit dans la question est à quoi sert un dictionnaire. Tous les concepteurs de langage n'ont pas foiré. Ils ne fournissent pas de méthode get car si vous avez l'objet et qu'il est dans l'ensemble, ils sont équivalents, ce qui signifie que vous "obtiendrez" un objet équivalent. Faire valoir que HashSet devrait être implémenté de telle manière que vous puissiez "obtenir" des objets non équivalents que vous avez définis comme égaux est un non-démarreur lorsque les langages fournissent d'autres structures de données qui vous permettent de le faire.
Une note sur le POO et les commentaires / réponses sur l'égalité. Il est normal que la clé du mappage soit une propriété / un membre de la valeur stockée dans un dictionnaire. Par exemple: avoir un Guid comme clé et aussi la propriété qui est utilisée pour la méthode equals est parfaitement raisonnable. Ce qui n'est pas raisonnable, c'est d'avoir des valeurs différentes pour le reste des propriétés. Je trouve que si je vais dans cette direction, j'ai probablement besoin de repenser ma structure de classe.
la source
Dès que vous remplacez égal à, il vaut mieux remplacer le code de hachage. Dès que vous avez fait cela, votre "instance" ne devrait plus jamais changer d'état interne.
Si vous ne remplacez pas égal et que l'identité d'objet de la machine virtuelle hashcode est utilisée pour déterminer l'égalité. Si vous placez cet objet dans un ensemble, vous pouvez le retrouver.
La modification d'une valeur d'un objet qui est utilisée pour déterminer l'égalité entraînera la non traçabilité de cet objet dans les structures basées sur le hachage.
Un poseur sur A est donc dangereux.
Maintenant, vous n'avez pas B qui ne participe pas à l'égalité. Le problème ici n'est pas sémantiquement et techniquement. Parce que changer techniquement B est neutre au fait de l'égalité. Sémantiquement, B doit être quelque chose comme un drapeau "version".
Le point est:
Si vous avez deux objets égaux à A mais pas B, vous supposez que l'un de ces objets est plus récent que l'autre. Si B n'a pas d'informations sur la version, cette hypothèse est masquée dans votre algorithme QUAND vous décidez de "remplacer / mettre à jour" cet objet dans un ensemble. Cet emplacement de code source où cela se produit peut ne pas être évident, donc un développeur aura du mal à identifier la relation entre l'objet X et l'objet Y qui diffère de X en B.
Si B possède des informations sur la version, vous exposez l'hypothèse qui n'était auparavant implicitement dérivable que du code. Vous pouvez maintenant voir que cet objet Y est une version plus récente de X.
Pensez à vous: votre identité reste toute votre vie, peut-être que certaines propriétés changent (par exemple la couleur de vos cheveux ;-)). Bien sûr, vous pouvez supposer que si vous avez deux photos, une avec des cheveux bruns et une avec des cheveux gris, vous pourriez être plus jeune sur la photo avec des cheveux bruns. Mais peut-être que vous avez coloré vos cheveux? Le problème est: VOUS savez peut-être que vous avez coloré vos cheveux. Les autres? Pour mettre cela dans un contexte valide, vous devez introduire l'âge de la propriété (version). Alors vous vous êtes sémantiquement explicite et sans ambiguïté.
Pour éviter l'opération cachée "remplacer l'ancien par un nouvel objet", un ensemble ne doit pas avoir de méthode get. Si vous voulez un comportement comme celui-ci, vous devez le rendre explicite en supprimant l'ancien objet et en ajoutant le nouvel objet.
BTW: Qu'est-ce que cela devrait signifier si vous passez un objet qui est égal à l'objet que vous souhaitez obtenir? Ça n'a pas de sens. Gardez votre sémantique propre et ne le faites pas bien que techniquement personne ne vous gênera.
la source
Plus précisément en Java, a
HashSet
été initialement implémenté en utilisant deHashMap
toute façon, et en ignorant simplement la valeur. La conception initiale ne prévoyait donc aucun avantage à fournir une méthode getHashSet
. Si vous souhaitez stocker et récupérer une valeur canonique parmi divers objets qui sont égaux, alors vous utilisez simplement vous-HashMap
même.Je ne me suis pas tenu au courant de ces détails d'implémentation, donc je ne peux pas dire si ce raisonnement s'applique toujours en Java, encore moins en C # etc. Mais même s'il a
HashSet
été réimplémenté pour utiliser moins de mémoire queHashMap
, en tout cas, serait un changement de rupture pour ajouter une nouvelle méthode à l'Set
interface. C'est donc beaucoup de peine pour un gain que tout le monde ne considère pas comme intéressant.la source
default
implémentation pour le faire sans interruption. Cela ne semble tout simplement pas un changement terriblement utile.O(n)
comparaisons même si la fonction de hachage donne une bonne distribution. Ensuite, les implémentationsSet
qui remplacent l'implémentation par défaut dans l'interface, y comprisHashSet
, pourraient donner une meilleure garantie.Il existe une langue majeure dont l'ensemble a la propriété que vous souhaitez.
En C ++,
std::set
est un ensemble ordonné. Il a une.find
méthode qui recherche l'élément en fonction de l'opérateur de commande<
ou de labool(T,T)
fonction binaire que vous fournissez. Vous pouvez utiliser find pour implémenter l'opération get que vous souhaitez.En fait, si la
bool(T,T)
fonction que vous fournissez possède un indicateur spécifique (is_transparent
), vous pouvez passer des objets d'un autre type pour lesquels la fonction a des surcharges. Cela signifie que vous n'avez pas à coller le deuxième champ "fictif" de données dans le deuxième champ, assurez-vous simplement que l'opération de commande que vous utilisez peut commander entre les types de recherche et de type ensemble.Cela permet une efficacité:
où
my_string_compare
comprend comment ordonner les entiers et les chaînes sans d'abord convertir l'entier en chaîne (à un coût potentiel).Pour
unordered_set
(l'ensemble de hachage C ++), il n'y a pas encore d'indicateur transparent équivalent. Vous devez passer unT
à uneunordered_set<T>.find
méthode. Il pourrait être ajouté, mais les hachages nécessitent==
et un hachage, contrairement aux ensembles ordonnés qui nécessitent simplement une commande.Le modèle général est que le conteneur effectuera la recherche, puis vous donnera un "itérateur" pour cet élément dans le conteneur. À quel moment vous pouvez récupérer l'élément dans l'ensemble, le supprimer, etc.
En bref, les conteneurs standard de toutes les langues n'ont pas les défauts que vous décrivez. Les conteneurs basés sur itérateur de la bibliothèque standard C ++ n'existent pas, et au moins certains des conteneurs existaient avant les autres langages que vous avez décrits, et la possibilité de faire un get encore plus efficacement que la façon dont vous décrivez a même été ajoutée. Il n'y a rien de mal à votre conception ou à vouloir cette opération; les concepteurs des ensembles que vous utilisez n'ont tout simplement pas fourni cette interface.
Conteneurs standard C ++ conçus pour envelopper proprement les opérations de bas niveau du code C équivalent roulé à la main, qui a été conçu pour correspondre à la façon dont vous pouvez l'écrire efficacement dans l'assemblage. Ses itérateurs sont une abstraction de pointeurs de style C. Les langues que vous mentionnez se sont toutes éloignées des pointeurs en tant que concept, donc elles n'ont pas utilisé l'abstraction de l'itérateur.
Il est possible que le fait que C ++ n'ait pas cette faille soit un accident de conception. Le chemin centré sur l'itérateur signifie que pour interagir avec un élément dans un conteneur associatif, vous obtenez d'abord un itérateur sur l'élément, puis vous utilisez cet itérateur pour parler de l'entrée dans le conteneur.
Le coût est qu'il existe des règles d'invalidation d'itération que vous devez suivre, et certaines opérations nécessitent 2 étapes au lieu d'une (ce qui rend le code client plus bruyant). L'avantage est que l'abstraction robuste permet une utilisation plus avancée que celles que les concepteurs d'API avaient à l'esprit à l'origine.
la source