| 

.NET C# Java Javascript Exception

2
Hallo zusammen,

wie würdet Ihr ein Dictionary mit zusammengesetzten Schlüssel realisieren?

Folgende Lösungen habe ich im Internet gefunden:

1. Verwendung von 2 verschachtelten Dictionaries:
Dictionary<TKey1, Dictionary<TKey2, TValue>>

2. Verwendung einer Tuple bzw. eines eigenen Datentypes:
Dictionary<Tuple<TKey1, TKey2>, TValue>

Welche Variante würdet Ihr bevorzugen bzw. kennt Ihr noch weitere Möglichkeiten?


Gruß,
Tachyon
News:
09.09.2011
Tachyon 690 6
5 Antworten
2
Hallo Tachyon,

ich würde einfach einen zusammengesetzten Schlüssel implementieren.
Die Frage ist für mich, was Du mit einem zusammengesetzten Schlüssel meinst? Wenn Du damit soetwas in Deinem 2. Vorschlag meinst (also ein Tuple<Key1,Key2>), dann würde ich das zumindest schon mal als eigene Klasse MyCompositeKeyXY implementieren. Dann kann ich in dieser Klasse GetHashCode(), Equals, ==, !=, .. also alles was ein nichtsortierbarer Schlüssel mitbringen muss implementieren. Damit kann ich dann schlussendlich wieder ein "herkömmliches" "Dictionary<TKey = MyCompositeKeyXY, TValue>" verwenden.

Wenn Du aber wie in Deinem 1. Vorschlag einen zweistufigen Schlüssel meinst, würde ich eine eigene Collection implementieren, also keinen eigenen Schlüssel (solange die im Standardverhalten vergleichbar sind).

Auch wenn man mit Deinen beiden Vorschlägen mehr oder minder dasselbe erreichen kann, sind über den Code die Absichten nicht klar kommuniziert. Deshalb würde ich an Deiner Stelle versuchen, Deine Intention expliziter (in Form eines oder mehrerer Typen) zu formulieren.

UPDATE: Ich greife Matthias Kommentar auf und nutze meinen Post für eine Antwort (der Länge wegen)

Gegeben ist folgender Code:
class SomeClass
{
public string Text { get; set; }
}

static void Main(string[] args)
{
SomeClass newSomeClass = new SomeClass() { Text = "abc" };
Tuple<int, SomeClass> tuple1 = new Tuple<int, SomeClass>(5, newSomeClass);
Tuple<int, SomeClass> tuple2 = new Tuple<int, SomeClass>(5, new SomeClass() { Text = "abc" });
Tuple<int, SomeClass> tuple3 = new Tuple<int, SomeClass>(5, newSomeClass);

Console.WriteLine(tuple1 == tuple2); // false
Console.WriteLine(tuple1 == tuple3); // false

Console.WriteLine(tuple1.Equals(tuple2)); // false
Console.WriteLine(tuple1.Equals(tuple3)); // true

Console.ReadKey();
}


Das Ergebnis des Vergleichs differiert je nach verwendetem Vergleich (==, Equals). Ein Tuple (IStructuralEquatable) führt keinen Referenzvergleich durch, sondern einen elementweisen Wertvergleich. Das Verhalten, welches obiges Codebeispiel zeigt, halte ich für hochgradig gefährlich. Die Berechtigung für einen eigenen Typ leite ich direkt aus diesem Verhalten ab. Selbst wenn ich SomeClass IEquatable implementieren lasse, ändert es nichts. Ein Entwickler muss sich also durch all dieses Zeugs denken, nur weil ein anderer Entwickler seine Absicht bzgl. dieses Schlüssels nicht klar in Form eines eigenen Typs formulieren wollte. Was ich allein deshalb schon fordere, weil ich ihm einen eigenen Namen verpassen kann.

Als zusammengesetzten Schlüssel bezeichne ich für mich einen Schlüssel, der aus mehreren einzelnen Typen besteht und als Einheit vergleichbar ist. Einen solchen zusammengesetzten Schlüssel kann ich (wenn er anständig implementiert ist) ganz normal in einem herkömmlichen Dictionary einsetzen. Der Lookup mit dem zusammengesetzten Schlüssel führt mich zu einem Value.

Einen zweistufigen Schlüssel sehe ich dann, wenn der Schlüssel eben nicht zusammengesetzt ist (also keinen eigenen Typ darstellt), und ich einen zweistufigen Lookup durchführe, also mit dem ersten Schlüssel eine Menge abrufe, in der ich dann mit dem zweiten Schlüssel arbeiten kann. Je nach beabsichtigtem Ergebnis macht das einen Unterscheid zu einem zusammengesetzten Schlüssel.

Was am Ende erreicht werden soll, bleibt IMHO bei untypisierter Benutzung subjektiv. Klar können beide Varianten verwendet werden, aber das ist ein Implementierungsdetail. Wichtiger ist, dass über saubere Typisierung und suggestive Namen Code entsteht, der wartbar ist und tunlichst keine Zweifel zulässt.

Viel Erfolg
Florian
09.09.2011
ffordermaier 4,7k 2 8
1
Mir ist nicht klar, was genau für Dich der Unterschied zwischen einem zusammengesetzten und einem zweistufigen Schlüssel ist.

Zumindest GetHashCode und Equals (und im übrigen sogar IComparable) bringt auch schon die Tuple-Klasse mit, weshalb sich allein daraus für mich noch keine Berechtigung für einen eigenen Datentyp ableitet.
Matthias Hlawatsch 09.09.2011
1
Ist etwas länglich geworden, deshalb hab ich den Post aktualisiert.
ffordermaier 09.09.2011
1
Danke. Ein Stück weit verstehe ich Dich jetzt besser. Aber Du hast einen Fehler in Deiner Argumentation. Wenn SomeClass Object.Equals(obj) implementiert, also insbesondere dann, wenn es IEquatable spezifikationsgemäß implementiert (MSDN: "Wenn Sie IEquatable<T>implementieren, müssen Sie auch die Implementierungen der Basisklasse von Object.Equals(Object) und GetHashCode überschreiben, sodass deren Verhalten dem der IEquatable<T>.Equals -Methode entspricht."), so ergibt tuple1.Equals(tuple2) true. [tbc...]
Matthias Hlawatsch 09.09.2011
1
Und dann (und nur dann!) würde ich auch von einem einem eigenen Datentyp erwarten, dass zwei verschiedene Instanzen, die mit der selben Referenzen von SomeClass initialisiert wurden, "equal" sind. Von daher verhält sich Tuple meines Erachtens hier völlig korrekt und erwartungskonform.

P.S. Wenn Du das noch vertiefen willst, lass uns eine eigene Frage aufmachen, denn hier ging es ja nicht um Tuple, sondern um Dictionaries
Matthias Hlawatsch 09.09.2011
1
Genau das ist es. Da triffst Du den Nagel sowas von auf den Kopf. WENN es spezifikationsgemäß implementiert wird. Da gehören dann auch noch die Operatoren dazu,... bin ich voll bei Dir. Ein weiterer Grund, warum ich den eigenen Typ präferiere, so weiß ich wenigstens wo ich beim CodeReview hinschauen muss, im andern Fall muss ich danach suchen.
ffordermaier 09.09.2011
1
Wieso? Du müßtest Dir in beiden Fällen SomeClass anschauen. Wenn dort jemand nur Equals(SomeClass), aber nicht Equals(Object) implementiert und Du das in Deinem eigenen Datentyp "kompensierst", dann versteckst Du einen Fehler und verschleierst das Problem. Du hättest dann den Fall, dass SomeClass als Einzel-Schlüssel sich anders verhalten würde als als Grundlage für Deinen eigenen Typ - das fände ich nun wiederum "hochgradig gefährlich".
Matthias Hlawatsch 09.09.2011
1
Damit wir uns nicht falsch verstehen: einen eigenen Datentyp fände ich prinzipiell schon auch gut, vor allem wenn die Wertkombination irgendwas fachlich relevantes bedeutet, das man durch einen eigenen Namen gut ausdrücken kann. Aber Equals & Co. sollte er m.E. genau so implementieren wie die Tuple-Klasse. Und eh das jemand verbockt, lass ich ihn lieber Tuple verwenden.
Matthias Hlawatsch 09.09.2011
1
Das meinte ich nicht. Es im eigenen Datentyp zu kompensieren ist ja noch schlimmer.
Klar muss ich den eigenen Datentyp (und natürlich alle damit einhergehenden, wie auch SomeClass) anschauen. Mit Suchen meinte ich das Auffinden der verstreuten Verwendung, die an sich nicht eindeutig ist. Ein zusammengesetzter Schlüssel Tuple<int, SomeClass> und Tuple<SomeClass,int> verhält sich gleich, hat dieselbe Semantik ist aber ein anderer Typ....[tbc]
ffordermaier 09.09.2011
1
Was ich letztendlich nur zum Ausdruck bringen will, ist, dass etwas zusammengesetztes fast immer einen eigenen Typ darstellt und einen sprechenden Namen verident, ob das ein Schlüssel oder eine Bestellung ist (die ist salopp fomuliert ja auch kein Tuple<Customer, List<Article>>). Ich glaub wir sind da letztlich derselben Meinung, nur die Qualität der Erfahrung mit solchen Dingen verschiebt die Toleranzgrenze marginal.
Danke auf alle Fälle für den Meinungsaustausch! Das ist mir viel wert, dass das hier auf codekicker so unkomliziert geht. Auch wenn Kommentare eher ungeeignet sind.
ffordermaier 09.09.2011
1
In meiner Antwort verweise ich auf folgenden Link bei SO: http://stackoverflow.com/questions/1171812/multi-key-dictionary-in-c/1171869#1171869
Hier wollte ich (ein wenig) Bezug auf Matthias Kommentar nehmen. Gleich der erste Kommentar führt zu folgendem Link, der IMO zu eurer Diskussion passt: http://msdn.microsoft.com/en-us/magazine/dd942829.aspx#id0400060
Jürgen Luhr 09.09.2011
3
PS: Tolle Diskussion von euch ... ++++++++++++++++++
Jürgen Luhr 09.09.2011
@Jürgen, danke für den Hinweis auf den Link im verlinkten Beitrag. Das muss ich mir morgen mal genau anschauen.
ffordermaier 09.09.2011
Danke für die ausführliche Darstellung. Ich habe eine große Liste von Items und benötige ein "MultiKeyDictionary" um performant über 2 Schlüssel das entsprechende Item zu bekommen. Da die Schlüssel immer primitive Typen sind kann ich guten Gewissens ein Tuple verwenden. Ich glaube ein guter Kompromiss (als Alternative zum eigenen Schlüsseltyp) ist, wenn das Tupel über eine Methode erzeugt wird.
Tachyon 10.09.2011
Hallo Tachyon, freut mich, dass wir Dir helfen konnten. Ich habe meine Antwort gerade nochmal erweitert, auch mit ein paar Gedanken zur Performance.
Matthias Hlawatsch 12.09.2011
2
Mir gefällt die struct Variante auf dofactory.

Noch mehr Info auf stackoverflow:
Multi-key dictionary in c#

Noch ein Nachschlag:
Wenn die Performance relevant ist, solltest du diesen Artikel lesen:
Performance tests between multiple "multi-key" dictionary implementations

Nachdem ich diesen Link gelesen hatte Building Tuple und erst bei sehr genauem Hinsehen feststellen kann, wie Tuple implementiert ist und zudem nur mit vertieftem Wissen sicher sein kann, dass es sich in unterschiedlichen Programmiersprachen gleich verhält (C# - F#) würde ich eher ein struc anstelle eines tuples verwenden.
In our first draft of the tuple specification, we kept the two-, three-, and four-element tuples as value types, with the rest being reference types. However, during a design meeting that included representatives from other languages it was decided that this "split" design would be confusing, due to the slightly different semantics between the two types. Consistency in behavior and design was determined to be of higher priority than potential performance increases.
09.09.2011
Jürgen Luhr 6,9k 1 8
Steh ich grad auf dem Schlauch, oder warum definiert der sich dort über ein halbes Jahr nach Erscheinen von .NET 4.0 noch seinen eigenen Tuple-Typ?
Matthias Hlawatsch 09.09.2011
Evt. verwendet er noch ein altes Framework aus Kompatibilitätsgründen zu alten Betriebsystemen?!? Das weiß nur Er.
Jürgen Luhr 09.09.2011
2
Hm, die Problemstellung hatte ich, ehrlich gesagt, noch nie. Aber mal überlegen. Die geschachtelte Variante hat m.E. den Vorteil, beim lesenden Zugriff eine sehr schön einfache und klare Syntax zu bieten:

var value = dict[key1][key2];

Schreibend muss man jedoch prüfen, ob key1 irgendwann schon mal benutzt wurde. Das naheliegende

dict[key1][key2] = value;

birgt die Gefahr einer NullReferenceException. Und schließlich finde ich die Typdeklaration in diesem Fall reichlich schwer zu lesen.

Bei der Variante mit Tuple oder eigenem Datentyp ist es gerade umgekehrt: man vermeidet die genannten Nachteile (was die Lesbarkeit des Typs angeht nicht völlig, aber etwas besser finde ich sie schon), dafür muss man sich jedesmal erst mit Tuple.Create(...) oder new CompositeKey(...) einen Schlüssel zusammenbauen, wenn man das Dictionary benutzen will. Das kann auf Dauer nervig sein und ist etwas schwerer lesbar, finde ich. Hängt auch davon ab, ob Du im typischen Einsatzfall schon aus anderen Gründen eine Instanz des Tupels/Datentyps in der Hand hast.

Es gibt noch eine dritte Variante: schreib Dir eine eigene Klasse - entweder von Grund auf, oder abgeleitet von einer der beiden Varianten. Dann kannst Du die API so gestalten, wie Du sie haben willst. Aus dem Bauch heraus würde ich dazu tendieren, dabei von der geschachtelten Variante zu erben, denn ich bekomme den Vorteil der leichten Lesbarkeit beim Zugriff geschenkt und kann durch Wahl eines "schönen" Namens, also z.B.

class CompositeKeyDictionary<TKey1, TKey2, Value> : Dictionary<TKey1, Dictionary<TKey2, TValue>> {...}

(oder noch kürzer nicht-generisch, wenn du das nicht brauchst) und Implementierung eines geeigneten Indexers, der "auf Verdacht" initialisiert, die Nachteile umgehen.

Soweit mal meine Gedanken, auf die Schnelle und am Freitagnachmittag...

Update
Noch ein paar Gedanken mehr dazu:

  • Die Variante mit geschachtelten Dictionaries dürfte Vorteile beim Speicherbedarf haben. Hängt aber auch davon ab, wieviele Key-Ausprägungen in den einzelnen Dimensionen es gibt, und wieviele Kombinationen tatsächlich benutzt werden. Bei einer vollbelegten 1000x1000-Matrix bräuchte ich für die Tuple-Variante 1 Mill. Objekte, die die Schlüssel repräsentieren, mit geschachtelten Dictionaries hingegen nur 1000 Dictionaries. Vermutlich ist ein Tuple leichtgewichtiger als ein Dictionary, aber so groß wird der Unterschied nicht sein, dass er das wieder wettmacht. Der Vorteil wird um so kleiner, je weniger Keys in der letzten Dimension benutzt werden.
    Auf jeden Fall aber muss man bei der Tuple-Variante für jeden lesenden Zugriff wieder ein neues Schlüssel-Objekt generieren. Da kriegt der Garbage Collector auf jeden Fall mehr zu tun als bei den geschachtelten Dictionaries.
  • Die Variante mit geschachtelten Dictionaries erlaubt auch schnelle Abfragen, wenn ich nur einen linksseitigen Teil des Schlüssels habe. Wenn mein Schlüssel z.B. PLZ-Straße-Hausnr-Name darstellt, kann ich auch sehr einfach alle Werte in einer Stadt oder einer Straße in einer Stadt bekommen. Das funktioniert ähnlich wie ein zusammengesetzter Datenbank-Index.
  • Neben der "schönen" Syntax zum Lesen von Werten haben die geschachtelten Dict. auch Nachteile:
    dict.ContainsKey(key1).ContainsKey(key2)
    liest sich nicht so toll, von der Add-Methode ganz zu schweigen. Durch eigene Methoden (ContainsKey(key1, key2) usw.) läßt sich das aber schön beheben.
  • Da die Dictionary-Klasse keine virtuellem Methoden anbietet, würde ich, anders als oben angedeutet, wohl doch nicht davon erben, sondern IDictionary durch Delegation auf Dictionary implementieren.

Mein Fazit: mit der Tuple-Lösung machst Du nichts falsch. Abgesehen vom "reinen" geschachtelten Dictionary, von dem ich aus den genannten Gründen abraten würde, ist sie die einzige, die ohne zusätzliche eigene Klasse auskommt. Für eine bessere Verständlichkeit des Codes kannst Du Florians Empfehlung zu einem eigenen Datentyp in Betracht ziehen. Wenn Du das Dictionary sehr oft brauchst, Dir eine elegante API, Abfragen mit Teil-Schlüsseln und Performance-Vorteile (vermutlich - wäre noch zu überprüfen!) wichtig sind, dann ist eine eigene Implentierung basierend auf dem Ansatz geschachtelter Dictionaries m.E. im Vorteil. In der in Jürgens Kommentar zu Florians Antwort verlinkten Diskussion bei stackoverflow findest Du auch noch ein paar Links zu fertigen Lösungen.
09.09.2011
Matthias Hlawatsch 8,4k 2 8
Schönes Update im Nachhinein! Die gesammelten Antworten und Kommentare beleuchten die Frage aus vielen Perspektiven. Hat Spass gemacht.
ffordermaier 12.09.2011
0
Dictionary<KeyValuePair<TKey, TValue>, TValue>

Gruß
09.09.2011
Nicolai Schönberg 2,3k 1 8
0
Arg! Kommentar mit Antwort verwechselt. Sorry.
09.09.2011
Jürgen Luhr 6,9k 1 8

Stelle deine .net-Frage jetzt!