Tuple (o array) come chiavi del dizionario in C #


109

Sto cercando di creare una tabella di ricerca del dizionario in C #. Ho bisogno di risolvere una tupla di 3 valori in una stringa. Ho provato a usare gli array come chiavi, ma non ha funzionato e non so cos'altro fare. A questo punto sto pensando di creare un dizionario di dizionari di dizionari, ma probabilmente non sarebbe molto carino da guardare, anche se è come lo farei in javascript.

Risposte:


113

Se sei su .NET 4.0 usa una tupla:

lookup = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();

In caso contrario, puoi definire un Tuplee usarlo come chiave. La tupla ha bisogno di ignorare GetHashCode, Equalse 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);
    }
}

6
Questa struttura dovrebbe anche implementare IEquatable <Tuple <T, U, W >>. In questo modo puoi evitare il boxing quando Equals () viene chiamato in caso di collisioni di codice hash.
Dustin Campbell,

16
@jerryjvl e tutti gli altri che lo trovano da Google come ho fatto io, Tuple di .NET 4 implementa gli uguali in modo che possa essere utilizzato in un dizionario.
Scott Chamberlain

30
La tua GetHashCodeimplementazione non è molto buona. È invariante sotto la permutazione dei campi.
CodesInChaos

2
La tupla non dovrebbe essere una struttura. Nel framework, Tuple è un tipo di riferimento.
Michael Graczyk

5
@ Thoraot - ovviamente il tuo esempio è falso ... dovrebbe essere. Perché sarebbe new object()uguale a un altro new object()? Non usa solo riferimenti diretti ... prova:bool test = new Tuple<int, string>(1, "foo").Equals(new Tuple<int, string>(1, "Foo".ToLower()));
Mike Marynowski

35

Tra approcci basati su tupla e dizionari annidati, è quasi sempre meglio optare per basati su tupla.

Dal punto di vista della manutenibilità ,

  • è molto più facile implementare una funzionalità simile a:

    var myDict = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();

    di

    var myDict = new Dictionary<TypeA, Dictionary<TypeB, Dictionary<TypeC, string>>>();

    dal lato callee. Nel secondo caso ogni aggiunta, ricerca, rimozione, ecc. Richiede un'azione su più di un dizionario.

  • Inoltre, se la tua chiave composita richiede un campo in più (o meno) in futuro, dovrai modificare il codice in modo significativo nel secondo caso (dizionario annidato) poiché devi aggiungere ulteriori dizionari annidati e controlli successivi.

Dal punto di vista delle prestazioni , la migliore conclusione che puoi raggiungere è misurarla tu stesso. Ma ci sono alcune limitazioni teoriche che puoi considerare in anticipo:

  • Nel caso del dizionario annidato, avere un dizionario aggiuntivo per ogni chiave (esterna e interna) avrà un certo sovraccarico di memoria (più di quello che avrebbe la creazione di una tupla).

  • Nel caso del dizionario annidato, ogni azione di base come aggiunta, aggiornamento, ricerca, rimozione, ecc. Deve essere eseguita in due dizionari. Ora c'è un caso in cui l'approccio al dizionario annidato può essere più veloce, cioè quando i dati ricercati sono assenti, poiché i dizionari intermedi possono bypassare il calcolo e il confronto del codice hash completo, ma poi di nuovo dovrebbe essere cronometrato per essere sicuri. In presenza di dati, dovrebbe essere più lento poiché le ricerche dovrebbero essere eseguite due volte (o tre volte a seconda della nidificazione).

  • Per quanto riguarda l'approccio tuple, tuple .NET non sono le più performanti quando sono destinati ad essere utilizzati come chiavi in set dal suoi Equalse GetHashCodecause di implementazione di boxe per i tipi di valore .

Vorrei utilizzare un dizionario basato su tupla, ma se voglio maggiori prestazioni, userei la mia tupla con una migliore implementazione.


In una nota a margine, pochi cosmetici possono rendere interessante il dizionario:

  1. Le chiamate in stile indicizzatore possono essere molto più pulite e intuitive. Ad esempio,

    string foo = dict[a, b, c]; //lookup
    dict[a, b, c] = ""; //update/insertion

    Quindi esponi gli indicizzatori necessari nella tua classe del dizionario che gestisce internamente gli inserimenti e le ricerche.

  2. Inoltre, implementa un'interfaccia adatta IEnumerablee fornisci un Add(TypeA, TypeB, TypeC, string)metodo che fornisca la sintassi dell'inizializzatore della raccolta, come:

    new MultiKeyDictionary<TypeA, TypeB, TypeC, string> 
    { 
        { a, b, c, null }, 
        ...
    };

Nel caso di dizionari annidati, la sintassi dell'indicizzatore non sarebbe più simile a questa string foo = dict[a][b][c]:?
Steven Rands,

@StevenRands sì, lo sarà.
nawfal

1
@nawfal Posso cercare nel dizionario tuplo quando ho solo una delle chiavi non tutte? o posso fare come questo dict [a, b] e poi dict [a, c]?
Khan Engineer

@KhanEngineer Molto dipende dallo scopo previsto del dizionario o da come intendi usarlo. Per esempio, si desidera ottenere indietro il valore da una parte della chiave, a. Potresti semplicemente iterare qualsiasi dizionario come una normale raccolta e verificare la proprietà della chiave, se lo è a. Se vuoi sempre ottenere l'elemento in dict dalla prima proprietà, puoi progettare meglio il dizionario come dizionario di dizionari come mostrato nella mia risposta e query come dict[a], che ti dà un altro dizionario.
nawfal

Se con "ricerca per una sola chiave" intendi recuperare il valore da una qualsiasi delle chiavi che hai, allora è meglio riprogettare il tuo dizionario come una sorta di "dizionario qualsiasi chiave". Ad esempio, se vuoi ottenere un valore 4per entrambe le chiavi ae b, puoi renderlo un dizionario standard e aggiungere valori come dict[a] = 4e dict[b] = 4. Potrebbe non avere senso se logicamente il tuo ae bdovrebbe essere un'unità. In tal caso è possibile definire un custom IEqualityComparerche identifica due istanze chiave come uguali se una delle loro proprietà è uguale. Tutto questo può essere fatto genericamente con la riflessione.
nawfal

30

Se utilizzi C # 7, dovresti considerare l'utilizzo di tuple di valori come chiave composta. Le tuple di valore in genere offrono prestazioni migliori rispetto alle tuple di riferimento tradizionali ( Tuple<T1, …>) poiché le tuple di valore sono tipi di valore (strutture), non tipi di riferimento, quindi evitano i costi di allocazione della memoria e di garbage collection. Inoltre, offrono una sintassi più concisa e intuitiva, consentendo di denominare i loro campi se lo desideri. Implementano anche l' IEquatable<T>interfaccia necessaria per il dizionario.

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

13

I modi buoni, puliti, veloci, facili e leggibili sono:

  • generare membri di uguaglianza ( metodo Equals () e GetHashCode ()) per il tipo corrente. Strumenti come ReSharper non solo creano i metodi, ma generano anche il codice necessario per un controllo di uguaglianza e / o per il calcolo del codice hash. Il codice generato sarà più ottimale rispetto alla realizzazione di Tuple.
  • basta creare una semplice classe chiave derivata da una tupla .

aggiungi qualcosa di simile come questo:

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

Quindi puoi usarlo con il dizionario:

var myDictinaryData = new Dictionary<myKey, string>()
{
    {new myKey(1, 2, 3), "data123"},
    {new myKey(4, 5, 6), "data456"},
    {new myKey(7, 8, 9), "data789"}
};
  • Puoi anche usarlo nei contratti
  • come chiave per unirsi o raggruppare in linq
  • andando in questo modo non sbaglierai mai a digitare l'ordine di Item1, Item2, Item3 ...
  • non è necessario ricordare o esaminare il codice per capire dove andare per ottenere qualcosa
  • non è necessario sostituire IStructuralEquatable, IStructuralComparable, IComparable, ITuple tutti già qui

1
Ora puoi usare i membri con corpo di espressione, è ancora più pulito, ad espublic TypeA DataA => Item1;
Andrewtatham

7

Se per qualche ragione si vuole davvero evitare di creare la propria classe Tuple, o di usarla in .NET 4.0, c'è un altro approccio possibile; puoi combinare i tre valori chiave insieme in un unico valore.

Ad esempio, se i tre valori sono tipi interi insieme che non richiedono più di 64 bit, è possibile combinarli in un file ulong.

Nel peggiore dei casi puoi sempre usare una stringa, purché ti assicuri che i tre componenti in essa contenuti siano delimitati da un carattere o una sequenza che non si trova all'interno dei componenti della chiave, ad esempio, con tre numeri che potresti provare:

string.Format("{0}#{1}#{2}", key1, key2, key3)

C'è ovviamente un sovraccarico di composizione in questo approccio, ma a seconda di cosa lo stai usando per questo potrebbe essere abbastanza banale da non preoccupartene.


6
Direi però che dipende fortemente dal contesto; se avessi tre tipi interi da combinare e le prestazioni non fossero critiche, funzionerebbe perfettamente con la minima possibilità di commettere un errore. Ovviamente, tutto questo è completamente ridondante a partire da .NET 4, poiché Microsoft ci fornirà (presumibilmente corretti!) Tipi di tupla pronti all'uso.
jerryjvl

È anche possibile utilizzare questo metodo in combinazione con a JavaScriptSerializerper concatenare un array di tipi di stringhe e / o interi per te. In questo modo, non è necessario creare da soli un carattere delimitatore.
binki

3
Questo potrebbe ottenere reali disordinato se uno qualsiasi dei tasti ( key1, key2, key3) sono stati stringhe contenenti il deliminator ( "#")
Greg

4

Vorrei sovrascrivere la tua tupla con un GetHashCode appropriato e usarlo come chiave.

Finché sovraccarichi i metodi corretti, dovresti vedere prestazioni decenti.


1
IComparable non ha alcun effetto sul modo in cui le chiavi vengono archiviate o posizionate in un dizionario <TKey, TValue>. È tutto fatto tramite GetHashCode () e un IEqualityComparer <T>. L'implementazione di IEquatable <T> consentirà di ottenere prestazioni migliori perché allevia la boxe causata dall'EqualityComparer predefinito, che ricade sulla funzione Equals (oggetto).
Dustin Campbell,

Stavo per menzionare GetHashCode, ma pensavo che il Dictionary usasse IComparable nel caso in cui gli HashCode fossero identici ... immagino di essermi sbagliato.
John Gietzen,

3

Ecco la tupla .NET per riferimento:

[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
Ottieni codice hash è implementato come ((elemento1 ^ elemento2) * 33) ^ elemento3
Michael Graczyk

2

Se il tuo codice consumante può accontentarsi di un'interfaccia IDictionary <>, invece di Dictionary, il mio istinto sarebbe stato quello di utilizzare un SortedDictionary <> con un comparatore di array personalizzato, ad esempio:

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

E crea così (usando int [] solo per un esempio concreto):

var dictionary = new SortedDictionary<int[], string>(new ArrayComparer<int>());
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.