web-dev-qa-db-fra.com

"Set" doit-il avoir une méthode Get?

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 uniquement de A. Il peut donc y avoir deux instances égales, mais contenant des informations différentes dans leur propriété B.

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 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 SO questions:

https://stackoverflow.com/questions/7283338/getting-an-element-from-a-set

https://stackoverflow.com/questions/7760364/how-to-retrieve-actual-item-from-hashsett

22
vojta

Le problème ici n'est pas que HashSet n'a pas de méthode Get, c'est que votre code n'a aucun sens du point de vue du type HashSet.

Cette méthode Get est en fait, "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 d'une autre valeur légèrement différente, utilisez Dictionary<String, MyClass> comme vous pouvez ensuite:

var mset = new Dictionary<String, MyClass>();
mset.Add("Hello", new MyClass {A = "Hello", B = "Bye"});

var item = mset["Hello"];
Console.WriteLine(item.B); // will print Bye

Les informations d'égalité fuient de la classe encapsulée. Si je voulais changer l'ensemble des propriétés impliquées dans Equals, je devrais changer le code en dehors de MyClass...

Eh bien oui, mais c'est parce que MyClass tourne en dérision 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:

HashSet<MyClass> mset = new HashSet<MyClass>();
mset.Add(new MyClass {A = "Hello", B = "Bye"});

if (mset.Contains(new MyClass {A = "Hello", B = "See you"})) 
{
    // this code is unreachable.
}

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, ça ne devrait pas changer et donc Dictionary<String, MyClass> est une bonne solution pour cette exigence étrange.

66
David Arno

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.

Un Set est aussi un cas particulier d'un Map | 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>X obtient en quelque sorte 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:

public class MyClass {
   public string A {get; set;}
   public string B {get; set;}
}

public class MyClassEquivalentAs : IEqualityComparer<MyClass>{
   public override bool Equals(MyClass left, MyClass right) {
        if (Object.ReferenceEquals(left, null) && Object.ReferenceEquals(right, null))
        {
            return true;
        }
        else if (Object.ReferenceEquals(left, null) || Object.ReferenceEquals(right, null))
        {
            return false;
        }
        return left.A == right.A;
   }

   public override int GetHashCode(MyClass obj) {
        return obj?.A != null ? obj.A.GetHashCode() : 0;
   }
}

Utilisé ainsi:

var mset = new Dictionary<MyClass, MyClass>(new MyClassEquivalentAs());
var bye = new MyClass {A = "Hello", B = "Bye"};
var seeyou = new MyClass {A = "Hello", B = "See you"};
mset.Add(bye);

if (mset.Contains(seeyou)) {
    //something
}

MyClass item = mset[seeyou];
Console.WriteLine(item.B); // prints Bye
24
Caleth

Votre problème est que vous avez deux concepts contradictoires d'égalité:

  • égalité réelle, où tous les champs sont égaux
  • définir l'égalité d'appartenance, où seul A est égal

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 set est un type de données abstrait qui est défini uniquement par le S contains x ou x is-element-of S relation ("fonction caractéristique"). Si vous voulez d'autres opérations, vous ne recherchez pas réellement un ensemble.

Ce qui arrive assez souvent - mais ce n'est pas un ensemble - c'est que nous groupons tous les objets en classes d'équivalence . Les objets de chacune de ces classes ou sous-ensembles sont uniquement équivalents et non é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:

// The type you actually want to store
class MyClass { ... }

// A equivalence class of MyClass objects,
// with regards to a particular equivalence relation.
// This relation is implemented in EquivalenceClass.Equals()
class EquivalenceClass {
  public MyClass instance { get; }
  public override bool Equals(object o) { ... } // compare instance.A
  public override int GetHashCode() { ... } // hash instance.A
  public static EquivalenceClass of(MyClass o) { return new EquivalenceClass { instance = o }; }
}

// The set-like object mapping equivalence classes
// to a particular representing element.
class EquivalenceHashSet {
  private Dictionary<EquivalenceClass, MyClass> dict = ...;
  public void Add(MyClass o) { dict.Add(EquivalenceClass.of(o), o)}
  public bool Contains(MyClass o) { return dict.Contains(EquivalenceClass.of(o)); }
  public MyClass Get(MyClass o) { return dict.Get(EquivalenceClass.of(o)); }
}
19
amon

Mais pourquoi est-il impossible d'obtenir un élément particulier de l'ensemble?

Parce que ce n'est pas à cela que servent les ensembles.

Permettez-moi de reformuler l'exemple.

"J'ai un HashSet dans lequel je veux stocker des objets MyClass et je veux pouvoir les obtenir en utilisant la propriété A qui est égale à la propriété A de l'objet".

Si remplacer "HashSet" par "Collection", "objets" par "Valeurs" et "propriété A" par "Clé", la phrase devient:

"J'ai une collection dans laquelle je veux stocker les valeurs de MyClass et je veux pouvoir les obtenir en utilisant la clé qui est égale à la clé de l'objet".

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 manière à ce 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 OOP 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 et 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 la structure de ma classe .

7
Old Fat Ned

Dès que vous remplacez égal à, vous feriez mieux de 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 equals et hashcode VM l'identité de l'objet 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.

6
oopexpert

Plus précisément en Java, HashSet a été initialement implémenté en utilisant un HashMap de toute façon, et en ignorant simplement la valeur. Ainsi, la conception initiale ne prévoyait aucun avantage à fournir une méthode get à HashSet. Si vous souhaitez stocker et récupérer une valeur canonique parmi différents objets qui sont égaux, alors vous utilisez simplement un HashMap vous-même.

Je n'ai pas été à jour avec 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 si HashSet a été réimplémenté pour utiliser moins de mémoire que HashMap, dans tous les cas, ce serait un changement de rupture d'ajouter une nouvelle méthode à l'interface Set. C'est donc beaucoup de douleur pour un gain que tout le monde ne mérite pas.

3
Steve Jessop

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 méthode .find Qui recherche l'élément en fonction de l'opérateur de commande < Ou de la fonction binaire bool(T,T) que vous fournissez. Vous pouvez utiliser find pour implémenter l'opération get que vous souhaitez.

En fait, si la fonction bool(T,T) que vous fournissez possède un drapeau spécifique (is_transparent), Vous pouvez passer des objets de type différent pour lesquels la fonction a des surcharges pour. Cela signifie que vous n'avez pas à coller le deuxième champ "fictif" dans le deuxième champ de données, assurez-vous simplement que l'opération de commande que vous utilisez peut commander entre les types recherche et ensemble.

Cela permet une efficacité:

std::set< std::string, my_string_compare > strings;
strings.find( 7 );

my_string_compare comprend comment ordonner des entiers et des chaînes sans d'abord convertir l'entier en chaîne (à un coût potentiel).

Pour unordered_set (L'ensemble de hachage de C++), il n'y a pas encore d'indicateur transparent équivalent. Vous devez passer un T à une méthode unordered_set<T>.find. 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 fera 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 l'un des 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é. 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 Assembly. 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.

2
Yakk