Existe-t-il une implémentation IDictionary qui, en cas de clé manquante, renvoie la valeur par défaut au lieu de la lancer?


129

L'indexeur dans Dictionary lève une exception si la clé est manquante. Existe-t-il une implémentation d'IDictionary qui renverra à la place la valeur par défaut (T)?

Je connais la méthode "TryGetValue", mais c'est impossible à utiliser avec linq.

Est-ce que cela ferait efficacement ce dont j'ai besoin?:

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

Je ne pense pas que ce sera le cas, car je pense que cela itérera les clés au lieu d'utiliser une recherche de hachage.


10
Voir aussi cette dernière question, marquée comme un double de celle-ci, mais avec des réponses différentes: Dictionnaire renvoyant une valeur par défaut si la clé n'existe pas
Rory O'Kane

2
@dylan Cela lèvera une exception sur la clé manquante, non nulle. De plus, il faudrait décider de la valeur par défaut partout où le dictionnaire est utilisé. Prenez également note de l'âge de cette question. Nous n'avions pas le ?? opérateur il y a 8 ans de toute façon
TheSoftwareJedi

Réponses:


147

En effet, ce ne sera pas du tout efficace.

Vous pouvez toujours écrire une méthode d'extension:

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

Ou avec C # 7.1:

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

Cela utilise:

  • Une méthode au corps d'expression (C # 6)
  • Une variable out (C # 7.0)
  • Un littéral par défaut (C # 7.1)

2
Ou de manière plus compacte:return (dictionary.ContainsKey(key)) ? dictionary[key] : default(TValue);
Peter Gluck

33
@PeterGluck: Plus compact, mais moins efficace ... pourquoi effectuer deux recherches dans le cas où la clé est présente?
Jon Skeet

5
@JonSkeet: Merci d'avoir corrigé Peter; J'utilise cette méthode "moins efficace" sans m'en rendre compte depuis un moment maintenant.
J Bryan Price

5
Très agréable! Je vais plagier sans vergogne - euh, fourchette. ;)
Jon Coombs

3
Apparemment, MS a décidé que c'était assez bon pour y ajouter System.Collections.Generic.CollectionExtensions, car j'ai juste essayé et c'est là.
theMayer

19

L'application de ces méthodes d'extension peut vous aider.

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
La dernière surcharge est intéressante. Comme il n'y a rien à sélectionner à partir , est - ce simplement une forme d'évaluation paresseuse?
MEMark

1
Le sélecteur de valeur @MEMark yes n'est exécuté que si nécessaire, donc d'une certaine manière, il peut être considéré comme une évaluation paresseuse, mais son exécution n'est pas différée comme dans le contexte Linq. Mais l'évaluation paresseuse ici ne dépend pas de rien pour sélectionner un facteur, vous pouvez bien sûr faire le sélecteur Func<K, V>.
nawfal

1
Cette troisième méthode est-elle publique parce qu'elle est utile en soi? Autrement dit, pensez-vous que quelqu'un pourrait passer dans un lambda qui utilise l'heure actuelle, ou un calcul basé sur ... hmm. Je me serais attendu à ce que la deuxième méthode fasse la valeur V; return dict.TryGetValue (clé, valeur de sortie)? valeur: defVal;
Jon Coombs

1
@JCoombs à droite, la troisième surcharge est là dans le même but. Et oui, bien sûr, vous pouvez le faire en deuxième surcharge, mais je le rendais un peu sec (pour ne pas répéter la logique).
nawfal

4
@nawfal Bon point. Rester au sec est plus important que d'éviter un appel de méthode misérable.
Jon Coombs

19

Si quelqu'un utilise .net core 2 et supérieur (C # 7.X), la classe CollectionExtensions est introduite et peut utiliser la méthode GetValueOrDefault pour obtenir la valeur par défaut si la clé n'est pas présente dans un dictionnaire.

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

4

Collections.Specialized.StringDictionaryfournit un résultat sans exception lors de la recherche de la valeur d'une clé manquante. Il est également insensible à la casse par défaut.

Mises en garde

Il n'est valable que pour ses utilisations spécialisées et - étant conçu avant les génériques - il n'a pas un très bon énumérateur si vous avez besoin de revoir toute la collection.


4

Si vous utilisez .Net Core, vous pouvez utiliser la méthode CollectionExtensions.GetValueOrDefault . C'est la même chose que l'implémentation fournie dans la réponse acceptée.

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

On pourrait définir une interface pour la fonction de recherche de clé d'un dictionnaire. Je le définirais probablement comme quelque chose comme:

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 version avec des clés non génériques permettrait au code qui utilisait du code utilisant des types de clés non structurés de permettre une variance de clé arbitraire, ce qui ne serait pas possible avec un paramètre de type générique. On ne devrait pas être autorisé à utiliser un mutable Dictionary(Of Cat, String)comme mutable Dictionary(Of Animal, String)puisque ce dernier le permettrait SomeDictionaryOfCat.Add(FionaTheFish, "Fiona"). Mais il n'y a rien de mal à utiliser un mutable Dictionary(Of Cat, String)comme immuable Dictionary(Of Animal, String), car il SomeDictionaryOfCat.Contains(FionaTheFish)doit être considéré comme une expression parfaitement bien formée (il doit renvoyer false, sans avoir à rechercher dans le dictionnaire, tout ce qui n'est pas de type Cat).

Malheureusement, la seule façon de pouvoir réellement utiliser une telle interface est de placer un Dictionaryobjet dans une classe qui implémente l'interface. En fonction de ce que vous faites, cependant, une telle interface et la variance qu'elle permet pourraient en valoir la peine.


1

Si vous utilisez ASP.NET MVC, vous pouvez tirer parti de la classe RouteValueDictionary qui effectue le travail.

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

1

Cette question a permis de confirmer que le TryGetValuejoue le FirstOrDefaultrôle ici.

Une fonctionnalité intéressante de C # 7 que je voudrais mentionner est la fonctionnalité de variables out , et si vous ajoutez l' opérateur conditionnel nul de C # 6 à l'équation, votre code pourrait être beaucoup plus simple sans avoir besoin de méthodes d'extension supplémentaires.

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

L'inconvénient est que vous ne pouvez pas tout faire en ligne comme ça;

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

Si nous avons besoin / voulons faire cela, nous devrions coder une méthode d'extension comme celle de Jon.


1

Voici une version de @ JonSkeet pour le monde de C # 7.1 qui permet également de passer une valeur par défaut facultative:

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

Il peut être plus efficace d'avoir deux fonctions pour optimiser le cas où vous souhaitez retourner 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;
}

Malheureusement, C # n'a pas (encore?) D'opérateur virgule (ou l'opérateur point-virgule proposé par C # 6), vous devez donc avoir un corps de fonction réel (halètement!) Pour l'une des surcharges.


1

J'ai utilisé l'encapsulation pour créer un IDictionary avec un comportement très similaire à une carte STL , pour ceux d'entre vous qui connaissent le c ++. Pour ceux qui ne le sont pas:

  • indexer get {} dans SafeDictionary ci-dessous renvoie la valeur par défaut si une clé n'est pas présente et ajoute cette clé au dictionnaire avec une valeur par défaut. C'est souvent le comportement souhaité, car vous recherchez des éléments qui finiront par apparaître ou qui auront de bonnes chances d'apparaître.
  • La méthode Add (clé TK, TV val) se comporte comme une méthode AddOrUpdate, remplaçant la valeur présente si elle existe au lieu de la lancer. Je ne vois pas pourquoi m $ n'a pas de méthode AddOrUpdate et pense que lancer des erreurs dans des scénarios très courants est une bonne idée.

TL / DR - SafeDictionary est écrit de manière à ne jamais lever d'exceptions en aucune circonstance, autres que des scénarios pervers , tels que l'ordinateur étant à court de mémoire (ou en feu). Pour ce faire, il remplace Add par le comportement AddOrUpdate et renvoie la valeur par défaut au lieu de lancer NotFoundException à partir de l'indexeur.

Voici le 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

Depuis .NET core 2.0, vous pouvez utiliser:

myDict.GetValueOrDefault(someKeyKalue)

0

Qu'en est-il de cette ligne unique qui vérifie si une clé est présente en utilisant ContainsKeyet retourne ensuite soit la valeur normalement récupérée, soit la valeur par défaut à l'aide de l'opérateur conditionnel?

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

Pas besoin d'implémenter une nouvelle classe Dictionary qui prend en charge les valeurs par défaut, remplacez simplement vos instructions de recherche par la ligne courte ci-dessus.


5
Cela a deux recherches au lieu d'une et introduit une condition de concurrence si vous utilisez ConcurrentDictionary.
piedar

0

Il peut s'agir d'une ligne unique pour vérifier TryGetValueet renvoyer la valeur par défaut si c'est le cas 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

Essayez-le en ligne


-1

Non, car sinon comment sauriez-vous la différence lorsque la clé existe mais stocke une valeur nulle? Cela pourrait être important.


11
Cela pourrait l'être - mais dans certaines situations, vous savez peut-être que ce n'est pas le cas. Je peux voir que cela est parfois utile.
Jon Skeet

8
Si vous savez que null n'est pas là - c'est extrêmement agréable à avoir. Cela rend beaucoup plus facile de rejoindre ces choses dans Linq (ce que je fais ces derniers temps)
TheSoftwareJedi

1
En utilisant la méthode ContainsKey?
MattDavey

4
Oui, c'est en fait très utile. Python a cela et je l'utilise beaucoup plus que la méthode simple, quand je suis en Python. Enregistre l'écriture de beaucoup d'instructions if supplémentaires qui ne font que vérifier le résultat nul. (Vous avez raison, bien sûr, que null est parfois utile, mais généralement je veux un "" ou 0 à la place.)
Jon Coombs

J'avais voté pour cela. Mais si c'était aujourd'hui, je ne l'aurais pas fait. Je ne peux pas annuler maintenant :). J'ai voté pour les commentaires pour faire valoir mon point :)
nawfal
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.