it-swarm.com.de

Sollte "Set" eine Get-Methode haben?

Lassen Sie uns diese C # -Klasse haben (in Java wäre es fast dasselbe)

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;
   }
}

Wie Sie sehen können, hängt die Gleichheit von zwei Instanzen von MyClass nur von A ab. Es kann also zwei Instanzen geben, die gleich sind, aber unterschiedliche Informationen in ihrer Eigenschaft B enthalten.

In einer Standard-Sammlungsbibliothek vieler Sprachen (einschließlich C # und Java natürlich) gibt es ein Set (HashSet in C #), eine Sammlung, die höchstens ein Element von jedem enthalten kann Satz gleicher Instanzen.

Man kann Elemente hinzufügen, Elemente entfernen und prüfen, ob das Set ein Element enthält. Aber warum ist es unmöglich, einen bestimmten Gegenstand aus dem Set zu bekommen?

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

Die einzige Möglichkeit, meinen Artikel abzurufen, besteht darin, die gesamte Sammlung zu durchlaufen und alle Artikel auf Gleichheit zu überprüfen. Dies dauert jedoch O(n) Zeit anstelle von O(1)!

Ich habe bisher keine Sprache gefunden, die Get von einem Set unterstützt. Alle mir bekannten "gängigen" Sprachen (Java, C #, Python, Scala, Haskell ...) scheinen auf die gleiche Weise gestaltet zu sein: Sie können Elemente hinzufügen, aber nicht abrufen. Gibt es einen guten Grund, warum all diese Sprachen etwas nicht unterstützen, das so einfach und offensichtlich nützlich ist? Sie können nicht einfach alle falsch sein, oder? Gibt es Sprachen, die dies unterstützen? Vielleicht ist es falsch, einen bestimmten Gegenstand aus einem Set zu erhalten, aber warum?


Es gibt einige verwandte SO Fragen:

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

Das Problem hierbei ist nicht, dass HashSet keine Get -Methode fehlt, sondern dass Ihr Code aus Sicht des Typs HashSet keinen Sinn ergibt.

Diese Get -Methode lautet effektiv "Hol mir bitte diesen Wert", worauf die .NET Framework-Leute vernünftigerweise antworten würden: "Wie? Sie haben diesen Wert bereits <confused face /> ".

Wenn Sie Elemente speichern und sie dann abrufen möchten, indem sie mit einem anderen geringfügig anderen Wert übereinstimmen, verwenden Sie Dictionary<String, MyClass> wie Sie dann tun können:

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

Die Informationen zur Gleichheit treten aus der gekapselten Klasse aus. Wenn ich die Eigenschaften von Equals ändern wollte, musste ich den Code außerhalb von MyClass... ändern.

Nun ja, aber das liegt daran, dass MyClass mit dem Prinzip des geringsten Erstaunens (POLA) Amok läuft. Wenn diese Gleichheitsfunktion gekapselt ist, ist es völlig vernünftig anzunehmen, dass der folgende Code gültig ist:

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.
}

Um dies zu verhindern, muss MyClass in Bezug auf seine seltsame Form der Gleichheit klar dokumentiert werden. Wenn dies getan ist, ist es nicht mehr gekapselt und eine Änderung der Funktionsweise dieser Gleichstellung würde das offene/geschlossene Prinzip brechen. Ergo sollte es sich nicht ändern und deshalb Dictionary<String, MyClass> ist eine gute Lösung für diese seltsame Anforderung.

66
David Arno

Sie haben bereits den Artikel "im" Set - Sie haben ihn als Schlüssel übergeben.

"Aber es ist nicht die Instanz, mit der ich Add aufgerufen habe" - Ja, aber Sie haben ausdrücklich behauptet, dass sie gleich sind.

Ein Set ist auch ein Sonderfall eines Map | Dictionary mit void als Werttyp (nun, die nutzlosen Methoden sind nicht definiert, aber das spielt keine Rolle) .

Die gesuchte Datenstruktur ist ein Dictionary<X, MyClass> wobei X irgendwie das As aus den MyClasses herausholt.

Der C # -Dictionary-Typ ist in dieser Hinsicht Nizza, da Sie damit einen IEqualityComparer für die Schlüssel angeben können.

Für das gegebene Beispiel hätte ich Folgendes:

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;
   }
}

So verwendet:

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

Ihr Problem ist, dass Sie zwei widersprüchliche Konzepte der Gleichheit haben:

  • tatsächliche Gleichheit, wobei alle Felder gleich sind
  • stellen Sie die Mitgliedschaftsgleichheit ein, wobei nur A gleich ist

Wenn Sie die tatsächliche Gleichheitsrelation in Ihrer Menge verwenden würden, tritt das Problem des Abrufs eines bestimmten Elements aus der Menge nicht auf. Um zu überprüfen, ob sich ein Objekt in der Menge befindet, haben Sie dieses Objekt bereits. Es ist daher niemals erforderlich, eine bestimmte Instanz aus einer Menge abzurufen, vorausgesetzt, Sie verwenden die richtige Gleichheitsrelation.

Wir könnten auch argumentieren, dass ein set ein abstrakter Datentyp ist, der nur durch das S contains x oder x is-element-of S Relation ("charakteristische Funktion"). Wenn Sie andere Operationen wünschen, suchen Sie nicht wirklich nach einem Satz.

Was ziemlich oft passiert - aber was keine Menge ist - ist, dass wir alle Objekte in verschiedene Äquivalenzklassen gruppieren. Die Objekte in jeder solchen Klasse oder Teilmenge sind nur äquivalent, nicht gleich. Wir können jede Äquivalenzklasse durch jedes Mitglied dieser Teilmenge darstellen, und es wird dann wünschenswert, dieses darstellende Element abzurufen. Dies wäre eine Zuordnung von der Äquivalenzklasse zum repräsentativen Element.

In C # kann ein Wörterbuch eine explizite Gleichheitsrelation verwenden, denke ich. Andernfalls kann eine solche Beziehung durch Schreiben einer Quick-Wrapper-Klasse implementiert werden. 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

Aber warum ist es unmöglich, einen bestimmten Gegenstand aus dem Set zu bekommen?

Denn dafür sind Sets nicht gedacht.

Lassen Sie mich das Beispiel umformulieren.

"Ich habe ein HashSet, in dem MyClass-Objekte gespeichert werden sollen, und ich möchte sie mithilfe der Eigenschaft A abrufen können, die der Eigenschaft A des Objekts entspricht.".

Wenn Sie "HashSet" durch "Sammlung", "Objekte" durch "Werte" und "Eigenschaft A" durch "Schlüssel" ersetzen, lautet der Satz:

"Ich habe eine Sammlung, in der ich MyClass-Werte speichern möchte, und ich möchte sie mithilfe des Schlüssels abrufen können, der dem Schlüssel des Objekts entspricht.".

Was beschrieben wird, ist ein Wörterbuch. Die eigentliche Frage lautet: "Warum kann ich HashSet nicht als Wörterbuch behandeln?"

Die Antwort ist, dass sie nicht für dasselbe verwendet werden. Der Grund für die Verwendung eines Sets besteht darin, die Eindeutigkeit seiner einzelnen Inhalte zu gewährleisten. Andernfalls können Sie einfach eine Liste oder ein Array verwenden. Das in der Frage beschriebene Verhalten ist das, wofür ein Wörterbuch gedacht ist. Alle Sprachdesigner haben es nicht vermasselt. Sie bieten keine get-Methode, da sie äquivalent sind, wenn Sie das Objekt haben und es in der Menge enthalten ist, was bedeutet, dass Sie ein äquivalentes Objekt "bekommen" würden. Das Argument, dass HashSet so implementiert werden sollte, dass Sie nicht äquivalente Objekte "erhalten" können, die Sie als gleich definiert haben, ist ein Nichtstarter, wenn die Sprachen andere Datenstrukturen bereitstellen, die dies ermöglichen.

Ein Hinweis zu OOP und Gleichheitskommentaren/-antworten. Es ist in Ordnung, wenn der Schlüssel der Zuordnung eine Eigenschaft/ein Mitglied des in einem Wörterbuch gespeicherten Werts ist. Beispiel: Eine Guid als Der Schlüssel und auch die Eigenschaft, die für die Methode equals verwendet wird, sind völlig vernünftig. Was nicht vernünftig ist, sind unterschiedliche Werte für den Rest der Eigenschaften. Ich finde, wenn ich in diese Richtung gehe, muss ich wahrscheinlich meine Klassenstruktur überdenken .

7
Old Fat Ned

Sobald Sie gleich überschreiben, sollten Sie den Hashcode besser überschreiben. Sobald Sie dies getan haben, sollte Ihre "Instanz" den internen Status nie wieder ändern.

Wenn Sie equals und hashcode nicht überschreiben VM Objektidentität wird verwendet, um die Gleichheit zu bestimmen. Wenn Sie dieses Objekt in ein Set einfügen, können Sie es wiederfinden.

Das Ändern eines Werts eines Objekts, mit dem die Gleichheit bestimmt wird, führt dazu, dass dieses Objekt in Hash-basierten Strukturen nicht mehr nachvollziehbar ist.

Ein Setter auf A ist also gefährlich.

Jetzt haben Sie kein B, das nicht an der Gleichstellung teilnimmt. Das Problem ist hier semantisch nicht technisch. Weil das technische Ändern von B neutral gegenüber der Tatsache der Gleichheit ist. Semantisch muss B so etwas wie ein "Versions" -Flag sein.

Der Punkt ist:

Wenn Sie zwei Objekte haben, die A, aber nicht B entsprechen, gehen Sie davon aus, dass eines dieser Objekte neuer als das andere ist. Wenn B keine Versionsinformationen hat, ist diese Annahme in Ihrem Algorithmus verborgen, wenn Sie sich entscheiden, dieses Objekt in einem Set zu "überschreiben/aktualisieren". Dieser Quellcode-Ort, an dem dies geschieht, ist möglicherweise nicht offensichtlich, sodass es für Entwickler schwierig sein wird, die Beziehung zwischen Objekt X und Objekt Y zu identifizieren, die sich von X in B unterscheidet.

Wenn B Versionsinformationen enthält, legen Sie die Annahme offen, dass diese zuvor nur implizit aus dem Code abgeleitet werden konnten. Jetzt können Sie sehen, dass das Objekt Y eine neuere Version von X ist.

Denken Sie an sich selbst: Ihre Identität bleibt Ihr ganzes Leben lang, möglicherweise ändern sich einige Eigenschaften (z. B. die Farbe Ihres Haares ;-)). Sicher können Sie davon ausgehen, dass Sie, wenn Sie zwei Fotos haben, eines mit braunen Haaren und eines mit grauen Haaren, auf dem Foto mit braunen Haaren möglicherweise jünger sind. Aber vielleicht hast du deine Haare gefärbt? Das Problem ist: Sie wissen vielleicht, dass Sie Ihre Haare gefärbt haben. Dürfen andere? Um dies in einen gültigen Kontext zu stellen, müssen Sie das Eigenschaftsalter (Version) eingeben. Dann bist du semantisch explizit und eindeutig.

Um die versteckte Operation "Ersetzen des alten durch neues Objekt" zu vermeiden, sollte ein Set keine get-Methode haben. Wenn Sie ein solches Verhalten wünschen, müssen Sie es explizit machen, indem Sie das alte Objekt entfernen und das neue Objekt hinzufügen.

Übrigens: Was sollte es bedeuten, wenn Sie ein Objekt übergeben, das dem Objekt entspricht, das Sie erhalten möchten? Das macht keinen Sinn. Halten Sie Ihre Semantik sauber und tun Sie dies nicht, obwohl Sie technisch niemand behindern wird.

6
oopexpert

Speziell in Java wurde HashSet zunächst ohnehin mit einem HashMap implementiert, wobei der Wert einfach ignoriert wurde. Das ursprüngliche Design hatte also keinen Vorteil bei der Bereitstellung einer get-Methode für HashSet erwartet. Wenn Sie einen kanonischen Wert zwischen verschiedenen Objekten speichern und abrufen möchten, die gleich sind, verwenden Sie einfach selbst ein HashMap.

Ich habe mich über solche Implementierungsdetails nicht auf dem Laufenden gehalten, daher kann ich nicht sagen, ob diese Argumentation in Java noch vollständig gilt, geschweige denn in C # usw. Aber selbst wenn HashSet neu implementiert wurde, um weniger Speicher zu verwenden als HashMap wäre es in jedem Fall eine bahnbrechende Änderung, der Set -Schnittstelle eine neue Methode hinzuzufügen. Es ist also ziemlich schmerzhaft für einen Gewinn, den nicht jeder für wert hält.

3
Steve Jessop

Es gibt eine Hauptsprache, deren Satz die gewünschte Eigenschaft hat.

In C++ ist std::set Eine geordnete Menge. Es verfügt über eine .find - Methode, die nach dem Element sucht, das auf dem von Ihnen angegebenen Bestelloperator < Oder der binären Funktion bool(T,T) basiert. Mit find können Sie die gewünschte get-Operation implementieren.

Wenn die von Ihnen bereitgestellte Funktion bool(T,T) ein bestimmtes Flag enthält (is_transparent), Können Sie Objekte vom Typ different übergeben, für die die Funktion verwendet wird hat Überlastungen für. Das bedeutet, dass Sie das "Dummy" -Datenfeld nicht in das zweite Feld stecken müssen, sondern nur sicherstellen müssen, dass der von Ihnen verwendete Bestellvorgang zwischen dem Lookup- und dem Set-enthaltenen Typ sortieren kann.

Dies ermöglicht eine effiziente:

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

dabei versteht my_string_compare, wie man Ganzzahlen und Zeichenfolgen bestellt, ohne die Ganzzahl zuerst in eine Zeichenfolge umzuwandeln (zu potenziellen Kosten).

Für unordered_set (Die Hash-Menge von C++) gibt es (noch) kein gleichwertiges transparentes Flag. Sie müssen ein T an eine unordered_set<T>.find Methode übergeben. Es könnte hinzugefügt werden, aber Hashes erfordern == Und einen Hash, im Gegensatz zu bestellten Sets, die nur eine Bestellung erfordern.

Das allgemeine Muster ist, dass der Container die Suche durchführt und Ihnen dann einen "Iterator" für dieses Element im Container gibt. An diesem Punkt können Sie das Element innerhalb des Satzes abrufen oder löschen usw.

Kurz gesagt, nicht alle Standardcontainer von Sprachen weisen die von Ihnen beschriebenen Fehler auf. Die iteratorbasierten Container der C++ - Standardbibliothek sind nicht vorhanden, und zumindest einige der Container existierten vor einer der anderen von Ihnen beschriebenen Sprachen, und die Möglichkeit, ein get noch effizienter durchzuführen, als Sie es beschrieben haben wurde sogar hinzugefügt. Es ist nichts Falsches an Ihrem Design oder daran, dass Sie diesen Vorgang ausführen möchten. Die Designer der von Ihnen verwendeten Sets haben diese Schnittstelle einfach nicht bereitgestellt.

C++ - Standardcontainer wurden entwickelt, um die Operationen auf niedriger Ebene des entsprechenden handgerollten C-Codes sauber zu verpacken, der so konzipiert wurde, wie Sie ihn in Assembly effizient schreiben können. Seine Iteratoren sind eine Abstraktion von Zeigern im C-Stil. Die Sprachen, die Sie erwähnen, haben sich alle von Zeigern als Konzept entfernt, sodass sie nicht die Iterator-Abstraktion verwendeten.

Es ist möglich, dass die Tatsache, dass C++ diesen Fehler nicht aufweist, ein Unfall des Designs ist. Der iteratorzentrierte Pfad bedeutet, dass Sie für die Interaktion mit einem Element in einem assoziativen Container zuerst einen Iterator für das Element erhalten und dann diesen Iterator verwenden, um über den Eintrag im Container zu sprechen.

Die Kosten bestehen darin, dass Sie Iterations-Ungültigkeitsregeln verfolgen müssen, und einige Vorgänge erfordern zwei Schritte anstelle von einem (was den Client-Code lauter macht). Der Vorteil besteht darin, dass die robuste Abstraktion eine fortgeschrittenere Verwendung ermöglicht als die, an die die API-Designer ursprünglich gedacht hatten.

2
Yakk