Inkrementieren eines numerischen Werts in einem Wörterbuch

74

Ich verwende den folgenden Code, um einen Wert in ein Wörterbuch zu erhöhen oder einzufügen. Wenn der Schlüssel, den ich inkrementiere, nicht vorhanden ist, möchte ich seinen Wert auf 1 setzen.

 public void IncrementCount(Dictionary<int, int> someDictionary, int id)
 {  
     int currentCount;
     if (someDictionary.TryGetValue(id, out currentCount))
     {
         someDictionary[id] = currentCount + 1;
     }
     else
     {
         someDictionary[id] = 1;
     }
 }

Ist dies ein angemessener Weg?

KingNestor
quelle
3
Was versuchst du damit zu erreichen? Welches Problem lösen Sie?
Oded
Das Dictionary-Objekt hat nicht wirklich eine 'Anzahl' und es ist nicht wirklich sinnvoll, es wie ein Array zu 'dekrementieren'. Trotzdem sollte der von Ihnen bereitgestellte Code funktionieren, obwohl es meiner Meinung nach prägnantere Möglichkeiten gibt, mit der Situation umzugehen: Überprüfen Sie die integrierten Methoden des Dictionary-Objekts.
Pete855217
1
Was hast du am besten gemeint? Thread sicher, prägnanter, am schnellsten, überhaupt am besten lesbar?
Tsul
@tsul diese Frage hat gerade eine späte Antwort erhalten . Die Frage ist mehr als fünf Jahre alt und das OP ist seit mehr als zwei Jahren nicht mehr hier.
CodeCaster
@ CodeCaster ja, jetzt verstehe ich. Diese Frage ist jedoch die erste in den Suchergebnissen nach der effizientesten Methode zum Inkrementieren eines Elements in einem .net-Wörterbuch. Daher habe ich versucht, sie zu verbessern.
Tsul

Antworten:

90

Wie sich herausstellte, war es sinnvoll, das ConcurrentDictionary zu verwenden, das über die praktische Upsert-Methode verfügt: AddOrUpdate.

Also habe ich gerade verwendet:

someDictionary.AddOrUpdate(id, 1, (id, count) => count + 1);  
KingNestor
quelle
3
Das ist in Ordnung, aber ich stelle mir vor, dass es einen gewissen Aufwand gibt, um Thread-Probleme zu lösen.
Torwart7960
In der Tat ist ConcurrentDictionary für eine Umgebung optimiert, in der viele Threads auf die Sammlung zugreifen.
Eric J.
83

Ihr Code ist in Ordnung. Aber hier ist eine Möglichkeit, um auf eine Weise zu vereinfachen, die keine Verzweigung in Ihrem Code erfordert:

int currentCount;

// currentCount will be zero if the key id doesn't exist..
someDictionary.TryGetValue(id, out currentCount); 

someDictionary[id] = currentCount + 1;

Dies hängt davon ab, dass die TryGetValueMethode valueden Standardwert ihres Typs festlegt , wenn der Schlüssel nicht vorhanden ist. In Ihrem Fall von dem Standardwert intist 0, das ist genau das, was Sie wollen.


UPD . Ab C # 7.0 kann dieses Snippet verkürzt werden mit out variables:

// declare variable right where it's passed
someDictionary.TryGetValue(id, out var currentCount); 
someDictionary[id] = currentCount + 1;
Ani
quelle
1
Dieser Code funktioniert nicht. Sie haben Recht, dass, wenn der Schlüssel nicht im Wörterbuch vorhanden ist, sein currentCountwird 0. Das heißt aber auch, someDictinary[id]einen zu werfen KeyNotFoundException.
JBSnorro
24
@JBSnorro: Nein, das wird gut funktionieren; Ich ermutige Sie, es zu versuchen. Beachten Sie, dass der Setter des Indexers aufgerufen wird, nicht der Getter. Aus der Dokumentation : "Wenn Sie den Eigenschaftswert festlegen und der Schlüssel im Wörterbuch <TKey, TValue> enthalten ist, wird der diesem Schlüssel zugeordnete Wert durch den zugewiesenen Wert ersetzt. Wenn sich der Schlüssel nicht im Wörterbuch <TKey, TValue befindet >, der Schlüssel und der Wert werden dem Wörterbuch hinzugefügt. "
Ani
1
Hm sorry. Ich war mir dessen nicht bewusst, danke, dass Sie darauf hingewiesen haben.
JBSnorro
9
Mir ist klar, dass dies etwas spät in der Show ist, aber ich dachte, ich würde anderen helfen, die dies treffen. Der obige Code schlägt in einer Umgebung mit mehreren Threads fehl. Zwischen dem TryGetValue und der darunter liegenden Inkrementierungsfunktion kann das Wörterbuch in einem anderen Thread aktualisiert werden. Dies bedeutet, dass currentCount nicht synchron ist und Sie eine Racebedingung erstellt haben.
MarkWalls
18
Alle Beispiele hier sind nicht threadsicher. Es war nicht Teil der Frage, daher wäre die Annahme, dass dies der Fall ist, ein Fehler.
Cyberconte
16

Hier ist eine schöne Erweiterungsmethode:

    public static void Increment<T>(this Dictionary<T, int> dictionary, T key)
    {
        int count;
        dictionary.TryGetValue(key, out count);
        dictionary[key] = count + 1;
    }

Verwendung:

var dictionary = new Dictionary<string, int>();
dictionary.Increment("hello");
dictionary.Increment("hello");
dictionary.Increment("world");

Assert.AreEqual(2, dictionary["hello"]);
Assert.AreEqual(1, dictionary["world"]);
Gelbblut
quelle
5
Mir ist klar, dass dies etwas spät in der Show ist, aber ich dachte, ich würde anderen helfen, die dies treffen. Der obige Code schlägt in einer Umgebung mit mehreren Threads fehl. Zwischen dem TryGetValue und der darunter liegenden Inkrementierungsfunktion kann das Wörterbuch in einem anderen Thread aktualisiert werden. Dies bedeutet, dass die Anzahl nicht synchron ist und Sie eine Racebedingung erstellt haben.
MarkWalls
1
Ich liebe das. Ich habe dies verwendet und den Rückgabewert in den inkrementierten Wert return geändert (dictionary [key] = ++ count);
Rhyous
14

Es ist lesbar und die Absicht ist klar. Ich denke das ist in Ordnung. Sie müssen keinen intelligenteren oder kürzeren Code erfinden. wenn es die Absicht nicht genauso klar hält wie deine ursprüngliche Version :-)

Davon abgesehen ist hier eine etwas kürzere Version:

public void IncrementCount(Dictionary<int, int> someDictionary, int id)
{
    if (!someDictionary.ContainsKey(id))
        someDictionary[id] = 0;

    someDictionary[id]++;
}

Wenn Sie gleichzeitig auf das Wörterbuch zugreifen, denken Sie daran, den Zugriff darauf zu synchronisieren.

driis
quelle
4
@inflagranti - das tut es auch. (Es erhöht die Null vor der Rückkehr - es war ein Beispiel für eine Möglichkeit, weniger Bedingungen in der Methode zu erhalten).
Driis
Erfordert dies nicht eine zusätzliche Suche, wenn die ID nicht vorhanden ist?
Torwart7960
Du hast recht, sorry. Also ich würde sagen, der Originalcode ist dann besser lesbar;)
Janick Bernet
2
Ja, es ist eine zusätzliche Suche erforderlich, wenn keine ID vorhanden ist. Ich lese "bester Weg" als "kürzer / einfacher". Wenn "bester Weg" "leistungsstärkster" bedeutet, sollte die Originalversion etwas effizienter sein. Ich bezweifle jedoch, dass dies messbar sein wird, es sei denn, dies wird in einer engen Schleife verwendet.
Driis
Ich bevorzuge auch den ContainsKeyAnsatz mit Werttypen, wenn ich sie "modifizieren" muss. Ihr Ansatz benötigt jedoch 3 Suchvorgänge, wenn die ID nicht enthalten ist. Sie sollten verwenden if...else.... Es ist noch besser lesbar, wenn Sie verwenden if(someDictionary.ContainsKey(id)) someDictionary[id]++; else someDictionary.Add(id, 1);.
Tim Schmelter
6

Nur einige Messungen unter .NET 4 für Ganzzahlschlüssel.

Es ist keine vollständige Antwort auf Ihre Frage, aber der Vollständigkeit halber habe ich das Verhalten verschiedener Klassen gemessen, die zum Inkrementieren von Ganzzahlen basierend auf Ganzzahlschlüsseln nützlich sind: einfach Array, Dictionary(@ Anis Ansatz), Dictionary(einfacher Ansatz), SortedDictionary(@ Anis Ansatz) ) und ConcurrentDictionary.TryAddOrUpdate.

Hier sind die Ergebnisse, angepasst um 2,5 ns für das Umschließen mit Klassen anstelle der direkten Verwendung:

Array                 2.5 ns/inc
Dictionary (@Ani)    27.5 ns/inc
Dictionary (Simple)  37.4 ns/inc
SortedDictionary    192.5 ns/inc
ConcurrentDictionary 79.7 ns/inc

Und das ist der Code .

Beachten Sie, dass ConcurrentDictionary.TryAddOrUpdatedreimal langsamer als ist Dictionarys‘ TryGetValue+ Indexer Setter ist. Und letzteres ist zehnmal langsamer als Array.

Ich würde also ein Array verwenden, wenn ich weiß, dass der Tastenbereich klein ist und ansonsten ein kombinierter Ansatz.

tsul
quelle
1

Hier ist ein praktischer Komponententest, mit dem Sie in Bezug auf ConcurrentDictionary spielen und wie Sie die Werte threadsicher halten können:

     ConcurrentDictionary<string, int> TestDict = new ConcurrentDictionary<string,int>();
     [TestMethod]
     public void WorkingWithConcurrentDictionary()
     {
         //If Test doesn't exist in the dictionary it will be added with a value of 0
         TestDict.AddOrUpdate("Test", 0, (OldKey, OldValue) => OldValue+1);

         //This will increment the test key value by 1 
         TestDict.AddOrUpdate("Test", 0, (OldKey, OldValue) => OldValue+1);
         Assert.IsTrue(TestDict["Test"] == 1);

         //This will increment it again
         TestDict.AddOrUpdate("Test", 0, (OldKey, OldValue) => OldValue+1);
         Assert.IsTrue(TestDict["Test"] == 2);

         //This is a handy way of getting a value from the dictionary in a thread safe manner
         //It would set the Test key to 0 if it didn't already exist in the dictionary
         Assert.IsTrue(TestDict.GetOrAdd("Test", 0) == 2);

         //This will decriment the Test Key by one
         TestDict.AddOrUpdate("Test", 0, (OldKey, OldValue) => OldValue-1);
         Assert.IsTrue(TestDict["Test"] == 1);
     }
MarkWalls
quelle
0

Wenn Sie nach einer verkürzten Version suchen:

someDictionary.Add(id, someDictionary.GetValueOrDefault(id, 0) + 1);
Farhad Shariatzadeh
quelle