Gibt es eine IDictionary-Implementierung, die bei fehlendem Schlüssel den Standardwert zurückgibt, anstatt zu werfen?


129

Der Indexer in Dictionary löst eine Ausnahme aus, wenn der Schlüssel fehlt. Gibt es eine Implementierung von IDictionary, die stattdessen den Standardwert (T) zurückgibt?

Ich kenne die "TryGetValue" -Methode, aber das ist mit linq unmöglich zu verwenden.

Würde dies effizient das tun, was ich brauche?:

myDict.FirstOrDefault(a => a.Key == someKeyKalue);

Ich denke nicht, dass es so sein wird, wie ich denke, dass es die Schlüssel iterieren wird, anstatt eine Hash-Suche zu verwenden.


10
Siehe auch diese spätere Frage, die als Duplikat dieser Frage markiert ist, aber unterschiedliche Antworten enthält: Wörterbuch, das einen Standardwert zurückgibt, wenn der Schlüssel nicht vorhanden ist
Rory O'Kane,

2
@dylan Das löst eine Ausnahme bei fehlendem Schlüssel aus, nicht null. Außerdem müsste entschieden werden, welche Standardeinstellung überall dort verwendet werden soll, wo das Wörterbuch verwendet wird. Beachten Sie auch das Alter dieser Frage. Wir hatten das nicht? Betreiber sowieso vor 8 Jahren
TheSoftwareJedi

Antworten:


147

In der Tat wird das überhaupt nicht effizient sein.

Sie können jederzeit eine Erweiterungsmethode schreiben:

public static TValue GetValueOrDefault<TKey,TValue>
    (this IDictionary<TKey, TValue> dictionary, TKey key)
{
    TValue ret;
    // Ignore return value
    dictionary.TryGetValue(key, out ret);
    return ret;
}

Oder mit C # 7.1:

public static TValue GetValueOrDefault<TKey,TValue>
    (this IDictionary<TKey, TValue> dictionary, TKey key) =>
    dictionary.TryGetValue(key, out var ret) ? ret : default;

Das verwendet:

  • Eine Methode mit Ausdruckskörper (C # 6)
  • Eine Out-Variable (C # 7.0)
  • Ein Standardliteral (C # 7.1)

2
Oder kompakter:return (dictionary.ContainsKey(key)) ? dictionary[key] : default(TValue);
Peter Gluck

33
@PeterGluck: Kompakter, aber weniger effizient ... warum zwei Suchvorgänge durchführen, wenn der Schlüssel vorhanden ist?
Jon Skeet

5
@ JonSkeet: Danke, dass du Peter korrigiert hast. Ich benutze diese "weniger effiziente" Methode seit einiger Zeit, ohne es zu merken.
J Bryan Price

5
Sehr schön! Ich werde schamlos plagiieren - ähm, gib es. ;)
Jon Coombs

3
Anscheinend hat MS entschieden, dass dies gut genug ist, um es zu ergänzen System.Collections.Generic.CollectionExtensions, da ich es gerade versucht habe und es da ist.
theMayer

19

Das Tragen dieser Erweiterungsmethoden kann helfen.

public static V GetValueOrDefault<K, V>(this IDictionary<K, V> dict, K key)
{
    return dict.GetValueOrDefault(key, default(V));
}

public static V GetValueOrDefault<K, V>(this IDictionary<K, V> dict, K key, V defVal)
{
    return dict.GetValueOrDefault(key, () => defVal);
}

public static V GetValueOrDefault<K, V>(this IDictionary<K, V> dict, K key, Func<V> defValSelector)
{
    V value;
    return dict.TryGetValue(key, out value) ? value : defValSelector();
}

3
Die letzte Überlastung ist interessant. Da gibt es nichts zu wählen ist aus , dann ist dies einfach eine Form von lazy evaluation?
MEMark

1
@MEMark yes value selector wird nur bei Bedarf ausgeführt. In gewisser Weise kann dies als verzögerte Auswertung bezeichnet werden, die Ausführung wird jedoch nicht wie im Linq-Kontext verschoben. Aber eine verzögerte Auswertung hängt hier nicht davon ab, dass nichts aus dem Faktor ausgewählt werden kann. Sie können natürlich die Auswahl treffen Func<K, V>.
Nawfal

1
Ist diese dritte Methode öffentlich, weil sie für sich genommen nützlich ist? Das heißt, denken Sie, jemand könnte ein Lambda übergeben, das die aktuelle Zeit verwendet, oder eine Berechnung basierend auf ... hmm. Ich hätte erwartet, dass nur die zweite Methode V-Wert macht; return dict.TryGetValue (Schlüssel, Out-Wert)? Wert: defVal;
Jon Coombs

1
@JCoombs richtig, die dritte Überladung ist für den gleichen Zweck da. Und du kannst das natürlich in der zweiten Überlastung tun, aber ich habe es ein bisschen trocken gemacht (damit ich die Logik nicht wiederhole).
Nawfal

4
@nawfal Guter Punkt. Trocken zu bleiben ist wichtiger als einen dürftigen Methodenaufruf zu vermeiden.
Jon Coombs

19

Wenn jemand .net Core 2 und höher (C # 7.X) verwendet, wird die CollectionExtensions- Klasse eingeführt und kann die GetValueOrDefault- Methode verwenden, um den Standardwert abzurufen , wenn der Schlüssel nicht in einem Wörterbuch vorhanden ist.

Dictionary<string, string> colorData = new  Dictionary<string, string>();
string color = colorData.GetValueOrDefault("colorId", string.Empty);

4

Collections.Specialized.StringDictionaryliefert ein Ergebnis ohne Ausnahme, wenn der Wert eines fehlenden Schlüssels nachgeschlagen wird. Standardmäßig wird auch die Groß- und Kleinschreibung nicht berücksichtigt.

Vorsichtsmaßnahmen

Es ist nur für spezielle Zwecke gültig und verfügt - da es vor Generika entwickelt wurde - nicht über einen sehr guten Enumerator, wenn Sie die gesamte Sammlung überprüfen müssen.


4

Wenn Sie .Net Core verwenden, können Sie die CollectionExtensions.GetValueOrDefault- Methode verwenden. Dies entspricht der Implementierung in der akzeptierten Antwort.

public static TValue GetValueOrDefault<TKey,TValue> (
   this System.Collections.Generic.IReadOnlyDictionary<TKey,TValue> dictionary,
   TKey key);

3
public class DefaultIndexerDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
    private IDictionary<TKey, TValue> _dict = new Dictionary<TKey, TValue>();

    public TValue this[TKey key]
    {
        get
        {
            TValue val;
            if (!TryGetValue(key, out val))
                return default(TValue);
            return val;
        }

        set { _dict[key] = value; }
    }

    public ICollection<TKey> Keys => _dict.Keys;

    public ICollection<TValue> Values => _dict.Values;

    public int Count => _dict.Count;

    public bool IsReadOnly => _dict.IsReadOnly;

    public void Add(TKey key, TValue value)
    {
        _dict.Add(key, value);
    }

    public void Add(KeyValuePair<TKey, TValue> item)
    {
        _dict.Add(item);
    }

    public void Clear()
    {
        _dict.Clear();
    }

    public bool Contains(KeyValuePair<TKey, TValue> item)
    {
        return _dict.Contains(item);
    }

    public bool ContainsKey(TKey key)
    {
        return _dict.ContainsKey(key);
    }

    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    {
        _dict.CopyTo(array, arrayIndex);
    }

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        return _dict.GetEnumerator();
    }

    public bool Remove(TKey key)
    {
        return _dict.Remove(key);
    }

    public bool Remove(KeyValuePair<TKey, TValue> item)
    {
        return _dict.Remove(item);
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        return _dict.TryGetValue(key, out value);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _dict.GetEnumerator();
    }
}

1

Man könnte eine Schnittstelle für die Schlüsselsuchfunktion eines Wörterbuchs definieren. Ich würde es wahrscheinlich so definieren wie:

Interface IKeyLookup(Of Out TValue)
  Function Contains(Key As Object)
  Function GetValueIfExists(Key As Object) As TValue
  Function GetValueIfExists(Key As Object, ByRef Succeeded As Boolean) As TValue
End Interface

Interface IKeyLookup(Of In TKey, Out TValue)
  Inherits IKeyLookup(Of Out TValue)
  Function Contains(Key As TKey)
  Function GetValue(Key As TKey) As TValue
  Function GetValueIfExists(Key As TKey) As TValue
  Function GetValueIfExists(Key As TKey, ByRef Succeeded As Boolean) As TValue
End Interface

Die Version mit nicht generischen Schlüsseln würde Code zulassen, der Code mit nicht strukturierten Schlüsseltypen verwendet, um eine beliebige Schlüsselvarianz zu ermöglichen, die mit einem generischen Typparameter nicht möglich wäre. Man sollte nicht erlaubt sein, eine veränderbare Dictionary(Of Cat, String)als veränderbare zu verwenden, Dictionary(Of Animal, String)da letztere dies erlauben würde SomeDictionaryOfCat.Add(FionaTheFish, "Fiona"). Aber es ist nichts Falsches daran, eine veränderbare Dictionary(Of Cat, String)als unveränderlich zu verwenden Dictionary(Of Animal, String), da SomeDictionaryOfCat.Contains(FionaTheFish)sie als perfekt geformter Ausdruck betrachtet werden sollte (sie sollte false, ohne das Wörterbuch durchsuchen zu müssen, nach etwas zurückkehren, das nicht vom Typ istCat ).

Leider kann man eine solche Schnittstelle nur dann tatsächlich verwenden, wenn man ein DictionaryObjekt in eine Klasse einschließt, die die Schnittstelle implementiert. Je nachdem, was Sie tun, ist eine solche Schnittstelle und die zulässige Varianz möglicherweise die Mühe wert.


1

Wenn Sie ASP.NET MVC verwenden, können Sie die RouteValueDictionary-Klasse nutzen, die den Job ausführt.

public object this[string key]
{
  get
  {
    object obj;
    this.TryGetValue(key, out obj);
    return obj;
  }
  set
  {
    this._dictionary[key] = value;
  }
}

1

Diese Frage half zu bestätigen, dass das TryGetValuespieltFirstOrDefault hier Rolle .

Eine interessante C # 7-Funktion, die ich erwähnen möchte, ist die Out-Variablen- Funktion. Wenn Sie der Gleichung den nullbedingten Operator von C # 6 hinzufügen, könnte Ihr Code viel einfacher sein, ohne dass zusätzliche Erweiterungsmethoden erforderlich sind.

var dic = new Dictionary<string, MyClass>();
dic.TryGetValue("Test", out var item);
item?.DoSomething();

Der Nachteil dabei ist, dass Sie nicht alles so inline machen können.

dic.TryGetValue("Test", out var item)?.DoSomething();

Wenn wir dies brauchen / wollen, sollten wir eine Erweiterungsmethode wie die von Jon codieren.


1

Hier ist eine Version von @ JonSkeet für die Welt von C # 7.1, mit der auch ein optionaler Standard übergeben werden kann:

public static TV GetValueOrDefault<TK, TV>(this IDictionary<TK, TV> dict, TK key, TV defaultValue = default) => dict.TryGetValue(key, out TV value) ? value : defaultValue;

Es kann effizienter sein, zwei Funktionen zu haben, um den Fall zu optimieren, in dem Sie zurückkehren möchten default(TV):

public static TV GetValueOrDefault<TK, TV>(this IDictionary<TK, TV> dict, TK key, TV defaultValue) => dict.TryGetValue(key, out TV value) ? value : defaultValue;
public static TV GetValueOrDefault2<TK, TV>(this IDictionary<TK, TV> dict, TK key) {
    dict.TryGetValue(key, out TV value);
    return value;
}

Leider hat C # (noch?) Keinen Kommaoperator (oder den von C # 6 vorgeschlagenen Semikolonoperator), so dass Sie für eine der Überladungen einen tatsächlichen Funktionskörper (keuchen!) Haben müssen.


1

Ich habe die Kapselung verwendet, um ein IDictionary mit einem Verhalten zu erstellen, das einer STL-Map sehr ähnlich ist , für diejenigen unter Ihnen, die mit C ++ vertraut sind. Für diejenigen, die nicht sind:

  • Der Indexer get {} in SafeDictionary unten gibt den Standardwert zurück, wenn kein Schlüssel vorhanden ist, und fügt diesen Schlüssel dem Wörterbuch mit einem Standardwert hinzu. Dies ist häufig das gewünschte Verhalten, da Sie nach Elementen suchen, die irgendwann angezeigt werden oder eine gute Chance haben, angezeigt zu werden.
  • Die Methode Add (TK-Taste, TV-Wert) verhält sich wie eine AddOrUpdate-Methode und ersetzt den vorhandenen Wert, falls vorhanden, anstatt zu werfen. Ich verstehe nicht, warum m $ keine AddOrUpdate-Methode hat und halte es für eine gute Idee, Fehler in sehr häufigen Szenarien zu werfen.

TL / DR - SafeDictionary wurde so geschrieben, dass unter keinen Umständen Ausnahmen außer in perversen Szenarien ausgelöst werden , z. B. wenn der Computer nicht Speicher hat (oder in Flammen steht). Dazu wird Add durch AddOrUpdate-Verhalten ersetzt und der Standardwert zurückgegeben, anstatt NotFoundException aus dem Indexer auszulösen.

Hier ist der Code:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

public class SafeDictionary<TK, TD>: IDictionary<TK, TD> {
    Dictionary<TK, TD> _underlying = new Dictionary<TK, TD>();
    public ICollection<TK> Keys => _underlying.Keys;
    public ICollection<TD> Values => _underlying.Values;
    public int Count => _underlying.Count;
    public bool IsReadOnly => false;

    public TD this[TK index] {
        get {
            TD data;
            if (_underlying.TryGetValue(index, out data)) {
                return data;
            }
            _underlying[index] = default(TD);
            return default(TD);
        }
        set {
            _underlying[index] = value;
        }
    }

    public void CopyTo(KeyValuePair<TK, TD>[] array, int arrayIndex) {
        Array.Copy(_underlying.ToArray(), 0, array, arrayIndex,
            Math.Min(array.Length - arrayIndex, _underlying.Count));
    }


    public void Add(TK key, TD value) {
        _underlying[key] = value;
    }

    public void Add(KeyValuePair<TK, TD> item) {
        _underlying[item.Key] = item.Value;
    }

    public void Clear() {
        _underlying.Clear();
    }

    public bool Contains(KeyValuePair<TK, TD> item) {
        return _underlying.Contains(item);
    }

    public bool ContainsKey(TK key) {
        return _underlying.ContainsKey(key);
    }

    public IEnumerator<KeyValuePair<TK, TD>> GetEnumerator() {
        return _underlying.GetEnumerator();
    }

    public bool Remove(TK key) {
        return _underlying.Remove(key);
    }

    public bool Remove(KeyValuePair<TK, TD> item) {
        return _underlying.Remove(item.Key);
    }

    public bool TryGetValue(TK key, out TD value) {
        return _underlying.TryGetValue(key, out value);
    }

    IEnumerator IEnumerable.GetEnumerator() {
        return _underlying.GetEnumerator();
    }
}

1

Seit .NET Core 2.0 können Sie Folgendes verwenden:

myDict.GetValueOrDefault(someKeyKalue)

0

Was ist mit diesem ContainsKeyEinzeiler , der prüft, ob ein Schlüssel vorhanden ist, und dann entweder den normalerweise erhaltenen Wert oder den Standardwert mit dem bedingten Operator zurückgibt?

var myValue = myDictionary.ContainsKey(myKey) ? myDictionary[myKey] : myDefaultValue;

Sie müssen keine neue Dictionary-Klasse implementieren, die Standardwerte unterstützt. Ersetzen Sie einfach Ihre Lookup-Anweisungen durch die kurze Zeile oben.


5
Dies hat zwei Suchvorgänge anstelle von einem und führt eine Racebedingung ein, wenn Sie ConcurrentDictionary verwenden.
Piedar

0

Es könnte ein Einzeiler sein, um den TryGetValueStandardwert zu überprüfen und zurückzugeben, wenn dies der Fall ist false.

 Dictionary<string, int> myDic = new Dictionary<string, int>() { { "One", 1 }, { "Four", 4} };
 string myKey = "One"
 int value = myDic.TryGetValue(myKey, out value) ? value : 100;

myKey = "One" => value = 1

myKey = "two" => value = 100

myKey = "Four" => value = 4

Probieren Sie es online aus


-1

Nein, denn wie würden Sie sonst den Unterschied erkennen, wenn der Schlüssel vorhanden ist, aber einen Nullwert gespeichert hat? Das könnte von Bedeutung sein.


11
Es könnte sein - aber in einigen Situationen wissen Sie vielleicht, dass es nicht so ist. Ich kann sehen, dass dies manchmal nützlich ist.
Jon Skeet

8
Wenn Sie wissen, dass null nicht vorhanden ist, ist dies äußerst schön zu haben. Es macht das Verbinden dieser Dinge in Linq (was alles ist, was ich in letzter Zeit mache) so viel einfacher
TheSoftwareJedi

1
Mit der ContainsKey-Methode?
MattDavey

4
Ja, es ist tatsächlich sehr nützlich. Python hat dies und ich benutze es weit mehr als die einfache Methode, wenn ich zufällig in Python bin. Spart das Schreiben einer Menge zusätzlicher if-Anweisungen, die nur auf Null prüfen. (Sie haben natürlich Recht, dass null gelegentlich nützlich ist, aber normalerweise möchte ich stattdessen ein "" oder 0).
Jon Coombs

Ich hatte das positiv bewertet. Aber wenn es heute wäre, hätte ich es nicht getan. Kann jetzt nicht stornieren :). Ich habe die Kommentare positiv bewertet, um meinen Standpunkt zu verdeutlichen :)
Nawfal
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.