Esiste un'implementazione IDictionary che, sulla chiave mancante, restituisce il valore predefinito invece di lanciare?


129

L'indicizzatore nel dizionario genera un'eccezione se manca la chiave. Esiste un'implementazione di IDictionary che invece restituirà il valore predefinito (T)?

Conosco il metodo "TryGetValue", ma è impossibile da usare con linq.

Questo farebbe in modo efficiente ciò di cui ho bisogno ?:

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

Non penso che lo farà perché penso che itererà le chiavi invece di utilizzare una ricerca hash.


10
Vedi anche questa domanda successiva, contrassegnata come duplicata di questa, ma con risposte diverse: Dizionario che restituisce un valore predefinito se la chiave non esiste
Rory O'Kane

2
@dylan Questo genererà un'eccezione sulla chiave mancante, non nulla. Inoltre, sarebbe necessario decidere quale dovrebbe essere l'impostazione predefinita ovunque venga utilizzato il dizionario. Prendi anche nota dell'età di questa domanda. Non abbiamo avuto il ?? operatore 8 anni fa
TheSoftwareJedi

Risposte:


147

In effetti, ciò non sarà affatto efficiente.

Puoi sempre scrivere un metodo di estensione:

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

O con C # 7.1:

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

Che utilizza:

  • Un metodo basato sull'espressione (C # 6)
  • Una variabile out (C # 7.0)
  • Un valore letterale predefinito (C # 7.1)

2
O in modo più compatto:return (dictionary.ContainsKey(key)) ? dictionary[key] : default(TValue);
Peter Gluck,

33
@PeterGluck: più compatto, ma meno efficiente ... perché eseguire due ricerche nel caso in cui la chiave è presente?
Jon Skeet,

5
@JonSkeet: Grazie per aver corretto Peter; Sto usando quel metodo "meno efficiente" senza rendermene conto per un po 'di tempo.
J Bryan Price

5
Molto bella! Ho intenzione di plagiare spudoratamente - ehm, biforcarlo. ;)
Jon Coombs il

3
Apparentemente MS ha deciso che questo era abbastanza buono da aggiungere System.Collections.Generic.CollectionExtensions, come ho appena provato ed è lì.
theMayer

19

Portare questi metodi di estensione può aiutare ..

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
L'ultimo sovraccarico è interessante. Dal momento che non c'è nulla da selezionare da , è questo semplicemente una forma di valutazione pigra?
MEM

1
@MEMark yes Il selettore di valori viene eseguito solo se necessario, quindi in un certo senso può essere definito come una valutazione pigra, ma la sua esecuzione non è differita come nel contesto di Linq. Ma la valutazione pigra qui non dipende da nulla da selezionare dal fattore, ovviamente puoi fare il selettore Func<K, V>.
nawfal,

1
Quel terzo metodo è pubblico perché è utile a sé stante? Cioè, stai pensando che qualcuno potrebbe passare un lambda che utilizza l'ora corrente o un calcolo basato su ... hmm. Mi sarei aspettato di avere solo il secondo metodo con valore V; restituire dict.TryGetValue (chiave, valore out)? valore: defVal;
Jon Coombs,

1
@JCoombs giusto, il terzo sovraccarico è lì per lo stesso scopo. E ovviamente puoi farlo nel secondo sovraccarico, ma lo stavo rendendo un po ' secco (in modo da non ripetere la logica).
nawfal,

4
@nawfal Un buon punto. Rimanere asciutti è più importante che evitare una chiamata con metodo misero.
Jon Coombs,

19

Se qualcuno utilizza .net core 2 e versioni successive (C # 7.X), viene introdotta la classe CollectionExtensions e può utilizzare il metodo GetValueOrDefault per ottenere il valore predefinito se la chiave non è presente in un dizionario.

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

4

Collections.Specialized.StringDictionaryfornisce un risultato senza eccezioni quando si cerca il valore di una chiave mancante. È anche insensibile alle maiuscole per impostazione predefinita.

Avvertenze

È valido solo per i suoi usi specializzati e - essendo progettato prima dei generici - non ha un ottimo enumeratore se è necessario rivedere l'intera collezione.


4

Se si utilizza .Net Core, è possibile utilizzare il metodo CollectionExtensions.GetValueOrDefault . È uguale all'implementazione fornita nella risposta accettata.

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

Si potrebbe definire un'interfaccia per la funzione di ricerca dei tasti di un dizionario. Probabilmente lo definirei come qualcosa di simile:

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

La versione con chiavi non generiche consentirebbe il codice che utilizzava il codice utilizzando tipi di chiavi non strutturali per consentire una varianza arbitraria delle chiavi, che non sarebbe possibile con un parametro di tipo generico. Non si dovrebbe permettere di usare un mutevole Dictionary(Of Cat, String)come mutevole Dictionary(Of Animal, String)poiché quest'ultimo lo permetterebbeSomeDictionaryOfCat.Add(FionaTheFish, "Fiona") . Ma non c'è niente di sbagliato nell'usare un mutevole Dictionary(Of Cat, String)come immutabile Dictionary(Of Animal, String), poiché SomeDictionaryOfCat.Contains(FionaTheFish)dovrebbe essere considerato un'espressione perfettamente ben formata (dovrebbe restituire false, senza dover cercare nel dizionario, tutto ciò che non è di tipo Cat).

Sfortunatamente, l'unico modo in cui uno sarà effettivamente in grado di utilizzare tale interfaccia è se si avvolge a Dictionary oggetto in una classe che implementa l'interfaccia. A seconda di ciò che stai facendo, tuttavia, tale interfaccia e la varianza che consente potrebbero valere la pena.


1

Se si utilizza ASP.NET MVC, è possibile sfruttare la classe RouteValueDictionary che esegue il lavoro.

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

1

Questa domanda ha contribuito a confermare che il TryGetValuegiocoFirstOrDefault qui ruolo.

Una caratteristica interessante di C # 7 che vorrei menzionare è la caratteristica delle variabili out , e se aggiungi l' operatore null-conditional da C # 6 all'equazione il tuo codice potrebbe essere molto più semplice senza la necessità di ulteriori metodi di estensione.

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

L'aspetto negativo di questo è che non puoi fare tutto in linea in questo modo;

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

Se avessimo bisogno / desideri farlo dovremmo codificare un metodo di estensione come quello di Jon.


1

Ecco una versione di @ JonSkeet per il mondo di C # 7.1 che consente anche di passare un valore predefinito opzionale:

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

Potrebbe essere più efficiente disporre di due funzioni per ottimizzare il caso in cui si desidera tornare 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;
}

Sfortunatamente C # non ha (ancora?) Un operatore virgola (o l'operatore punto e virgola C # 6 proposto) quindi devi avere un vero corpo di funzione (sussulto!) Per uno dei sovraccarichi.


1

Ho usato l'incapsulamento per creare un IDictionary con un comportamento molto simile a una mappa STL , per quelli di voi che hanno familiarità con c ++. Per coloro che non lo sono:

  • l'indicizzatore get {} in SafeDictionary di seguito restituisce il valore predefinito se non è presente una chiave e aggiunge quella chiave al dizionario con un valore predefinito. Questo è spesso il comportamento desiderato, poiché stai cercando oggetti che appariranno alla fine o che hai buone probabilità di apparire.
  • Il metodo Add (tasto TK, TV val) si comporta come un metodo AddOrUpdate, sostituendo il valore presente se esiste invece di lanciare. Non vedo perché m $ non abbia un metodo AddOrUpdate e pensa che lanciare errori in scenari molto comuni sia una buona idea.

TL / DR: SafeDictionary è scritto in modo tale da non generare eccezioni in nessun caso, tranne che in scenari perversi , come la memoria del computer (o in fiamme). Lo fa sostituendo Aggiungi con il comportamento AddOrUpdate e restituendo il valore predefinito invece di lanciare NotFoundException dall'indicizzatore.

Ecco il codice:

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

Da .NET core 2.0 è possibile utilizzare:

myDict.GetValueOrDefault(someKeyKalue)

0

Che dire di questo one-liner che controlla se una chiave è presente usando ContainsKeye quindi restituisce il valore normalmente recuperato o il valore predefinito usando l'operatore condizionale?

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

Non è necessario implementare una nuova classe di dizionario che supporti i valori predefiniti, è sufficiente sostituire le istruzioni di ricerca con la breve riga sopra.


5
Ha due ricerche anziché una e introduce una condizione di competizione se si utilizza ConcurrentDictionary.
piedar

0

Potrebbe essere una riga per verificare TryGetValuee restituire il valore predefinito, se lo è 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

Provalo online


-1

No, perché altrimenti come riconosceresti la differenza quando la chiave esiste ma memorizzi un valore nullo? Questo potrebbe essere significativo.


11
Potrebbe essere - ma in alcune situazioni potresti sapere che non lo è. Vedo che questo è utile a volte.
Jon Skeet,

8
Se sai che null non è presente, è estremamente bello averlo. Rende molto più semplice unire queste cose a Linq (che è tutto ciò che faccio di recente)
TheSoftwareJedi

1
Utilizzando il metodo ContainsKey?
MattDavey,

4
Sì, in realtà è molto utile. Python ha questo e lo uso molto più del semplice metodo, quando mi capita di trovarmi in Python. Risparmia di scrivere un sacco di extra se le istruzioni che stanno solo controllando il risultato null. (Hai ragione, ovviamente, che null è occasionalmente utile, ma di solito voglio un "" o 0 invece.)
Jon Coombs

Ho votato questo. Ma se fosse oggi non avrei. Non posso annullare ora :). Ho votato a favore dei commenti per esprimere il mio punto :)
nawfal,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.