Tuple vs ssortingng en tant que clé de dictionnaire en C #

J’ai un cache que j’implémente à l’aide d’un ConcurrentDictionary. Les données que je dois conserver dépendent de 5 parameters. Donc, la méthode pour l’obtenir à partir du cache est la suivante: (Je montre seulement 3 parameters ici pour plus de simplicité et j’ai changé le type de données pour représenter CarData pour plus de clarté)

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

Je me demande quel type de clé sera mieux utilisé dans mon ConcurrentDictionary, je peux le faire comme ceci:

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

Ou comme ceci:

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

Je n’utilise pas ces parameters ensemble ailleurs, il n’y a donc aucune justification pour créer une classe simplement pour les garder ensemble.

Je veux savoir quelle approche est la meilleure en termes de performances et de maintenabilité.

Vous pouvez créer une classe (sa seule utilisation ici importe peu) qui remplace GetHashCode et Equals:

Merci theDmi (et les autres) pour les améliorations …

 public class CarKey : IEquatable { public CarKey(ssortingng carModel, ssortingng engineType, int year) { CarModel = carModel; EngineType= engineType; Year= year; } public ssortingng CarModel {get;} public ssortingng 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 ssortingng.Equals(CarModel,obj.CarModel) && ssortingng.Equals(EngineType, obj.EngineType) && Year == obj.Year; } } 

Si vous ne les remplacez pas, ContainsKey fait une référence égale.

Remarque: la classe Tuple a ses propres fonctions d’égalité qui feraient fondamentalement la même chose que ci-dessus. L’utilisation d’une classe sur mesure montre clairement que c’est ce qui est censé se produire – et est donc préférable pour la maintenabilité. Il a également l’avantage de pouvoir nommer les propriétés pour que ce soit clair.

Remarque 2: la classe est immuable, car les clés du dictionnaire doivent être évitées afin d’éviter d’éventuels bugs avec des codes de hachage modifiés après l’ajout de l’object au dictionnaire. Voir ici

GetHashCode pris d’ici

Je veux savoir quelle approche est la meilleure en termes de performances et de maintenabilité.

Comme toujours, vous avez les outils pour le comprendre. Codez les deux solutions possibles et faites-les courir . Celui qui gagne est le gagnant, vous n’avez besoin de personne ici pour répondre à cette question particulière.

En ce qui concerne la maintenance, la solution gagnante est celle qui améliore elle-même les documents automatiques et a une meilleure évolutivité. Dans ce cas, le code est tellement sortingvial que l’autodocumentation n’est pas un problème. Du sharepoint vue de l’évolutivité, à mon humble avis, la meilleure solution consiste à utiliser Tuple :

  • Vous obtenez une sémantique d’égalité libre que vous n’avez pas besoin de maintenir.
  • Les collisions ne sont pas possibles, ce qui n’est pas vrai si vous choisissez la solution de concaténation de chaînes:

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

    Oui, loin de là, mais, en théorie, tout à fait possible et votre question porte précisément sur des événements inconnus dans le futur, alors …

  • Enfin, le compilateur vous aide à gérer le code. Si, par exemple, vous devez append param4 à votre clé, Tuple param4 fortement votre clé. En revanche, votre algorithme de concaténation de chaînes peut vivre sur des clés générant une joie heureuse sans param4 et vous ne saurez pas ce qui se param4 jusqu’à ce que votre client vous appelle, car son logiciel ne fonctionne pas comme prévu.

Si les performances sont vraiment importantes, la réponse est que vous ne devez utiliser aucune des deux options, car elles allouent inutilement un object à chaque access.

À la place, vous devez utiliser une struct , une struct personnalisée ou ValueTuple du package System.ValueTuple :

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

C # 7.0 contient également la syntaxe sugar pour faciliter l’écriture de ce code (mais vous n’avez pas besoin d’attendre que C # 7.0 commence à utiliser ValueTuple sans le sucre):

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

Implémentez une classe de clés personnalisée et assurez-vous qu’elle convient à de tels cas d’utilisation, c.-à-d. IEquatable et rendez la classe immuable :

 public class CacheKey : IEquatable { public CacheKey(ssortingng param1, ssortingng param2, int param3) { Param1 = param1; Param2 = param2; Param3 = param3; } public ssortingng Param1 { get; } public ssortingng Param2 { get; } public int Param3 { get; } public bool Equals(CacheKey other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return ssortingng.Equals(Param1, other.Param1) && ssortingng.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; } } } 

Ceci est une implémentation de GetHashCode() telle que Resharper la génère. C’est une bonne implémentation polyvalente. Adapter au besoin.


Vous pouvez également utiliser quelque chose comme Equ (je suis le créateur de cette bibliothèque) qui génère automatiquement les implémentations de Equals et GetHashCode . Cela garantira que ces méthodes incluent toujours tous les membres de la classe CacheKey , de sorte que le code devient beaucoup plus facile à gérer . Une telle implémentation ressemblerait alors simplement à ceci:

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

Remarque: vous devez évidemment utiliser des noms de propriété significatifs , sinon l’introduction d’une classe personnalisée n’apporte pas beaucoup d’avantages par rapport à l’utilisation d’un Tuple .

Je voulais comparer les approches Tuple versus Class et “id_id_id” décrites dans les autres commentaires. J’ai utilisé ce code simple:

 public class Key : IEquatable { public ssortingng Param1 { get; set; } public ssortingng 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 ssortingng.Equals(Param1, other.Param1) && ssortingng.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(); 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, ssortingng>(); for (var i = 0; i < 10000000; i++) { tupleDictionary.Add(new Tuple(i.ToSsortingng(), i.ToSsortingng(), i), i.ToSsortingng()); } stopwatch.Stop(); Console.WriteLine($"initialization: {stopwatch.Elapsed}"); stopwatch.Restart(); for (var i = 0; i < 10000000; i++) { var s = tupleDictionary[new Tuple(i.ToSsortingng(), i.ToSsortingng(), i)]; } stopwatch.Stop(); Console.WriteLine($"Resortingeving: {stopwatch.Elapsed}"); } static void TestFlat() { var stopwatch = new Stopwatch(); stopwatch.Start(); var tupleDictionary = new Dictionary(); 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(); } } 

Résultats:

J'ai exécuté chaque méthode 3 fois dans Release sans déboguer, chaque exécution commentant les appels aux autres méthodes. J'ai pris la moyenne des 3 points, mais il n'y avait pas beaucoup de variance de toute façon.

TestTuple:

 initialization: 00:00:14.2512736 Resortingeving: 00:00:08.1912167 

TestClass:

 initialization: 00:00:11.5091160 Resortingeving: 00:00:05.5127963 

TestFlat:

 initialization: 00:00:16.3672901 Resortingeving: 00:00:08.6512009 

J'ai été surpris de voir que l'approche de classe était plus rapide que l'approche de tuple et l'approche de chaîne. À mon avis, il est plus lisible et plus sûr pour l'avenir, en ce sens que davantage de fonctionnalités peuvent être ajoutées à la classe Key (en supposant que ce n'est pas simplement une clé, cela représente quelque chose).

IMHO, je préfère utiliser dans de tels cas une structure intermédiaire (dans votre cas, ce sera Tuple ). Une telle approche crée une couche supplémentaire entre les parameters et le dictionnaire cible. Bien sûr, cela dépendra des objectives. Cela vous permet par exemple de créer une transition de parameters non sortingviale (par exemple, le conteneur peut “déformer” les données).

J’ai exécuté les scénarios de test de Tomer, en ajoutant ValueTuples en tant que scénario de test (nouveau type de valeur c #). A été impressionné par leur performance.

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

Le code pour le test est ci-dessous:

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

Avoir ces parameters utilisés comme une clé complexe est une bonne justification pour les combiner en classe comme à mon goût. La classe Tuple <...> en tant que telle ne convient pas à une clé de dictionnaire, car son implémentation GetHashCode renvoie toujours 0. Vous devez donc créer une nouvelle classe qui soit dérivée du conteneur Tuple ou du tout nouveau conteneur. Du sharepoint vue des performances, tout dépend de l’efficacité de l’implémentation des méthodes GetHashCode et Equals.