whats novedades news new microsoft features docs c# .net caching equality concurrentdictionary

novedades - new features c# 6



Tuple vs cadena como una clave de diccionario en C# (8)

Tengo un caché que implemento usando un ConcurrentDictionary. Los datos que necesito conservar dependen de 5 parámetros. Entonces, el Método para obtenerlo del caché es: (Aquí solo muestro 3 parámetros por simplicidad, y cambié el tipo de datos para representar CarData para claridad)

public CarData GetCarData(string carModel, string engineType, int year);

Me pregunto qué tipo de clave será mejor usar en mi ConcurrentDictionary, puedo hacerlo así:

var carCache = new ConcurrentDictionary<string, CarData>(); // check for car key bool exists = carCache.ContainsKey(string.Format("{0}_{1}_{2}", carModel, engineType, year);

O así:

var carCache = new ConcurrentDictionary<Tuple<string, string, int>, CarData>(); // check for car key bool exists = carCache.ContainsKey(new Tuple(carModel, engineType, year));

No utilizo estos parámetros juntos en ningún otro lugar, por lo que no hay justificación para crear una clase solo para mantenerlos juntos.

Quiero saber qué enfoque es mejor en términos de rendimiento y mantenimiento.


Quiero saber qué enfoque es mejor en términos de rendimiento y mantenimiento.

Como siempre, tienes las herramientas para resolverlo. Codifica ambas posibles soluciones y hazlas competir . El que gana es el ganador, no necesitas a nadie aquí para responder a esta pregunta en particular.

En cuanto al mantenimiento, la solución que se autodocumenta mejor y tiene mejor escalabilidad debería ser la ganadora. En este caso, el código es tan trivial que la auto documentación no es un gran problema. Desde un punto de vista de escalabilidad, en mi humilde opinión, la mejor solución es usar Tuple<T1, T2, ...> :

  • Obtienes semántica de igualdad gratuita que no necesitas mantener.
  • Las colisiones no son posibles, algo que no es cierto si elige la solución de concatenación de cadenas:

    var param1 = "Hey_I''m a weird string"; var param2 = "!" var param3 = 1; key = "Hey_I''m a weird string_!_1"; var param1 = "Hey"; var param2 = "I''m a weird string_!" var param3 = 1; key = "Hey_I''m a weird string_!_1";

    Sí, exagerado, pero, en teoría, es completamente posible y tu pregunta es precisamente sobre eventos desconocidos en el futuro, entonces ...

  • Y por último, pero no menos importante, el compilador lo ayuda a mantener el código. Si, por ejemplo, mañana tienes que agregar param4 a tu clave, Tuple<T1, T2, T3, T4> param4 fuertemente tu clave. Por otro lado, su algoritmo de concatenación de cadenas puede vivir felizmente generando claves sin param4 y no sabrá qué está sucediendo hasta que su cliente lo llame porque su software no funciona como se esperaba.


Ejecuté los casos de prueba de Tomer y agregué ValueTuples como caso de prueba (nuevo tipo de valor de c #). Estaba impresionado por lo bien que se desempeñaron.

TestClass initialization: 00:00:11.8787245 Retrieving: 00:00:06.3609475 TestTuple initialization: 00:00:14.6531189 Retrieving: 00:00:08.5906265 TestValueTuple initialization: 00:00:10.8491263 Retrieving: 00:00:06.6928401 TestFlat initialization: 00:00:16.6559780 Retrieving: 00:00:08.5257845

El código para la prueba está a continuación:

static void TestValueTuple(int n = 10000000) { var stopwatch = new Stopwatch(); stopwatch.Start(); var tupleDictionary = new Dictionary<(string, string, int), string>(); for (var i = 0; i < n; i++) { tupleDictionary.Add((i.ToString(), i.ToString(), i), i.ToString()); } stopwatch.Stop(); Console.WriteLine($"initialization: {stopwatch.Elapsed}"); stopwatch.Restart(); for (var i = 0; i < n; i++) { var s = tupleDictionary[(i.ToString(), i.ToString(), i)]; } stopwatch.Stop(); Console.WriteLine($"Retrieving: {stopwatch.Elapsed}"); }


En mi humilde opinión, prefiero usar en tales casos alguna estructura intermedia (en su caso será Tuple ). Tal enfoque crea una capa adicional entre los parámetros y el diccionario del objetivo final. Por supuesto, dependerá de los propósitos. Tal forma, por ejemplo, le permite crear una transición de parámetros no trivial (por ejemplo, el contenedor puede "distorsionar" datos).


Implemente una clase de clave personalizada y asegúrese de que sea adecuada para dichos casos de uso, es decir, implemente IEquatable y haga que la clase sea inmutable :

public class CacheKey : IEquatable<CacheKey> { public CacheKey(string param1, string param2, int param3) { Param1 = param1; Param2 = param2; Param3 = param3; } public string Param1 { get; } public string Param2 { get; } public int Param3 { get; } public bool Equals(CacheKey other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return string.Equals(Param1, other.Param1) && string.Equals(Param2, other.Param2) && Param3 == other.Param3; } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; return Equals((CacheKey)obj); } public override int GetHashCode() { unchecked { var hashCode = Param1?.GetHashCode() ?? 0; hashCode = (hashCode * 397) ^ (Param2?.GetHashCode() ?? 0); hashCode = (hashCode * 397) ^ Param3; return hashCode; } } }

Esta es una implementación de GetHashCode() cómo Resharper lo genera. Es una buena implementación de propósito general. Adaptarse según sea necesario.

Alternativamente, use algo como Equ (soy el creador de esa biblioteca) que genera automáticamente implementaciones Equals y GetHashCode . Esto asegurará que estos métodos siempre incluyan todos los miembros de la clase CacheKey , por lo que el código se vuelve mucho más fácil de mantener . Tal implementación simplemente se vería así:

public class CacheKey : MemberwiseEquatable<CacheKey> { public CacheKey(string param1, string param2, int param3) { Param1 = param1; Param2 = param2; Param3 = param3; } public string Param1 { get; } public string Param2 { get; } public int Param3 { get; } }

Nota: Obviamente, debe utilizar nombres de propiedad significativos , de lo contrario, la introducción de una clase personalizada no ofrece muchos beneficios con respecto al uso de una Tuple .


Podrías crear una clase (no importa que solo se use aquí) que anule GetHashCode e Igual:

Gracias theDmi (y otros) por las mejoras ...

public class CarKey : IEquatable<CarKey> { public CarKey(string carModel, string engineType, int year) { CarModel = carModel; EngineType= engineType; Year= year; } public string CarModel {get;} public string EngineType {get;} public int Year {get;} public override int GetHashCode() { unchecked // Overflow is fine, just wrap { int hash = (int) 2166136261; hash = (hash * 16777619) ^ CarModel?.GetHashCode() ?? 0; hash = (hash * 16777619) ^ EngineType?.GetHashCode() ?? 0; hash = (hash * 16777619) ^ Year.GetHashCode(); return hash; } } public override bool Equals(object other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; if (other.GetType() != GetType()) return false; return Equals(other as CarKey); } public bool Equals(CarKey other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return string.Equals(CarModel,obj.CarModel) && string.Equals(EngineType, obj.EngineType) && Year == obj.Year; } }

Si no los reemplaza, ContainsKey iguala una referencia.

Nota: la clase Tuple tiene sus propias funciones de igualdad que básicamente harían lo mismo que arriba. El uso de una clase personalizada deja en claro que es lo que se pretende que suceda y, por lo tanto, es mejor para la mantenibilidad. También tiene la ventaja de que puede nombrar las propiedades para que quede claro

Nota 2: la clase es inmutable, ya que las claves del diccionario deben ser para evitar posibles errores con los códigos hash que cambian después de que el objeto se agrega al diccionario. Consulte aquí

GetHashCode tomado de aquí


Quería comparar los enfoques Tuple versus Class versus "id_id_id" descritos en los otros comentarios. Usé este código simple:

public class Key : IEquatable<Key> { public string Param1 { get; set; } public string Param2 { get; set; } public int Param3 { get; set; } public bool Equals(Key other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return string.Equals(Param1, other.Param1) && string.Equals(Param2, other.Param2) && Param3 == other.Param3; } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; return Equals((Key) obj); } public override int GetHashCode() { unchecked { var hashCode = (Param1 != null ? Param1.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (Param2 != null ? Param2.GetHashCode() : 0); hashCode = (hashCode * 397) ^ Param3; return hashCode; } } } static class Program { static void TestClass() { var stopwatch = new Stopwatch(); stopwatch.Start(); var classDictionary = new Dictionary<Key, string>(); for (var i = 0; i < 10000000; i++) { classDictionary.Add(new Key { Param1 = i.ToString(), Param2 = i.ToString(), Param3 = i }, i.ToString()); } stopwatch.Stop(); Console.WriteLine($"initialization: {stopwatch.Elapsed}"); stopwatch.Restart(); for (var i = 0; i < 10000000; i++) { var s = classDictionary[new Key { Param1 = i.ToString(), Param2 = i.ToString(), Param3 = i }]; } stopwatch.Stop(); Console.WriteLine($"Retrieving: {stopwatch.Elapsed}"); } static void TestTuple() { var stopwatch = new Stopwatch(); stopwatch.Start(); var tupleDictionary = new Dictionary<Tuple<string, string, int>, string>(); for (var i = 0; i < 10000000; i++) { tupleDictionary.Add(new Tuple<string, string, int>(i.ToString(), i.ToString(), i), i.ToString()); } stopwatch.Stop(); Console.WriteLine($"initialization: {stopwatch.Elapsed}"); stopwatch.Restart(); for (var i = 0; i < 10000000; i++) { var s = tupleDictionary[new Tuple<string, string, int>(i.ToString(), i.ToString(), i)]; } stopwatch.Stop(); Console.WriteLine($"Retrieving: {stopwatch.Elapsed}"); } static void TestFlat() { var stopwatch = new Stopwatch(); stopwatch.Start(); var tupleDictionary = new Dictionary<string, string>(); for (var i = 0; i < 10000000; i++) { tupleDictionary.Add($"{i}_{i}_{i}", i.ToString()); } stopwatch.Stop(); Console.WriteLine($"initialization: {stopwatch.Elapsed}"); stopwatch.Restart(); for (var i = 0; i < 10000000; i++) { var s = tupleDictionary[$"{i}_{i}_{i}"]; } stopwatch.Stop(); Console.WriteLine($"Retrieving: {stopwatch.Elapsed}"); } static void Main() { TestClass(); TestTuple(); TestFlat(); } }

Resultados:

Ejecuté cada método 3 veces en Release sin depuración, cada ejecución comentaba las llamadas a los otros métodos. Tomé el promedio de las 3 carreras, pero de todos modos no hubo mucha variación.

TestTuple:

initialization: 00:00:14.2512736 Retrieving: 00:00:08.1912167

TestClass:

initialization: 00:00:11.5091160 Retrieving: 00:00:05.5127963

TestFlat:

initialization: 00:00:16.3672901 Retrieving: 00:00:08.6512009

Me sorprendió ver que el enfoque de clase era más rápido que el enfoque de tupla y el enfoque de la secuencia. En mi opinión, es más legible y más seguro para el futuro, en el sentido de que se puede agregar más funcionalidad a la clase Key (asumiendo que no es solo una clave, representa algo).


Si el rendimiento es realmente importante, la respuesta es que no debe usar ninguna de las dos opciones, ya que ambas asignan un objeto innecesariamente en cada acceso.

En su lugar, debe usar una struct , ya sea personalizada o ValueTuple del paquete System.ValueTuple :

var myCache = new ConcurrentDictionary<ValueTuple<string, string, int>, CachedData>(); bool exists = myCache.ContainsKey(ValueTuple.Create(param1, param2, param3));

C # 7.0 también contiene azúcar de sintaxis para hacer que este código sea más fácil de escribir (pero no es necesario esperar a que C # 7.0 empiece a usar ValueTuple sin el azúcar):

var myCache = new ConcurrentDictionary<(string, string, int), CachedData>(); bool exists = myCache.ContainsKey((param1, param2, param3));


Tener estos parámetros usados ​​como una clave compleja es una buena justificación para tenerlos combinados en clase en cuanto a mi gusto. La clase Tuple <...> tal cual no es adecuada para propósitos de claves de diccionario, ya que su implementación GetHashCode siempre devuelve 0. Por lo tanto, deberá crear una nueva clase que provenga del contenedor Tuple o totalmente nuevo. Desde la perspectiva del rendimiento, todo depende de la eficacia de la implementación de los métodos GetHashCode y Equals.