J'essaye de faire une table de consultation de dictionnaire en C #. J'ai besoin de résoudre un triple de valeurs en une chaîne. J'ai essayé d'utiliser des tableaux comme clés, mais cela n'a pas fonctionné et je ne sais pas quoi faire d'autre. À ce stade, j'envisage de créer un dictionnaire de dictionnaires de dictionnaires, mais ce ne serait probablement pas très joli à regarder, bien que ce soit comme cela que je le ferais en javascript.
Si vous êtes sur .NET 4.0, utilisez un tuple:
lookup = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();
Sinon, vous pouvez définir un tuple et l'utiliser comme clé. Le tuple doit remplacer GetHashCode, Equals et IEquatable:
struct Tuple<T, U, W> : IEquatable<Tuple<T,U,W>>
{
readonly T first;
readonly U second;
readonly W third;
public Tuple(T first, U second, W third)
{
this.first = first;
this.second = second;
this.third = third;
}
public T First { get { return first; } }
public U Second { get { return second; } }
public W Third { get { return third; } }
public override int GetHashCode()
{
return first.GetHashCode() ^ second.GetHashCode() ^ third.GetHashCode();
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
return Equals((Tuple<T, U, W>)obj);
}
public bool Equals(Tuple<T, U, W> other)
{
return other.first.Equals(first) && other.second.Equals(second) && other.third.Equals(third);
}
}
Entre les approches basées sur les dictionnaires imbriqués et les dictionnaires imbriqués, il est presque toujours préférable d'opter pour l'approche basée sur Tuple.
Du point de vue de la maintenabilité ,
sa beaucoup plus facile d'implémenter une fonctionnalité qui ressemble à:
var myDict = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();
que
var myDict = new Dictionary<TypeA, Dictionary<TypeB, Dictionary<TypeC, string>>>();
du côté de l'appelé. Dans le second cas, chaque ajout, recherche, suppression, etc. requiert une action sur plusieurs dictionnaires.
De plus, si votre clé composite nécessite un ou plusieurs champs à l'avenir, vous devrez modifier le code d'un lot important dans le deuxième cas (dictionnaire imbriqué), car vous devrez ajouter d'autres dictionnaires imbriqués et les vérifications suivantes.
Du point de vue de la performance , la meilleure conclusion à laquelle vous pouvez parvenir est en le mesurant vous-même. Mais il y a quelques limitations théoriques que vous pouvez considérer à l'avance:
Dans le cas d'un dictionnaire imbriqué, le fait d'avoir un dictionnaire supplémentaire pour chaque clé (externe et interne) entraînera une surcharge de mémoire (plus que ce que créerait un tuple).
Dans le cas d'un dictionnaire imbriqué, chaque action de base telle que l'addition, la mise à jour, la recherche, la suppression, etc. doit être effectuée dans deux dictionnaires. Il existe maintenant un cas où l’approche par dictionnaire imbriqué peut être plus rapide, c’est-à-dire lorsque les données recherchées sont absentes, car les dictionnaires intermédiaires peuvent contourner le calcul et la comparaison du code de hachage complet, mais il doit être chronométré pour être sûr. En présence de données, cela devrait être plus lent car les recherches doivent être effectuées deux fois (ou trois fois en fonction de l'imbrication).
En ce qui concerne l’approche Tuple, les n-uplets .NET ne sont pas les plus performants quand ils sont censés être utilisés comme clés dans des ensembles étant donné que Equals
et GetHashCode
l’implémentation provoque la mise en boîte des types de valeur .
Je choisirais un dictionnaire basé sur Tuple, mais si je voulais plus de performances, j'utiliserais mon propre Tuple avec une meilleure implémentation.
En passant, peu de produits cosmétiques peuvent rendre le dictionnaire cool:
Les appels de style indexeur peuvent être beaucoup plus propres et intuitifs. Pour, par exemple,
string foo = dict[a, b, c]; //lookup
dict[a, b, c] = ""; //update/insertion
Exposez donc les indexeurs nécessaires dans votre classe de dictionnaire, qui gère en interne les insertions et les recherches.
En outre, implémentez une interface IEnumerable
appropriée et fournissez une méthode Add(TypeA, TypeB, TypeC, string)
qui vous donnerait la syntaxe d'initialiseur de collection, comme par exemple:
new MultiKeyDictionary<TypeA, TypeB, TypeC, string>
{
{ a, b, c, null },
...
};
Le bon, propre, rapide, facile et lisible est:
ajoutez quelque chose de similaire comme ceci:
public sealed class myKey : Tuple<TypeA, TypeB, TypeC>
{
public myKey(TypeA dataA, TypeB dataB, TypeC dataC) : base (dataA, dataB, dataC) { }
public TypeA DataA => Item1;
public TypeB DataB => Item2;
public TypeC DataC => Item3;
}
Vous pouvez donc l'utiliser avec dictionnaire:
var myDictinaryData = new Dictionary<myKey, string>()
{
{new myKey(1, 2, 3), "data123"},
{new myKey(4, 5, 6), "data456"},
{new myKey(7, 8, 9), "data789"}
};
Si, pour une raison quelconque, vous souhaitez vraiment éviter de créer votre propre classe Tuple ou d'utiliser la version intégrée de .NET 4.0, il existe une autre approche possible; vous pouvez combiner les trois valeurs de clé en une seule valeur.
Par exemple, si les trois valeurs sont des types entiers ne prenant pas plus de 64 bits, vous pouvez les combiner dans un ulong
.
Dans le pire des cas, vous pouvez toujours utiliser une chaîne, à condition de vous assurer que ses trois composants sont délimités par un caractère ou une séquence ne se produisant pas dans les composants de la clé. Par exemple, vous pouvez essayer trois chiffres:
string.Format("{0}#{1}#{2}", key1, key2, key3)
Il est évident que cette approche comporte des frais généraux de composition, mais en fonction de l'utilisation que vous en faites, cela peut être assez trivial pour ne pas s'en soucier.
Si vous êtes sur C # 7, vous devriez envisager d'utiliser des nuplets de valeur comme clé composite. Les tuples de valeur offrent généralement de meilleures performances que les tuples de référence traditionnels (Tuple<T1, …>
) étant donné que les nuplets de valeur sont des types de valeur (structs) et non des types de référence, ils évitent ainsi les coûts d’allocation de mémoire et de garbage collection. En outre, ils offrent une syntaxe plus concise et plus intuitive, permettant de nommer leurs champs si vous le souhaitez. Ils implémentent également le IEquatable<T>
interface nécessaire pour le dictionnaire.
var dict = new Dictionary<(int PersonId, int LocationId, int SubjectId), string>();
dict.Add((3, 6, 9), "ABC");
dict.Add((PersonId: 4, LocationId: 9, SubjectId: 10), "XYZ");
var personIds = dict.Keys.Select(k => k.PersonId).Distinct().ToList();
Je remplacerais votre tuple par un GetHashCode approprié et l'utiliserais simplement comme clé.
Tant que vous surchargez les méthodes appropriées, vous devriez voir des performances décentes.
Voici le tuple .NET pour référence:
[Serializable]
public class Tuple<T1, T2, T3> : IStructuralEquatable, IStructuralComparable, IComparable, ITuple {
private readonly T1 m_Item1;
private readonly T2 m_Item2;
private readonly T3 m_Item3;
public T1 Item1 { get { return m_Item1; } }
public T2 Item2 { get { return m_Item2; } }
public T3 Item3 { get { return m_Item3; } }
public Tuple(T1 item1, T2 item2, T3 item3) {
m_Item1 = item1;
m_Item2 = item2;
m_Item3 = item3;
}
public override Boolean Equals(Object obj) {
return ((IStructuralEquatable) this).Equals(obj, EqualityComparer<Object>.Default);;
}
Boolean IStructuralEquatable.Equals(Object other, IEqualityComparer comparer) {
if (other == null) return false;
Tuple<T1, T2, T3> objTuple = other as Tuple<T1, T2, T3>;
if (objTuple == null) {
return false;
}
return comparer.Equals(m_Item1, objTuple.m_Item1) && comparer.Equals(m_Item2, objTuple.m_Item2) && comparer.Equals(m_Item3, objTuple.m_Item3);
}
Int32 IComparable.CompareTo(Object obj) {
return ((IStructuralComparable) this).CompareTo(obj, Comparer<Object>.Default);
}
Int32 IStructuralComparable.CompareTo(Object other, IComparer comparer) {
if (other == null) return 1;
Tuple<T1, T2, T3> objTuple = other as Tuple<T1, T2, T3>;
if (objTuple == null) {
throw new ArgumentException(Environment.GetResourceString("ArgumentException_TupleIncorrectType", this.GetType().ToString()), "other");
}
int c = 0;
c = comparer.Compare(m_Item1, objTuple.m_Item1);
if (c != 0) return c;
c = comparer.Compare(m_Item2, objTuple.m_Item2);
if (c != 0) return c;
return comparer.Compare(m_Item3, objTuple.m_Item3);
}
public override int GetHashCode() {
return ((IStructuralEquatable) this).GetHashCode(EqualityComparer<Object>.Default);
}
Int32 IStructuralEquatable.GetHashCode(IEqualityComparer comparer) {
return Tuple.CombineHashCodes(comparer.GetHashCode(m_Item1), comparer.GetHashCode(m_Item2), comparer.GetHashCode(m_Item3));
}
Int32 ITuple.GetHashCode(IEqualityComparer comparer) {
return ((IStructuralEquatable) this).GetHashCode(comparer);
}
public override string ToString() {
StringBuilder sb = new StringBuilder();
sb.Append("(");
return ((ITuple)this).ToString(sb);
}
string ITuple.ToString(StringBuilder sb) {
sb.Append(m_Item1);
sb.Append(", ");
sb.Append(m_Item2);
sb.Append(", ");
sb.Append(m_Item3);
sb.Append(")");
return sb.ToString();
}
int ITuple.Size {
get {
return 3;
}
}
}
Si votre code consommateur pouvait se contenter d'une interface IDictionary <> au lieu de Dictionary, mon instinct aurait été d'utiliser un SortedDictionary <> avec un comparateur personnalisé, par exemple:
class ArrayComparer<T> : IComparer<IList<T>>
where T : IComparable<T>
{
public int Compare(IList<T> x, IList<T> y)
{
int compare = 0;
for (int n = 0; n < x.Count && n < y.Count; ++n)
{
compare = x[n].CompareTo(y[n]);
}
return compare;
}
}
Et créez ainsi (en utilisant int [] juste pour l'exemple concret):
var dictionary = new SortedDictionary<int[], string>(new ArrayComparer<int>());