¿Existe una implementación IDictionary que, en la clave faltante, devuelve el valor predeterminado en lugar de arrojar?

129

El indexador en el diccionario genera una excepción si falta la clave. ¿Existe una implementación de IDictionary que en su lugar devolverá el valor predeterminado (T)?

Sé sobre el método "TryGetValue", pero eso es imposible de usar con linq.

¿Haría esto eficientemente lo que necesito ?:

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

No creo que lo haga, ya que creo que iterará las teclas en lugar de usar una búsqueda de Hash.

TheSoftwareJedi
fuente
10
Vea también esta pregunta posterior, marcada como un duplicado de esta, pero con diferentes respuestas: Diccionario que devuelve un valor predeterminado si la clave no existe
Rory O'Kane
2
@dylan Eso arrojará una excepción en la clave faltante, no nula. Además, requeriría decidir cuál debería ser el valor predeterminado en todos los lugares donde se usa el diccionario. También tome nota de la edad de esta pregunta. No tuvimos el ?? operador hace 8 años de todos modos
TheSoftwareJedi

Respuestas:

147

De hecho, eso no será eficiente en absoluto.

Siempre puedes escribir un método de extensión:

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;

Que usa:

  • Un método con cuerpo de expresión (C # 6)
  • Una variable de salida (C # 7.0)
  • Un literal predeterminado (C # 7.1)
Jon Skeet
fuente
2
O más compacto:return (dictionary.ContainsKey(key)) ? dictionary[key] : default(TValue);
Peter Gluck
33
@PeterGluck: de forma más compacta, pero menos eficiente ... ¿por qué realizar dos búsquedas en el caso cuando la clave está presente?
Jon Skeet
55
@ JonSkeet: Gracias por corregir a Peter; He estado usando ese método "menos eficiente" sin darme cuenta por un tiempo.
J Bryan Precio
55
¡Muy agradable! Voy a plagiar descaradamente, er, bifurcarlo. ;)
Jon Coombs
3
Aparentemente, MS decidió que esto era lo suficientemente bueno como para agregarlo System.Collections.Generic.CollectionExtensions, ya que lo intenté y está ahí.
theMayer
19

Llevar estos métodos de extensión puede ayudar.

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
fuente
3
La última sobrecarga es interesante. Puesto que no hay nada para seleccionar a partir , se trata simplemente de una forma de evaluación perezosa?
MEMark
1
El selector de valor @MEMark yes solo se ejecuta si es necesario, por lo que, en cierto modo, se puede decir que es una evaluación diferida, pero no es una ejecución diferida como en el contexto de Linq. Pero la evaluación perezosa aquí no depende de nada para seleccionar el factor, por supuesto, puede hacer el selector Func<K, V>.
nawfal
1
¿Es público ese tercer método porque es útil por derecho propio? Es decir, ¿está pensando que alguien podría pasar una lambda que usa la hora actual o un cálculo basado en ... hmm? Hubiera esperado tener el segundo método para hacer el valor V; return dict. TryGetValue (clave, valor de salida)? valor: defVal;
Jon Coombs
1
@JCoombs bien, la tercera sobrecarga está ahí con el mismo propósito. Y, por supuesto, puedes hacerlo en una segunda sobrecarga, pero lo estaba haciendo un poco seco (para que no repita la lógica).
nawfal
44
@nawfal Buen punto. Mantenerse seco es más importante que evitar una llamada de método miserable.
Jon Coombs
19

Si alguien usa .net core 2 y superior (C # 7.X), se introduce la clase CollectionExtensions y puede usar el método GetValueOrDefault para obtener el valor predeterminado si la clave no está en un diccionario.

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

Collections.Specialized.StringDictionaryproporciona un resultado sin excepción al buscar el valor de una clave faltante. También es insensible a mayúsculas y minúsculas por defecto.

Advertencias

Solo es válido para sus usos especializados y, al estar diseñado antes que los genéricos, no tiene un enumerador muy bueno si necesita revisar toda la colección.

Mark Hurd
fuente
4

Si usa .Net Core, puede usar el método CollectionExtensions.GetValueOrDefault . Esto es lo mismo que la implementación proporcionada en la respuesta aceptada.

public static TValue GetValueOrDefault<TKey,TValue> (
   this System.Collections.Generic.IReadOnlyDictionary<TKey,TValue> dictionary,
   TKey key);
theMayer
fuente
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
fuente
1

Se podría definir una interfaz para la función de búsqueda de teclas de un diccionario. Probablemente lo definiría como algo como:

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 versión con claves no genéricas permitiría que el código que usaba código utilizara tipos de clave no estructurados para permitir una variación de clave arbitraria, lo que no sería posible con un parámetro de tipo genérico. No debería permitirse el uso de un mutable Dictionary(Of Cat, String)como mutable Dictionary(Of Animal, String)ya que este último lo permitiría SomeDictionaryOfCat.Add(FionaTheFish, "Fiona"). Pero no hay nada de malo en usar un mutable Dictionary(Of Cat, String)como inmutable Dictionary(Of Animal, String), ya que SomeDictionaryOfCat.Contains(FionaTheFish)debe considerarse una expresión perfectamente bien formada (debe devolver false, sin tener que buscar en el diccionario, cualquier cosa que no sea de tipoCat ).

Desafortunadamente, la única forma en que uno podrá usar una interfaz de este tipo es si envuelve un Dictionaryobjeto en una clase que implementa la interfaz. Sin embargo, dependiendo de lo que esté haciendo, tal interfaz y la variación que permite pueden hacer que valga la pena el esfuerzo.

Super gato
fuente
1

Si está utilizando ASP.NET MVC, podría aprovechar la clase RouteValueDictionary que hace el trabajo.

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

Esta pregunta ayudó a confirmar que las TryGetValuejugadasFirstOrDefault papel aquí.

Una característica interesante de C # 7 que me gustaría mencionar es la característica de variables externas, y si agrega el operador condicional nulo de C # 6 a la ecuación, su código podría ser mucho más simple sin necesidad de métodos de extensión adicionales.

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

La desventaja de esto es que no puedes hacer todo en línea de esta manera;

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

Si necesitamos / queremos hacer esto, debemos codificar un método de extensión como el de Jon.

hardkoded
fuente
1

Aquí hay una versión de @ JonSkeet's para el mundo de C # 7.1 que también permite pasar un valor predeterminado opcional:

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

Puede ser más eficiente tener dos funciones para optimizar el caso en el que desea regresar 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;
}

Desafortunadamente, C # no tiene (¿todavía?) Un operador de coma (o el operador de punto y coma propuesto por C # 6), por lo que debe tener un cuerpo de función real (¡jadeo!) Para una de las sobrecargas.

NetMage
fuente
1

Utilicé la encapsulación para crear un IDictionary con un comportamiento muy similar a un mapa STL , para aquellos de ustedes que están familiarizados con c ++. Para aquellos que no son:

  • indexer get {} en SafeDictionary a continuación devuelve el valor predeterminado si una clave no está presente, y agrega esa clave al diccionario con un valor predeterminado. Este suele ser el comportamiento deseado, ya que está buscando elementos que aparecerán eventualmente o que tendrán una buena posibilidad de aparecer.
  • El método Add (tecla TK, TV val) se comporta como un método AddOrUpdate, reemplazando el valor presente si existe en lugar de arrojarlo. No veo por qué m $ no tiene un método AddOrUpdate y creo que arrojar errores en escenarios muy comunes es una buena idea.

TL / DR: SafeDictionary está escrito para no arrojar excepciones bajo ninguna circunstancia, que no sean escenarios perversos , como que la computadora se quede sin memoria (o en llamas). Para ello, reemplaza Agregar con el comportamiento AddOrUpdate y devuelve el valor predeterminado en lugar de lanzar NotFoundException desde el indexador.

Aquí está el código:

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();
    }
}
neural
fuente
1

Desde .NET core 2.0 puede usar:

myDict.GetValueOrDefault(someKeyKalue)
Antoine L
fuente
0

¿Qué pasa con esta línea única que verifica si una clave está presente usando ContainsKeyy luego devuelve el valor normalmente recuperado o el valor predeterminado usando el operador condicional?

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

No es necesario implementar una nueva clase de Diccionario que admita valores predeterminados, simplemente reemplace sus declaraciones de búsqueda con la línea corta anterior.

Byte Commander
fuente
55
Esto tiene dos búsquedas en lugar de una e introduce una condición de carrera si está usando ConcurrentDictionary.
piedar
0

Podría ser una línea para verificar TryGetValuey devolver el valor predeterminado si es así 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

Pruébalo en línea

Aria Firouzian
fuente
-1

No, porque de lo contrario, ¿cómo podría saber la diferencia cuando la clave existe pero almacena un valor nulo? Eso podría ser significativo.

Joel Coehoorn
fuente
11
Podría ser, pero en algunas situaciones es posible que sepa que no lo es. Puedo ver que esto es útil a veces.
Jon Skeet
8
Si sabes que nulo no está ahí, es extremadamente bueno tenerlo. Hace que une a estas cosas en LINQ (que es todo lo que hago últimamente) mucho más fácil
TheSoftwareJedi
1
¿Usando el método ContainsKey?
MattDavey
44
Sí, en realidad es muy útil. Python tiene esto y lo uso mucho más que el método simple, cuando estoy en Python. Ahorra escribiendo muchos extra si las declaraciones que solo están buscando resultados nulos. (Tiene razón, por supuesto, ese valor nulo es útil ocasionalmente, pero generalmente quiero un "" o 0 en su lugar).
Jon Coombs
Había votado por esto. Pero si fuera hoy no lo habría hecho. No puedo cancelar ahora :). Voté los comentarios para hacer mi punto :)
nawfal