¿Hay una mejor manera de usar los diccionarios de C # que TryGetValue?

19

A menudo me encuentro buscando preguntas en línea, y muchas soluciones incluyen diccionarios. Sin embargo, cada vez que trato de implementarlos, obtengo este horrible hedor en mi código. Por ejemplo, cada vez que quiero usar un valor:

int x;
if (dict.TryGetValue("key", out x)) {
    DoSomethingWith(x);
}

Esas son 4 líneas de código para hacer esencialmente lo siguiente: DoSomethingWith(dict["key"])

Escuché que usar la palabra clave out es un anti patrón porque hace que las funciones muten sus parámetros.

Además, a menudo necesito un diccionario "invertido", donde volteo las claves y los valores.

Del mismo modo, a menudo me gustaría recorrer los elementos de un diccionario y convertir las claves o valores en listas, etc. para hacerlo mejor.

Siento que casi siempre hay una forma mejor y más elegante de usar los diccionarios, pero estoy perdido.

Adam B
fuente
77
Pueden existir otras formas, pero generalmente uso primero una ContainsKey antes de intentar obtener un valor.
Robbie Dee
2
Honestamente, con algunas excepciones, si sabe cuáles son las claves de su diccionario con anticipación, probablemente haya una mejor manera. Si no sabe, las claves dict generalmente no deberían estar codificadas en absoluto. Los diccionarios son útiles para trabajar con objetos o datos semiestructurados donde los campos son al menos mayormente ortogonales a la aplicación. En mi opinión, cuanto más se acerquen esos campos a los conceptos empresariales / de dominio relevantes, menos útiles serán los diccionarios para trabajar con esos conceptos.
svidgen
66
@RobbieDee: sin embargo, debes tener cuidado cuando haces eso, ya que hacerlo crea una condición de carrera. Es posible que la clave se elimine entre llamar a ContainsKey y obtener el valor.
cuál es el
12
@whatsisname: en esas condiciones, un ConcurrentDictionary sería más adecuado. Las colecciones en el System.Collections.Genericespacio de nombres no son seguras para subprocesos .
Robert Harvey
44
Un buen 50% del tiempo veo a alguien usando un Dictionary, lo que realmente quería era una nueva clase. Para esos 50% (solo), es un olor de diseño.
BlueRaja - Danny Pflughoeft

Respuestas:

23

Los diccionarios (C # o de otra manera) son simplemente un contenedor donde busca un valor basado en una clave. En muchos idiomas, se identifica más correctamente como un mapa, siendo la implementación más común un HashMap.

El problema a considerar es qué sucede cuando una clave no existe. Algunos idiomas se comportan mediante la devolución nullo nilo algún otro valor equivalente. En silencio, el valor predeterminado es un valor en lugar de informarle que un valor no existe.

Para bien o para mal, los diseñadores de la biblioteca de C # idearon un modismo para lidiar con el comportamiento. Razonaron que el comportamiento predeterminado para buscar un valor que no existe es lanzar una excepción. Si desea evitar excepciones, puede usar la Tryvariante. Es el mismo enfoque que utilizan para analizar cadenas en enteros u objetos de fecha / hora. Esencialmente, el impacto es así:

T count = int.Parse("12T45"); // throws exception

if (int.TryParse("12T45", out count))
{
    // Does not throw exception
}

Y eso se trasladó al diccionario, cuyo indexador delega a Get(index):

var myvalue = dict["12345"]; // throws exception
myvalue = dict.Get("12345"); // throws exception

if (dict.TryGet("12345", out myvalue))
{
    // Does not throw exception
}

Esta es simplemente la forma en que se diseña el lenguaje.


¿Deben outdesalentarse las variables?

C # no es el primer idioma que los tiene, y tienen su propósito en situaciones específicas. Si está tratando de construir un sistema altamente concurrente, entonces no puede usar outvariables en los límites de concurrencia.

En muchos sentidos, si hay un idioma que es adoptado por el idioma y los proveedores de la biblioteca central, trato de adoptar esos idiomas en mis API. Eso hace que la API se sienta más consistente y como en casa en ese idioma. Entonces, un método escrito en Ruby no se verá como un método escrito en C #, C o Python. Cada uno tiene una forma preferida de construir código, y trabajar con eso ayuda a los usuarios de su API a aprenderlo más rápidamente.


¿Son los mapas en general un antipatrón?

Tienen su propósito, pero muchas veces pueden ser la solución incorrecta para el propósito que usted tiene. Particularmente si tiene un mapeo bidireccional que necesita. Hay muchos contenedores y formas de organizar los datos. Hay muchos enfoques que puede usar, y a veces necesita pensar un poco antes de elegir ese contenedor.

Si tiene una lista muy corta de valores de mapeo bidireccional, es posible que solo necesite una lista de tuplas. O una lista de estructuras, donde puede encontrar fácilmente la primera coincidencia a cada lado de la asignación.

Piense en el dominio del problema y elija la herramienta más adecuada para el trabajo. Si no hay uno, créelo.

Berin Loritsch
fuente
44
"entonces es posible que solo necesite una lista de tuplas. O una lista de estructuras, donde pueda encontrar fácilmente la primera coincidencia a cada lado del mapeo". - Si hay optimizaciones para conjuntos pequeños, deben hacerse en la biblioteca, no estampados en el código del usuario.
Blrfl
Si elige una lista de tuplas o un diccionario, eso es un detalle de implementación. Se trata de comprender el dominio de su problema y utilizar la herramienta adecuada para el trabajo. Obviamente, existe una lista y existe un diccionario. Para el caso común, el diccionario es correcto, pero quizás para una o dos aplicaciones necesite usar la lista.
Berin Loritsch
3
Lo es, pero si el comportamiento sigue siendo el mismo, esa determinación debería estar en la biblioteca. Me he encontrado con implementaciones de contenedores en otros idiomas que cambiarán los algoritmos si se me dice a priori que el número de entradas será pequeño. Estoy de acuerdo con su respuesta, pero una vez que se haya resuelto, debería estar en una biblioteca, tal vez como un SmallBidirectionalMap.
Blrfl
55
"Para bien o para mal, los diseñadores de la biblioteca de C # idearon un modismo para lidiar con el comportamiento". - Creo que has dado en el clavo: "se les ocurrió" una expresión idiomática, en lugar de usar una existente de uso generalizado, que es un tipo de devolución opcional.
Jörg W Mittag
3
@ JörgWMittag Si estás hablando de algo así Option<T>, entonces creo que eso haría que el método sea mucho más difícil de usar en C # 2.0, porque no tenía coincidencia de patrones.
svick
21

Algunas buenas respuestas aquí sobre los principios generales de las tablas hash / diccionarios. Pero pensé que tocaría tu ejemplo de código,

int x;
if (dict.TryGetValue("key", out x)) 
{
    DoSomethingWith(x);
}

A partir de C # 7 (que creo que tiene alrededor de dos años), eso se puede simplificar para:

if (dict.TryGetValue("key", out var x))
{
    DoSomethingWith(x);
}

Y, por supuesto, podría reducirse a una línea:

if (dict.TryGetValue("key", out var x)) DoSomethingWith(x);

Si tiene un valor predeterminado para cuando la clave no existe, puede convertirse en:

DoSomethingWith(dict.TryGetValue("key", out var x) ? x : defaultValue);

Para que pueda lograr formas compactas utilizando adiciones de lenguaje razonablemente recientes.

David Arno
fuente
1
Buena decisión sobre la sintaxis v7, una pequeña y buena opción para tener que cortar la línea de definición adicional mientras se permite el uso de var +1
BrianH
2
Tenga en cuenta también que si "key"no está presente, xse inicializará adefault(TValue)
Peter Duniho
También podría ser bueno como una extensión genérica, invocada como"key".DoSomethingWithThis()
Ross Presser
Prefiero en gran medida los diseños que utilizan el getOrElse("key", defaultValue) objeto nulo sigue siendo mi patrón favorito. Trabaja de esa manera y no te importa si TryGetValuedevuelve verdadero o falso.
candied_orange
1
Siento que esta respuesta merece un descargo de responsabilidad de que escribir código compacto por ser compacto es malo. Puede hacer que su código sea mucho más difícil de leer. Además, si no recuerdo mal, TryGetValueno es atómico / seguro para subprocesos, por lo que podría realizar fácilmente una verificación de existencia y otra para obtener y operar sobre el valor
Marie
12

Esto no es un olor a código ni un antipatrón, ya que el uso de funciones de estilo TryGet con un parámetro de salida es idiomático C #. Sin embargo, hay 3 opciones proporcionadas en C # para trabajar con un diccionario, por lo que debe estar seguro de que está utilizando la correcta para su situación. Creo que sé de dónde proviene el rumor de que hay un problema al usar un parámetro out, así que lo manejaré al final.

Qué características usar cuando se trabaja con un diccionario C #:

  1. Si está seguro de que la clave estará en el Diccionario, use la propiedad Item [TKey]
  2. Si la clave generalmente debe estar en el Diccionario, pero es malo / raro / problemático que no esté allí, debe usar Try ... Catch para que se genere un error y luego pueda intentar manejar el error con gracia
  3. Si no está seguro de que la clave estará en el Diccionario, use TryGet con el parámetro out

Para justificar esto, solo es necesario consultar la documentación del Diccionario TryGetValue, en "Observaciones" :

Este método combina la funcionalidad del método ContainsKey y la propiedad Item [TKey].

...

Utilice el método TryGetValue si su código intenta acceder con frecuencia a claves que no están en el diccionario. Usar este método es más eficiente que capturar la KeyNotFoundException generada por la propiedad Item [TKey].

Este método se acerca a una operación O (1).

La razón por la que existe TryGetValue es para actuar como una forma más conveniente de usar ContainsKey y Item [TKey], evitando tener que buscar el diccionario dos veces, por lo que fingir que no existe y hacer las dos cosas que hace manualmente es bastante incómodo. elección.

En la práctica, rara vez he usado un diccionario sin formato, debido a esta máxima simple: elija la clase / contenedor más genérico que le brinde la funcionalidad que necesita . El diccionario no fue diseñado para buscar por valor en lugar de por clave (por ejemplo), por lo que si eso es algo que desea, puede tener más sentido usar una estructura alternativa. Creo que pude haber usado Dictionary una vez en el proyecto de desarrollo de un año que hice, simplemente porque rara vez era la herramienta adecuada para el trabajo que estaba tratando de hacer. El diccionario ciertamente no es la navaja suiza de la caja de herramientas C #.

¿Qué hay de malo con nuestros parámetros?

CA1021: evitar parámetros

Aunque los valores de retorno son comunes y muy utilizados, la aplicación correcta de los parámetros out y ref requiere habilidades intermedias de diseño y codificación. Los arquitectos de bibliotecas que diseñan para una audiencia general no deben esperar que los usuarios dominen el trabajo sin parámetros de referencia.

Supongo que ahí es donde escuchaste que nuestros parámetros eran algo así como un antipatrón. Como con todas las reglas, debe leer más de cerca para entender el "por qué", y en este caso incluso hay una mención explícita de cómo el patrón Try no viola la regla :

Los métodos que implementan el patrón Try, como System.Int32. CalculateParse, no generan esta infracción.

BrianH
fuente
Toda esa lista de orientación es algo que desearía que más personas leyeran. Para aquellos que siguen, aquí está la raíz de esto. docs.microsoft.com/en-us/visualstudio/code-quality/…
Peter Wone
10

Faltan al menos dos métodos en los diccionarios de C # que, en mi opinión, limpian el código considerablemente en muchas situaciones en otros lenguajes. El primero es devolver unOption , que le permite escribir código como el siguiente en Scala:

dict.get("key").map(doSomethingWith)

El segundo es devolver un valor predeterminado especificado por el usuario si no se encuentra la clave:

doSomethingWith(dict.getOrElse("key", "key not found"))

Hay algo que decir sobre el uso de las expresiones idiomáticas que proporciona un idioma cuando es apropiado, como el Trypatrón, pero eso no significa que solo tenga que usar lo que proporciona el idioma. Somos programadores Está bien crear nuevas abstracciones para que nuestra situación específica sea más fácil de entender, especialmente si elimina muchas repeticiones. Si con frecuencia necesita algo, como búsquedas inversas o iterar a través de valores, haga que suceda. Crea la interfaz que desearías tener.

Karl Bielefeldt
fuente
Me gusta la segunda opción. Tal vez tendré que escribir un método de extensión o dos :)
Adam B
2
en el lado positivo, los métodos de extensión de C # significan que puede implementarlos usted mismo
jk.
5

Esas son 4 líneas de código para hacer esencialmente lo siguiente: DoSomethingWith(dict["key"])

Estoy de acuerdo en que esto es poco elegante. Un mecanismo que me gusta usar en este caso, donde el valor es un tipo de estructura, es:

public static V? TryGetValue<K, V>(
      this Dictionary<K, V> dict, K key) where V : struct => 
  dict.TryGetValue(key, out V v)) ? new V?(v) : new V?();

Ahora tenemos una nueva versión de los TryGetValueretornos int?. Entonces podemos hacer un truco similar para extender T?:

public static void DoIt<T>(
      this T? item, Action<T> action) where T : struct
{
  if (item != null) action(item.GetValueOrDefault());
}

Y ahora póngalo junto:

dict.TryGetValue("key").DoIt(DoSomethingWith);

y nos queda solo una declaración clara y clara.

Escuché que usar la palabra clave out es un anti patrón porque hace que las funciones muten sus parámetros.

Estaría un poco menos redactado, y diría que evitar mutaciones cuando sea posible es una buena idea.

A menudo me encuentro necesitando un diccionario "invertido", donde cambio las claves y los valores.

Luego implemente u obtenga un diccionario bidireccional. Son fáciles de escribir, o hay muchas implementaciones disponibles en Internet. Aquí hay un montón de implementaciones, por ejemplo:

/programming/268321/bidirectional-1-to-1-dictionary-in-c-sharp

Del mismo modo, a menudo me gustaría recorrer los elementos de un diccionario y convertir las claves o valores en listas, etc. para hacerlo mejor.

Claro, todos lo hacemos.

Siento que casi siempre hay una forma mejor y más elegante de usar los diccionarios, pero estoy perdido.

Pregúntese "suponga que tengo una clase distinta a la Dictionaryque implementó la operación exacta que deseo hacer; ¿cómo sería esa clase?" Luego, una vez que haya respondido esa pregunta, implemente esa clase . Eres un programador de computadoras. ¡Resuelve tus problemas escribiendo programas de computadora!

Eric Lippert
fuente
Gracias. ¿Crees que podrías dar un poco más de explicación de lo que el método de acción está haciendo realmente? Soy nuevo en las acciones.
Adam B
1
@AdamB una acción es un objeto que representa la capacidad de llamar a un método nulo. Como puede ver, en el lado de la persona que llama, pasamos el nombre de un método nulo cuya lista de parámetros coincide con los argumentos de tipo genérico de la acción. En el lado del llamante, la acción se invoca como cualquier otro método.
Eric Lippert
1
@AdamB: Puedes pensar Action<T>que es muy similar a interface IAction<T> { void Invoke(T t); }, pero con reglas muy indulgentes sobre qué "implementa" esa interfaz y cómo Invokese puede invocar. Si quiere saber más, aprenda sobre "delegados" en C #, y luego aprenda sobre expresiones lambda.
Eric Lippert
Ok, creo que lo tengo. Entonces, la acción le permite poner un método como parámetro para DoIt (). Y llama a ese método.
Adam B
@ AdamB: Exactamente correcto. Este es un ejemplo de "programación funcional con funciones de orden superior". La mayoría de las funciones toman datos como argumentos; las funciones de orden superior toman funciones como argumentos y luego hacen cosas con esas funciones. A medida que aprenda más C #, verá que LINQ se implementa completamente con funciones de orden superior.
Eric Lippert
4

La TryGetValue()construcción solo es necesaria si no sabe si "clave" está presente como una clave dentro del diccionario o no, de lo contrario DoSomethingWith(dict["key"])es perfectamente válido.

Un enfoque "menos sucio" podría ser utilizarlo ContainsKey()como un cheque.

Halcón peregrino
fuente
66
"Un enfoque" menos sucio "podría ser utilizar ContainsKey () como un cheque en su lugar". Estoy en desacuerdo. Tan subóptimo como TryGetValuees, al menos hace que sea difícil olvidar manejar el caso vacío. Idealmente, desearía que esto solo devuelva un Opcional.
Alexander - Restablece a Mónica el
1
Existe un problema potencial con el ContainsKeyenfoque para las aplicaciones multiproceso, que es la vulnerabilidad de tiempo de verificación al tiempo de uso (TOCTOU). ¿Qué pasa si algún otro hilo elimina la clave entre las llamadas a ContainsKeyy GetValue?
David Hammen
2
@JAD Opcionales componen. Puedes tener unOptional<Optional<T>>
Alexander - Restablecer a Mónica el
2
@DavidHammen Para que quede claro, lo que se necesita TryGetValueen ConcurrentDictionary. El diccionario regular no se sincronizaría
Alexander - Restablece a Mónica el
2
@JAD No, (el tipo opcional de Java explota, no me hagas empezar). Estoy hablando de manera más abstracta
Alexander - Restablece a Mónica el
2

Otras respuestas contienen grandes puntos, por lo que no las volveré a exponer aquí, sino que me centraré en esta parte, que parece ser ignorada hasta ahora:

Del mismo modo, a menudo me gustaría recorrer los elementos de un diccionario y convertir las claves o valores en listas, etc. para hacerlo mejor.

En realidad, es bastante fácil iterar sobre un diccionario, ya que implementa IEnumerable:

var dict = new Dictionary<int, string>();

foreach ( var item in dict ) {
    Console.WriteLine("{0} => {1}", item.Key, item.Value);
}

Si prefiere Linq, eso también funciona bien:

dict.Select(x=>x.Value).WhateverElseYouWant();

En general, no creo que los diccionarios sean un antipatrón: son solo una herramienta específica con usos específicos.

Además, para obtener más detalles, consulte SortedDictionary(utiliza árboles RB para un rendimiento más predecible) y SortedList(que también es un diccionario confusamente llamado que sacrifica la velocidad de inserción por la velocidad de búsqueda, pero brilla si está mirando con un pre fijo conjunto ordenado). He tenido un caso en el que reemplazar un Dictionarycon un SortedDictionaryresultado en una ejecución de orden de magnitud más rápida (pero también podría ocurrir al revés).

Vilx-
fuente
1

Si cree que usar un diccionario es incómodo, puede que no sea la opción correcta para su problema. Los diccionarios son geniales, pero como notó un comentarista, a menudo se usan como atajos para algo que debería haber sido una clase. O el diccionario en sí puede ser correcto como un método de almacenamiento central, pero debe haber una clase de envoltura alrededor para proporcionar los métodos de servicio deseados.

Mucho se ha dicho sobre Dict [clave] versus TryGet. Lo que uso mucho es iterar sobre el diccionario usando KeyValuePair. Aparentemente, esta es una construcción menos conocida.

La bendición principal de un diccionario es que es realmente rápido en comparación con otras colecciones a medida que aumenta el número de elementos. Si tiene que verificar si una clave existe mucho, puede preguntarse si su uso de un diccionario es apropiado. Como cliente, normalmente debe saber qué pone allí y, por lo tanto, qué sería seguro consultar.

Martin Maat
fuente