Czy istnieje implementacja IDictionary, która w przypadku braku klucza zwraca wartość domyślną zamiast rzucać?

129

Indeksator do słownika zgłasza wyjątek, jeśli brakuje klucza. Czy istnieje implementacja IDictionary, która zamiast tego zwróci wartość default (T)?

Wiem o metodzie „TryGetValue”, ale nie da się jej użyć z linq.

Czy to skutecznie zrobi to, czego potrzebuję ?:

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

Nie sądzę, żeby tak było, ponieważ myślę, że będzie iterować klucze zamiast używać wyszukiwania skrótu.

TheSoftwareJedi
źródło
10
Zobacz także to późniejsze pytanie, oznaczone jako duplikat tego, ale z różnymi odpowiedziami: Słownik zwraca wartość domyślną, jeśli klucz nie istnieje
Rory O'Kane
2
@dylan To spowoduje zgłoszenie wyjątku dla brakującego klucza, a nie null. Ponadto wymagałoby to podjęcia decyzji, jaka powinna być wartość domyślna wszędzie tam, gdzie używany jest słownik. Zwróć także uwagę na wiek tego pytania. Nie mieliśmy ?? operator w każdym razie 8 lat temu
TheSoftwareJedi

Odpowiedzi:

147

Rzeczywiście, to wcale nie będzie skuteczne.

Zawsze możesz napisać metodę rozszerzenia:

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

Lub z C # 7.1:

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

To używa:

  • Metoda z wyrażeniem (C # 6)
  • Zmienna wyjściowa (C # 7.0)
  • Domyślny literał (C # 7.1)
Jon Skeet
źródło
2
Lub bardziej zwięźle:return (dictionary.ContainsKey(key)) ? dictionary[key] : default(TValue);
Peter Gluck
33
@PeterGluck: Bardziej zwięzłe, ale mniej wydajne ... po co przeprowadzać dwa wyszukiwania w przypadku, gdy klucz jest obecny?
Jon Skeet
5
@JonSkeet: Dzięki za poprawienie Petera; Używam tej „mniej wydajnej” metody, nie zdając sobie z tego sprawy od jakiegoś czasu.
J Bryan Price,
5
Bardzo dobrze! Mam zamiar bezwstydnie plagiatować - hm, rozwidlić to. ;)
Jon Coombs
3
Najwyraźniej stwardnienie rozsiane zdecydowało, że to wystarczy, aby dodać System.Collections.Generic.CollectionExtensions, ponieważ właśnie próbowałem i jest.
Mayer
19

Noszenie tych metod rozszerzeń może pomóc.

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();
}
nawfal
źródło
3
Ostatnie przeciążenie jest interesujące. Ponieważ nie ma nic, aby wybrać z , to jest po prostu formą oceny leniwy?
MEMark
1
@MEMark yes selektor wartości jest uruchamiany tylko w razie potrzeby, więc w pewnym sensie można go powiedzieć jako leniwą ocenę, ale nie jest to odroczone wykonanie, jak w kontekście Linq. Ale leniwa ocena tutaj nie jest zależna od niczego do wyboru z czynnika, możesz oczywiście dokonać selektora Func<K, V>.
nawfal
1
Czy ta trzecia metoda jest publiczna, ponieważ jest użyteczna sama w sobie? To znaczy, czy myślisz, że ktoś może podać lambdę używającą bieżącego czasu, czy też obliczenia oparte na ... hmm. Spodziewałbym się po prostu drugiej metody do wartości V; return dict.TryGetValue (klucz, wartość wyjściowa)? wartość: defVal;
Jon Coombs,
1
@JCoombs racja, trzecie przeciążenie istnieje w tym samym celu. I tak, oczywiście, możesz to zrobić w drugim przeciążeniu, ale robiłem to trochę sucho (żeby nie powtarzać logiki).
nawfal
4
@nawfal Słuszna uwaga. Pozostawanie suchym jest ważniejsze niż unikanie jednej marnej metody.
Jon Coombs
19

Jeśli ktoś używa .net core 2 i nowszych (C # 7.X), wprowadzana jest klasa CollectionExtensions i może użyć metody GetValueOrDefault , aby uzyskać wartość domyślną, jeśli klucza nie ma w słowniku.

Dictionary<string, string> colorData = new  Dictionary<string, string>();
string color = colorData.GetValueOrDefault("colorId", string.Empty);
cdev
źródło
4

Collections.Specialized.StringDictionaryzapewnia wynik niebędący wyjątkiem podczas wyszukiwania wartości brakującego klucza. Domyślnie wielkość liter nie jest rozróżniana.

Ostrzeżenia

Jest ważny tylko do wyspecjalizowanych zastosowań i - ponieważ został zaprojektowany przed typami ogólnymi - nie ma bardzo dobrego modułu wyliczającego, jeśli chcesz przejrzeć całą kolekcję.

Mark Hurd
źródło
4

Jeśli używasz .Net Core, możesz użyć metody CollectionExtensions.GetValueOrDefault . To jest to samo, co implementacja podana w zaakceptowanej odpowiedzi.

public static TValue GetValueOrDefault<TKey,TValue> (
   this System.Collections.Generic.IReadOnlyDictionary<TKey,TValue> dictionary,
   TKey key);
theMayer
źródło
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();
    }
}
Brian
źródło
1

Można zdefiniować interfejs dla funkcji wyszukiwania kluczy w słowniku. Prawdopodobnie zdefiniowałbym to jako coś takiego:

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

Wersja z kluczami nieogólnymi pozwoliłaby kodowi, który używał kodu przy użyciu typów kluczy innych niż strukturalne, aby umożliwić dowolną wariancję klucza, co nie byłoby możliwe w przypadku parametru typu ogólnego. Nie powinno się pozwalać na używanie mutable Dictionary(Of Cat, String)jako mutable, Dictionary(Of Animal, String)ponieważ ta ostatnia na to pozwala SomeDictionaryOfCat.Add(FionaTheFish, "Fiona"). Ale nie ma nic złego w używaniu mutable Dictionary(Of Cat, String)jako niezmiennego Dictionary(Of Animal, String), ponieważ SomeDictionaryOfCat.Contains(FionaTheFish)powinno być uważane za doskonale sformułowane wyrażenie (powinno zwracać false, bez konieczności przeszukiwania słownika, niczego, co nie jest typu Cat).

Niestety, jedynym sposobem, w jaki można faktycznie użyć takiego interfejsu, jest umieszczenie Dictionaryobiektu w klasie implementującej interfejs. Jednak w zależności od tego, co robisz, taki interfejs i możliwe warianty mogą sprawić, że będzie to warte wysiłku.

superkat
źródło
1

Jeśli używasz ASP.NET MVC, możesz wykorzystać klasę RouteValueDictionary, która wykonuje to zadanie.

public object this[string key]
{
  get
  {
    object obj;
    this.TryGetValue(key, out obj);
    return obj;
  }
  set
  {
    this._dictionary[key] = value;
  }
}
Jone Polvora
źródło
1

To pytanie pomogło potwierdzić, że TryGetValuegra FirstOrDefaulttutaj rolę.

Jedną z interesujących funkcji języka C # 7, o którym chciałbym wspomnieć, jest funkcja zmiennych wyjściowych, a jeśli dodasz operator warunkowy zerowy z C # 6 do równania, kod może być znacznie prostszy bez konieczności stosowania dodatkowych metod rozszerzających.

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

Wadą tego jest to, że nie da się zrobić wszystkiego w ten sposób;

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

Jeśli potrzebowalibyśmy / chcielibyśmy to zrobić, powinniśmy zakodować jedną metodę rozszerzenia, taką jak Jon's.

hardkoded
źródło
1

Oto wersja @ JonSkeet dla świata C # 7.1, która pozwala również na przekazanie opcjonalnej wartości domyślnej:

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

Bardziej wydajne może być posiadanie dwóch funkcji, aby zoptymalizować przypadek, do którego chcesz zwrócić 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;
}

Niestety C # nie ma (jeszcze?) Operatora przecinka (lub operatora C # 6 proponowanego średnika), więc musisz mieć rzeczywistą treść funkcji (sap!) Dla jednego z przeciążeń.

NetMage
źródło
1

Użyłem hermetyzacji, aby stworzyć IDictionary o zachowaniu bardzo podobnym do mapy STL , dla tych z Was, którzy znają c ++. Dla tych, którzy nie są:

  • indeksator get {} w SafeDictionary poniżej zwraca wartość domyślną, jeśli nie ma klucza, i dodaje ten klucz do słownika z wartością domyślną. Jest to często pożądane zachowanie, ponieważ szukasz przedmiotów, które ostatecznie się pojawią lub mają dużą szansę na pojawienie się.
  • metoda Add (klucz TK, wartość TV) zachowuje się jak metoda AddOrUpdate, zastępując wartość obecną, jeśli istnieje, zamiast rzucać. Nie rozumiem, dlaczego m $ nie ma metody AddOrUpdate i uważa, że ​​rzucanie błędów w bardzo typowych scenariuszach jest dobrym pomysłem.

TL / DR - SafeDictionary jest napisany tak, aby nigdy nie zgłaszać wyjątków w żadnych okolicznościach, innych niż perwersyjne scenariusze , takie jak brak pamięci na komputerze (lub pożar). Robi to, zastępując Add zachowaniem AddOrUpdate i zwracając wartość default zamiast wyrzucania NotFoundException z indeksatora.

Oto kod:

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();
    }
}
nerwowy
źródło
1

Od .NET core 2.0 możesz używać:

myDict.GetValueOrDefault(someKeyKalue)
Antoine L.
źródło
0

A co z tym jednowierszowym, który sprawdza, czy klucz jest obecny, ContainsKeya następnie zwraca normalnie pobraną wartość lub wartość domyślną za pomocą operatora warunkowego?

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

Nie ma potrzeby implementowania nowej klasy Dictionary, która obsługuje wartości domyślne, po prostu zamień instrukcje wyszukiwania na krótką linię powyżej.

Byte Commander
źródło
5
Ma dwa wyszukiwania zamiast jednego i wprowadza warunek wyścigu, jeśli używasz ConcurrentDictionary.
piedar
0

Sprawdzanie TryGetValuei zwracanie wartości domyślnej może być jednoliniowe, jeśli tak jest 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

Wypróbuj online

Aryan Firouzian
źródło
-1

Nie, ponieważ w przeciwnym razie skąd znasz różnicę, gdy klucz istnieje, ale zapisał wartość null? To może być znaczące.

Joel Coehoorn
źródło
11
Może tak być - ale w niektórych sytuacjach możesz dobrze wiedzieć, że tak nie jest. Czasami widzę, że jest to przydatne.
Jon Skeet
8
Jeśli wiesz, że nie ma tam null - to jest bardzo przyjemne. To sprawia, że ​​dołączanie do tych rzeczy w Linq (co jest wszystkim, co robię ostatnio) o wiele łatwiejsze
TheSoftwareJedi
1
Używając metody ContainsKey?
MattDavey,
4
Tak, w rzeczywistości jest to bardzo przydatne. Python to ma i używam go znacznie częściej niż zwykłej metody, kiedy jestem w Pythonie. Oszczędza wiele dodatkowych instrukcji if, które sprawdzają tylko wynik zerowy. (Masz oczywiście rację, ta wartość null jest czasami przydatna, ale zwykle wolę zamiast tego „” lub 0).
Jon Coombs,
Głosowałem za tym. Ale gdyby to było dzisiaj, nie zrobiłbym tego. Nie mogę teraz anulować :). Głosowałem za komentarzami, żeby wyrazić swoją opinię :)
nawfal