web-dev-qa-db-fra.com

Tuples (ou tableaux) en tant que clés de dictionnaire en C #

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.

100
AlexH

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);
    }
}
106
Hallgrim

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:

  1. 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.

  2. 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 }, 
        ...
    };
    
34
nawfal

Le bon, propre, rapide, facile et lisible est:

  • génère des membres d'égalité (Equals () et GetHashCode ()) pour le type actuel. Des outils tels que ReSharper crée non seulement les méthodes, mais génère également le code nécessaire pour un contrôle d'égalité et/ou pour le calcul du code de hachage. Le code généré sera plus optimal que la réalisation de Tuple.
  • créez simplement une classe de clé simple dérivée d'un tuple .

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"}
};
  • Vous pouvez également l'utiliser dans les contrats
  • comme clé pour rejoindre ou grouper dans linq
  • en procédant de cette façon, vous n’avez jamais mal saisi l’ordre de Item1, Item2, Item3 ...
  • vous n'avez pas besoin de vous souvenir ou de regarder dans le code pour comprendre où aller pour obtenir quelque chose
  • pas besoin de passer outre IStructuralEquatable, IStructuralComparable, IComparable, ITuple ils sont déjà tous ici
12
gabba

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.

7
jerryjvl

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();
5
Douglas

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.

4
John Gietzen

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;
        }
    } 
}
3
Michael Graczyk

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>());
2
mungflesh