Gibt es eine bessere Möglichkeit, C # -Wörterbücher zu verwenden als TryGetValue?

19

Ich finde mich oft dabei, Fragen online nachzuschlagen, und viele Lösungen enthalten Wörterbücher. Wenn ich jedoch versuche, sie zu implementieren, steckt dieser schreckliche Gestank in meinem Code. Zum Beispiel jedes Mal, wenn ich einen Wert verwenden möchte:

int x;
if (dict.TryGetValue("key", out x)) {
    DoSomethingWith(x);
}

Das sind 4 Zeilen Code, um im Wesentlichen Folgendes zu tun: DoSomethingWith(dict["key"])

Ich habe gehört, dass die Verwendung des Schlüsselworts out ein Anti-Pattern ist, da dadurch Funktionen ihre Parameter verändern.

Außerdem brauche ich oft ein "umgekehrtes" Wörterbuch, in dem ich die Schlüssel und Werte umblättere.

In ähnlicher Weise möchte ich oft die Elemente in einem Wörterbuch durchlaufen und dabei Schlüssel oder Werte in Listen usw. konvertieren, um dies besser zu tun.

Ich habe das Gefühl, dass es fast immer eine bessere und elegantere Art gibt, Wörterbücher zu benutzen. Aber ich bin ratlos.

Adam B
quelle
7
Es kann auch andere Möglichkeiten geben, aber ich verwende im Allgemeinen zuerst einen ContainsKey, bevor ich versuche, einen Wert abzurufen.
Robbie Dee
2
Ehrlich gesagt, mit wenigen Ausnahmen, wenn Sie wissen , was Ihre Wörterbuchschlüssel sind vor der Zeit, es wahrscheinlich ist , ein besserer Weg. Wenn Sie es nicht wissen, sollten die Diktattasten im Allgemeinen überhaupt nicht fest codiert sein. Wörterbücher sind nützlich für die Arbeit mit halbstrukturierten Objekten oder Daten, bei denen die Felder zumindest größtenteils orthogonal zur Anwendung sind. IMO: Je näher diese Felder an relevanten Geschäfts- / Domänenkonzepten liegen, desto weniger hilfreich sind Wörterbücher für die Arbeit mit diesen Konzepten.
Svidgen
6
@RobbieDee: Du musst allerdings vorsichtig sein, da dies eine Racebedingung schafft. Es ist möglich, dass der Schlüssel zwischen dem Aufrufen von ContainsKey und dem Abrufen des Werts entfernt wird.
Whatsisname
12
@whatsisname: Unter diesen Bedingungen wäre ein ConcurrentDictionary besser geeignet. Sammlungen im System.Collections.GenericNamespace sind nicht threadsicher .
Robert Harvey
4
Gut die Hälfte der Zeit, in der ich jemanden sehe, der einen benutzt Dictionary, war das, was er wirklich wollte, eine neue Klasse. Für diese 50% (nur) ist es ein Designgeruch.
BlueRaja - Danny Pflughoeft

Antworten:

23

Wörterbücher (C # oder anders) sind einfach ein Container, in dem Sie einen Wert basierend auf einem Schlüssel nachschlagen. In vielen Sprachen wird es korrekter als Map identifiziert, wobei die häufigste Implementierung eine HashMap ist.

Das zu berücksichtigende Problem ist, was passiert, wenn ein Schlüssel nicht vorhanden ist. Einige Sprachen verhalten sich durch Rücksendung nulloder niloder einen anderen Gegenwert. Stille Vorgabe eines Werts, anstatt Sie darüber zu informieren, dass kein Wert vorhanden ist.

Die C # -Bibliotheksdesigner hatten sich eine Redewendung ausgedacht, um mit dem Verhalten umzugehen. Sie argumentierten, dass das Standardverhalten beim Suchen eines nicht vorhandenen Werts darin besteht, eine Ausnahme auszulösen. Wenn Sie Ausnahmen vermeiden möchten, können Sie die TryVariante verwenden. Dies ist der gleiche Ansatz, den sie zum Parsen von Zeichenfolgen in Ganzzahlen oder Datums- / Uhrzeitobjekte verwenden. Im Wesentlichen ist die Auswirkung wie folgt:

T count = int.Parse("12T45"); // throws exception

if (int.TryParse("12T45", out count))
{
    // Does not throw exception
}

Und das übertrug sich auf das Wörterbuch, dessen Indexer delegiert an Get(index):

var myvalue = dict["12345"]; // throws exception
myvalue = dict.Get("12345"); // throws exception

if (dict.TryGet("12345", out myvalue))
{
    // Does not throw exception
}

Dies ist einfach die Art und Weise, wie die Sprache gestaltet ist.


Sollte out Variablen entmutigt werden?

C # ist nicht die erste Sprache, die sie hat, und sie haben ihren Zweck in bestimmten Situationen. Wenn Sie versuchen, ein System mit hoher Gleichzeitigkeit zu erstellen, können Sie es nicht verwendenout Variablen an den Parallelitätsgrenzen verwenden.

In vielerlei Hinsicht versuche ich, diese Redewendungen in meine APIs aufzunehmen, wenn es eine Redewendung gibt, die von den Anbietern von Sprach- und Kernbibliotheken vertreten wird. Dadurch fühlt sich die API in dieser Sprache konsistenter und besser aufgehoben. Eine in Ruby geschriebene Methode sieht also nicht wie eine in C #, C oder Python geschriebene Methode aus. Sie verfügen jeweils über eine bevorzugte Methode zum Erstellen von Code. Wenn Sie damit arbeiten, können die Benutzer der API diese schneller erlernen.


Sind Maps im Allgemeinen ein Anti-Pattern?

Sie haben ihren Zweck, aber oft sind sie die falsche Lösung für den Zweck, den Sie haben. Besonders wenn Sie eine bidirektionale Abbildung haben, die Sie benötigen. Es gibt viele Container und Möglichkeiten, Daten zu organisieren. Es gibt viele Ansätze, die Sie verwenden können, und manchmal müssen Sie ein wenig nachdenken, bevor Sie diesen Container auswählen.

Wenn Sie eine sehr kurze Liste bidirektionaler Zuordnungswerte haben, benötigen Sie möglicherweise nur eine Liste von Tupeln. Oder eine Liste von Strukturen, in denen Sie die erste Übereinstimmung auf beiden Seiten des Mappings finden können.

Denken Sie an die Problemdomäne und wählen Sie das am besten geeignete Werkzeug für den Job aus. Wenn es keinen gibt, dann erstelle ihn.

Berin Loritsch
quelle
4
"Dann brauchen Sie vielleicht nur eine Liste von Tupeln. Oder eine Liste von Strukturen, in denen Sie genauso einfach die erste Übereinstimmung auf beiden Seiten des Mappings finden können." - Wenn es Optimierungen für kleine Mengen gibt, sollten diese in der Bibliothek vorgenommen werden und nicht in den Benutzercode eingegossen werden.
Freitag,
Wenn Sie eine Liste von Tupeln oder ein Wörterbuch auswählen, ist dies ein Implementierungsdetail. Es geht darum, Ihre Problemdomäne zu verstehen und das richtige Werkzeug für den Job zu verwenden. Offensichtlich existiert eine Liste und ein Wörterbuch. Im Normalfall ist das Wörterbuch korrekt, aber möglicherweise müssen Sie für ein oder zwei Anwendungen die Liste verwenden.
Berin Loritsch
3
Dies ist der Fall, aber wenn das Verhalten immer noch dasselbe ist, sollte sich diese Bestimmung in der Bibliothek befinden. Ich bin auf Container-Implementierungen in anderen Sprachen gestoßen , die den Algorithmus wechseln, wenn a priori bekannt gegeben wird, dass die Anzahl der Einträge gering sein wird. Ich stimme Ihrer Antwort zu, aber sobald dies herausgefunden wurde, sollte es sich in einer Bibliothek befinden, möglicherweise als SmallBidirectionalMap.
Blrfl
5
"Die C # -Bibliothek-Designer haben sich eine Redewendung ausgedacht, um mit dem Verhalten umzugehen." - Ich denke, Sie haben den Nagel auf den Kopf getroffen: Sie haben ein Idiom "erfunden", anstatt ein bereits weit verbreitetes zu verwenden, bei dem es sich um einen optionalen Rückgabetyp handelt.
Jörg W Mittag
3
@ JörgWMittag Wenn du über so etwas redest Option<T>, dann würde das meiner Meinung nach die Verwendung der Methode in C # 2.0 erheblich erschweren, da es keine Mustererkennung gab.
Svick
21

Einige gute Antworten hier auf die allgemeinen Prinzipien von Hashtabellen / Wörterbüchern. Aber ich dachte, ich würde Ihr Codebeispiel berühren,

int x;
if (dict.TryGetValue("key", out x)) 
{
    DoSomethingWith(x);
}

Ab C # 7 (was meiner Meinung nach ungefähr zwei Jahre alt ist) kann dies vereinfacht werden zu:

if (dict.TryGetValue("key", out var x))
{
    DoSomethingWith(x);
}

Und natürlich könnte es auf eine Zeile reduziert werden:

if (dict.TryGetValue("key", out var x)) DoSomethingWith(x);

Wenn Sie einen Standardwert für den Fall haben, dass der Schlüssel nicht vorhanden ist, kann er folgendermaßen lauten:

DoSomethingWith(dict.TryGetValue("key", out var x) ? x : defaultValue);

So können Sie kompakte Formulare erstellen, indem Sie relativ neue Spracherweiterungen verwenden.

David Arno
quelle
1
Guter Aufruf der v7-Syntax, nette kleine Option, die zusätzliche Definitionszeile ausschneiden zu müssen, während die Verwendung von var +1
erlaubt ist
2
Beachten Sie auch, dass, wenn "key"nicht vorhanden, xaufdefault(TValue)
Peter Duniho
Könnte auch als generische Erweiterung schön sein, aufgerufen wie"key".DoSomethingWithThis()
Ross Presser
Ich bevorzuge Designs, bei denen das getOrElse("key", defaultValue) Null-Objekt immer noch mein Lieblingsmuster ist. Arbeiten Sie auf diese Weise, und es ist Ihnen egal, ob das TryGetValueErgebnis wahr oder falsch ist.
candied_orange
1
Ich bin der Meinung, dass diese Antwort einen Haftungsausschluss verdient, der besagt, dass das Schreiben von kompaktem Code aus Gründen der Kompaktheit schlecht ist. Dies kann das Lesen Ihres Codes erheblich erschweren. Wenn ich mich richtig erinnere, TryGetValueist es außerdem nicht atomar / thread-sicher, sodass Sie genauso einfach eine Überprüfung auf Existenz und eine andere durchführen können, um den Wert
Marie,
12

Dies ist weder ein Codegeruch noch ein Anti-Pattern, da die Verwendung von TryGet-Funktionen mit einem out-Parameter ein idiomatisches C # ist. In C # stehen jedoch drei Optionen zur Verfügung, um mit einem Wörterbuch zu arbeiten. Sollten Sie also sicher sein, dass Sie die richtige für Ihre Situation verwenden. Ich glaube, ich weiß, woher das Gerücht kommt, dass es ein Problem mit einem out-Parameter gibt, also werde ich das am Ende behandeln.

Welche Funktionen sind beim Arbeiten mit einem C # -Wörterbuch zu verwenden?

  1. Wenn Sie sicher sind, dass sich der Schlüssel im Wörterbuch befindet, verwenden Sie die Item-Eigenschaft [TKey]
  2. Wenn sich der Schlüssel im Allgemeinen im Dictionary befinden sollte, es jedoch schlecht / selten / problematisch ist, dass er nicht vorhanden ist, sollten Sie Try ... Catch verwenden, damit ein Fehler ausgelöst wird, und dann können Sie versuchen, den Fehler ordnungsgemäß zu behandeln
  3. Wenn Sie nicht sicher sind, ob sich der Schlüssel im Dictionary befindet, verwenden Sie TryGet mit dem Parameter out

Um dies zu rechtfertigen, muss nur auf die Dokumentation für Dictionary TryGetValue unter "Bemerkungen" verwiesen werden :

Diese Methode kombiniert die Funktionalität der ContainsKey-Methode und der Item-Eigenschaft [TKey].

...

Verwenden Sie die TryGetValue-Methode, wenn Ihr Code häufig versucht, auf Schlüssel zuzugreifen, die nicht im Wörterbuch enthalten sind. Die Verwendung dieser Methode ist effizienter als das Abfangen der KeyNotFoundException, die von der Item-Eigenschaft [TKey] ausgelöst wird.

Dieses Verfahren nähert sich einer O (1) -Operation.

Der ganze Grund, warum TryGetValue existiert, besteht darin, die Verwendung von ContainsKey und Item [TKey] zu vereinfachen und gleichzeitig zu vermeiden, dass das Wörterbuch zweimal durchsucht werden muss Wahl.

In der Praxis habe ich aufgrund dieser einfachen Maxime selten ein unformatiertes Wörterbuch verwendet: Wählen Sie die allgemeinste Klasse / den allgemeinsten Container aus , der Ihnen die Funktionen bietet, die Sie benötigen . Das Wörterbuch wurde nicht für die Suche nach Werten und nicht nach Schlüsseln entwickelt. Wenn Sie dies wünschen, ist es möglicherweise sinnvoller, eine alternative Struktur zu verwenden. Ich glaube, ich habe Dictionary im letzten Jahr meines Entwicklungsprojekts vielleicht einmal verwendet, einfach weil es selten das richtige Werkzeug für den Job war, den ich versuchte. Dictionary ist sicherlich nicht das Schweizer Taschenmesser der C # -Toolbox.

Was ist los mit unseren Parametern?

CA1021: Vermeiden Sie Parameter

Rückgabewerte sind zwar alltäglich und werden häufig verwendet, die korrekte Anwendung von out- und ref-Parametern erfordert jedoch fortgeschrittene Design- und Codierungsfähigkeiten. Bibliotheksarchitekten, die für ein allgemeines Publikum entwerfen, sollten nicht erwarten, dass Benutzer das Arbeiten mit Parametern beherrschen.

Vermutlich haben Sie dort gehört, dass unsere Parameter so etwas wie ein Anti-Pattern sind. Wie bei allen Regeln sollten Sie näher lesen, um das Warum zu verstehen. In diesem Fall wird sogar explizit erwähnt, dass das Try-Muster nicht gegen die Regel verstößt :

Methoden, die das Try-Muster implementieren, z. B. System.Int32.TryParse, lösen diese Verletzung nicht aus.

BrianH
quelle
Ich würde mir wünschen, dass mehr Menschen diese ganze Liste von Anleitungen lesen. Für die, die folgen, ist hier die Wurzel davon. docs.microsoft.com/en-us/visualstudio/code-quality/…
Peter Wone
10

In C # -Wörterbüchern fehlen mindestens zwei Methoden, die meiner Meinung nach in vielen Situationen in anderen Sprachen den Code erheblich bereinigen. Der erste Befehl gibt einen zurück Option, mit dem Sie in Scala Code wie den folgenden schreiben können:

dict.get("key").map(doSomethingWith)

Die zweite Methode gibt einen benutzerdefinierten Standardwert zurück, wenn der Schlüssel nicht gefunden wird:

doSomethingWith(dict.getOrElse("key", "key not found"))

Es gibt etwas zu sagen, um die Redewendungen zu verwenden, die eine Sprache bietet, wenn dies angemessen ist, wie das TryMuster, aber das bedeutet nicht, dass Sie nur das verwenden müssen, was die Sprache bietet. Wir sind Programmierer. Es ist in Ordnung, neue Abstraktionen zu erstellen, um das Verständnis unserer spezifischen Situation zu erleichtern, insbesondere, wenn dadurch viele Wiederholungen vermieden werden. Wenn Sie häufig etwas benötigen, z. B. Reverse-Lookups oder das Durchlaufen von Werten, können Sie dies durchführen. Erstellen Sie die Schnittstelle, die Sie sich gewünscht haben.

Karl Bielefeldt
quelle
Ich mag die 2. Option. Vielleicht muss ich eine oder zwei Erweiterungsmethoden schreiben :)
Adam B
2
Auf der positiven Seite bedeutet c # -Erweiterungsmethoden, dass Sie diese selbst implementieren können
jk.
5

Das sind 4 Zeilen Code, um im Wesentlichen Folgendes zu tun: DoSomethingWith(dict["key"])

Ich stimme zu, dass dies unelegant ist. Ein Mechanismus, den ich in diesem Fall gerne verwende, wenn der Wert ein Strukturtyp ist, ist:

public static V? TryGetValue<K, V>(
      this Dictionary<K, V> dict, K key) where V : struct => 
  dict.TryGetValue(key, out V v)) ? new V?(v) : new V?();

Jetzt haben wir eine neue Version , TryGetValuedass die Renditen int?. Wir können dann einen ähnlichen Trick ausführen, um Folgendes zu erweitern T?:

public static void DoIt<T>(
      this T? item, Action<T> action) where T : struct
{
  if (item != null) action(item.GetValueOrDefault());
}

Und jetzt füge es zusammen:

dict.TryGetValue("key").DoIt(DoSomethingWith);

und wir sind auf eine einzige, klare Aussage zurückzuführen.

Ich habe gehört, dass die Verwendung des Schlüsselworts out ein Anti-Pattern ist, da dadurch Funktionen ihre Parameter verändern.

Ich würde etwas weniger stark formuliert sein und sagen, dass es eine gute Idee ist, Mutationen zu vermeiden, wenn dies möglich ist.

Ich brauche oft ein "umgekehrtes" Wörterbuch, in dem ich die Schlüssel und Werte umblättere.

Implementieren oder beziehen Sie dann ein bidirektionales Wörterbuch. Sie sind einfach zu schreiben, oder es gibt viele Implementierungen im Internet. Hier gibt es eine Reihe von Implementierungen, zum Beispiel:

/programming/268321/bidirectional-1-to-1-dictionary-in-c-sharp

In ähnlicher Weise möchte ich oft die Elemente in einem Wörterbuch durchlaufen und dabei Schlüssel oder Werte in Listen usw. konvertieren, um dies besser zu tun.

Klar, das machen wir alle.

Ich habe das Gefühl, dass es fast immer eine bessere und elegantere Art gibt, Wörterbücher zu benutzen. Aber ich bin ratlos.

Fragen Sie sich: "Angenommen, ich hätte eine andere Klasse als Dictionarydiese, die genau die Operation implementiert, die ich ausführen möchte. Wie würde diese Klasse aussehen?" Wenn Sie diese Frage beantwortet haben, implementieren Sie diese Klasse . Du bist ein Computerprogrammierer. Lösen Sie Ihre Probleme, indem Sie Computerprogramme schreiben!

Eric Lippert
quelle
Vielen Dank. Glauben Sie, Sie könnten etwas genauer erklären, was die Aktionsmethode tatsächlich tut? Ich bin neu in Aktionen.
Adam B
1
@AdamB Eine Aktion ist ein Objekt, das die Fähigkeit zum Aufrufen einer Void-Methode darstellt. Wie Sie sehen, übergeben wir auf der Aufruferseite den Namen einer void-Methode, deren Parameterliste mit den generischen Typargumenten der Aktion übereinstimmt. Auf der Aufruferseite wird die Aktion wie jede andere Methode aufgerufen.
Eric Lippert
1
@AdamB: Sie können sich vorstellen Action<T>, sehr ähnlich zu sein interface IAction<T> { void Invoke(T t); }, aber mit sehr nachgiebigen Regeln darüber, was diese Schnittstelle "implementiert" und wie Invokesie aufgerufen werden kann. Wenn Sie mehr wissen möchten, informieren Sie sich in C # über "Delegaten" und anschließend über Lambda-Ausdrücke.
Eric Lippert
Ok, ich glaube, ich habe es verstanden. Mit dieser Aktion können Sie eine Methode als Parameter für DoIt () einfügen. Und nennt diese Methode.
Adam B
@AdamB: Genau richtig. Dies ist ein Beispiel für "Funktionsprogrammierung mit Funktionen höherer Ordnung". Die meisten Funktionen verwenden Daten als Argumente. Funktionen höherer Ordnung nehmen Funktionen als Argumente und machen dann Sachen mit diesen Funktionen. Wenn Sie mehr über C # erfahren, werden Sie feststellen, dass LINQ vollständig mit Funktionen höherer Ordnung implementiert ist.
Eric Lippert
4

Das TryGetValue()Konstrukt ist nur erforderlich, wenn Sie nicht wissen, ob "key" als Schlüssel im Wörterbuch vorhanden ist oder nicht, andernfalls DoSomethingWith(dict["key"])ist es vollkommen gültig.

Ein "weniger schmutziger" Ansatz könnte sein, ihn ContainsKey()stattdessen als Kontrolle zu verwenden.

Wanderer
quelle
6
"Ein" weniger schmutziger "Ansatz könnte darin bestehen, stattdessen ContainsKey () als Check zu verwenden." Ich stimme dir nicht zu. So suboptimal es auch TryGetValueist, so schwer fällt es einem doch, den leeren Koffer in den Griff zu bekommen. Idealerweise würde ich mir wünschen, dass dies nur ein Optional zurückgibt.
Alexander - Reinstate Monica
1
Es gibt ein potenzielles Problem mit dem ContainsKeyAnsatz für Multithread-Anwendungen, bei dem es sich um die Sicherheitsanfälligkeit TOCTOU (Time-to-Time-of-Use) handelt. Was ist, wenn ein anderer Thread den Schlüssel zwischen den Aufrufen von ContainsKeyund löscht GetValue?
David Hammen
2
@ JAD Optionals komponieren. Sie können eineOptional<Optional<T>>
Alexander - Reinstate Monica
2
@DavidHammen Um klar zu sein, müsstest du TryGetValueauf ConcurrentDictionary. Das reguläre Wörterbuch würde nicht synchronisiert werden
Alexander - Reinstate Monica
2
@JAD Nein, (Javas optionaler Typ schlägt, fang nicht an). Ich spreche abstrakter
Alexander - Setzen Sie Monica
2

Andere Antworten enthalten großartige Punkte, deshalb werde ich sie hier nicht wiederholen, sondern mich auf diesen Teil konzentrieren, der bislang weitgehend ignoriert zu werden scheint:

In ähnlicher Weise möchte ich oft die Elemente in einem Wörterbuch durchlaufen und dabei Schlüssel oder Werte in Listen usw. konvertieren, um dies besser zu tun.

Tatsächlich ist es ziemlich einfach, ein Wörterbuch zu durchlaufen, da es Folgendes implementiert IEnumerable:

var dict = new Dictionary<int, string>();

foreach ( var item in dict ) {
    Console.WriteLine("{0} => {1}", item.Key, item.Value);
}

Wenn Sie Linq bevorzugen, funktioniert das auch ganz gut:

dict.Select(x=>x.Value).WhateverElseYouWant();

Alles in allem finde ich Wörterbücher nicht als Antimuster - sie sind nur ein spezifisches Werkzeug mit spezifischen Verwendungszwecken.

Für noch mehr Details, check out SortedDictionary(verwendet RB-Bäume für eine vorhersagbarere Leistung) und SortedList(das ist auch ein verwirrend benanntes Wörterbuch, das die Einfügungsgeschwindigkeit für die Suchgeschwindigkeit opfert, aber leuchtet, wenn Sie mit einer festen, vorab festgelegten sortierter Satz). Ich hatte einen Fall, in dem das Ersetzen von a Dictionarydurch a SortedDictionaryzu einer um eine Größenordnung schnelleren Ausführung führte (aber es könnte auch umgekehrt passieren).

Vilx-
quelle
1

Wenn Sie das Gefühl haben, dass die Verwendung eines Wörterbuchs umständlich ist, ist dies möglicherweise nicht die richtige Wahl für Ihr Problem. Wörterbücher sind großartig, aber wie ein Kommentator bemerkt hat, werden sie oft als Abkürzung für etwas verwendet, das eine Klasse sein sollte. Oder das Wörterbuch selbst kann als Kernspeichermethode in Frage kommen, sollte jedoch von einer Wrapper-Klasse umgeben sein, um die gewünschten Dienstmethoden bereitzustellen.

Es wurde viel über Dict [key] im Vergleich zu TryGet gesagt. Was ich häufig benutze, ist das Durchlaufen des Wörterbuchs mit KeyValuePair. Anscheinend ist dies ein weniger bekanntes Konstrukt.

Der Hauptvorteil eines Wörterbuchs ist, dass es im Vergleich zu anderen Sammlungen sehr schnell ist, wenn die Anzahl der Elemente zunimmt. Wenn Sie prüfen müssen, ob ein Schlüssel häufig vorhanden ist, möchten Sie sich möglicherweise fragen, ob die Verwendung eines Wörterbuchs angemessen ist. Als Kunde sollten Sie in der Regel wissen, was Sie dort eingeben und was sicher abgefragt werden kann.

Martin Maat
quelle