Dizionario chiave composita


90

Ho alcuni oggetti in List, diciamo List<MyClass>e MyClass ha diverse proprietà. Vorrei creare un indice della lista basato su 3 proprietà di di MyClass. In questo caso 2 delle proprietà sono di tipo int e una proprietà è un datetime.

Fondamentalmente mi piacerebbe poter fare qualcosa come:

Dictionary< CompositeKey , MyClass > MyClassListIndex = Dictionary< CompositeKey , MyClass >();
//Populate dictionary with items from the List<MyClass> MyClassList
MyClass aMyClass = Dicitonary[(keyTripletHere)];

A volte creo più dizionari su un elenco per indicizzare diverse proprietà delle classi che contiene. Non sono sicuro di come gestire al meglio le chiavi composite. Ho considerato di fare un checksum dei tre valori ma questo corre il rischio di collisioni.


2
Perché non usi le tuple? Fanno tutto il compositing per te.
Enigma Eldritch,

21
Non so come rispondere a questo. Fai questa domanda come se avessi dato per scontato che sto deliberatamente evitando le tuple.
AaronLS

6
Scusa, l'ho riscritto come risposta più dettagliata.
Enigma Eldritch

1
Prima di implementare una classe personalizzata, leggi Tuple (come suggerito da Eldritch Conundrum) - msdn.microsoft.com/en-us/library/system.tuple.aspx . Sono più facili da modificare e ti faranno risparmiare la creazione di classi personalizzate.
OSH

Risposte:


105

Dovresti usare le tuple. Sono equivalenti a una classe CompositeKey, ma Equals () e GetHashCode () sono già implementati.

var myClassIndex = new Dictionary<Tuple<int, bool, string>, MyClass>();
//Populate dictionary with items from the List<MyClass> MyClassList
foreach (var myObj in myClassList)
    myClassIndex.Add(Tuple.Create(myObj.MyInt, myObj.MyBool, myObj.MyString), myObj);
MyClass myObj = myClassIndex[Tuple.Create(4, true, "t")];

O usando System.Linq

var myClassIndex = myClassList.ToDictionary(myObj => Tuple.Create(myObj.MyInt, myObj.MyBool, myObj.MyString));
MyClass myObj = myClassIndex[Tuple.Create(4, true, "t")];

A meno che non sia necessario personalizzare il calcolo dell'hash, è più semplice usare le tuple.

Se ci sono molte proprietà che vuoi includere nella chiave composta, il nome del tipo Tuple può diventare piuttosto lungo, ma puoi abbreviare il nome creando la tua classe derivante da Tuple <...>.


** modificato nel 2017 **

C'è una nuova opzione che inizia con C # 7: il valore tuple . L'idea è la stessa, ma la sintassi è diversa, più leggera:

Il tipo Tuple<int, bool, string>diventa (int, bool, string)e il valore Tuple.Create(4, true, "t")diventa (4, true, "t").

Con le tuple di valore, diventa anche possibile denominare gli elementi. Nota che le prestazioni sono leggermente diverse, quindi potresti voler fare qualche benchmarking se sono importanti per te.


4
Tuple non è un buon candidato per una chiave poiché crea un numero elevato di collisioni di hash. stackoverflow.com/questions/12657348/…
paparazzo

1
@Blam KeyValuePair<K,V>e altre strutture hanno una funzione hash predefinita nota per essere cattiva (vedere stackoverflow.com/questions/3841602/… per maggiori dettagli). Tuple<>tuttavia non è un ValueType e la sua funzione hash predefinita utilizzerà almeno tutti i campi. Detto questo, se il problema principale del tuo codice sono le collisioni, implementa un ottimizzato GetHashCode()che si adatti ai tuoi dati.
Enigma di Eldritch

1
Anche se Tuple non è un ValueType dai miei test, soffre di molte collisioni
paparazzo

5
Penso che questa risposta sia obsoleta ora che abbiamo ValueTuples. Hanno una sintassi migliore in C # e sembrano eseguire GetHashCode due volte più velocemente delle Tuple: gist.github.com/ljw1004/61bc96700d0b03c17cf83dbb51437a69
Lucian Wischik

3
@LucianWischik Grazie, ho aggiornato la risposta per menzionarli.
Enigma Eldritch,

22

Il modo migliore a cui potrei pensare è creare una struttura CompositeKey e assicurarmi di sovrascrivere i metodi GetHashCode () ed Equals () per garantire velocità e precisione quando si lavora con la raccolta:

class Program
{
    static void Main(string[] args)
    {
        DateTime firstTimestamp = DateTime.Now;
        DateTime secondTimestamp = firstTimestamp.AddDays(1);

        /* begin composite key dictionary populate */
        Dictionary<CompositeKey, string> compositeKeyDictionary = new Dictionary<CompositeKey, string>();

        CompositeKey compositeKey1 = new CompositeKey();
        compositeKey1.Int1 = 11;
        compositeKey1.Int2 = 304;
        compositeKey1.DateTime = firstTimestamp;

        compositeKeyDictionary[compositeKey1] = "FirstObject";

        CompositeKey compositeKey2 = new CompositeKey();
        compositeKey2.Int1 = 12;
        compositeKey2.Int2 = 9852;
        compositeKey2.DateTime = secondTimestamp;

        compositeKeyDictionary[compositeKey2] = "SecondObject";
        /* end composite key dictionary populate */

        /* begin composite key dictionary lookup */
        CompositeKey compositeKeyLookup1 = new CompositeKey();
        compositeKeyLookup1.Int1 = 11;
        compositeKeyLookup1.Int2 = 304;
        compositeKeyLookup1.DateTime = firstTimestamp;

        Console.Out.WriteLine(compositeKeyDictionary[compositeKeyLookup1]);

        CompositeKey compositeKeyLookup2 = new CompositeKey();
        compositeKeyLookup2.Int1 = 12;
        compositeKeyLookup2.Int2 = 9852;
        compositeKeyLookup2.DateTime = secondTimestamp;

        Console.Out.WriteLine(compositeKeyDictionary[compositeKeyLookup2]);
        /* end composite key dictionary lookup */
    }

    struct CompositeKey
    {
        public int Int1 { get; set; }
        public int Int2 { get; set; }
        public DateTime DateTime { get; set; }

        public override int GetHashCode()
        {
            return Int1.GetHashCode() ^ Int2.GetHashCode() ^ DateTime.GetHashCode();
        }

        public override bool Equals(object obj)
        {
            if (obj is CompositeKey)
            {
                CompositeKey compositeKey = (CompositeKey)obj;

                return ((this.Int1 == compositeKey.Int1) &&
                        (this.Int2 == compositeKey.Int2) &&
                        (this.DateTime == compositeKey.DateTime));
            }

            return false;
        }
    }
}

Un articolo di MSDN su GetHashCode ():

http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx


Non penso che in realtà sia certo al 100% un hashcode univoco, molto probabilmente.
Hans Olsson

Potrebbe benissimo essere vero! Secondo l'articolo MSDN collegato, questo è il modo consigliato per sovrascrivere GetHashCode (). Tuttavia, poiché non uso molte chiavi composite nel mio lavoro quotidiano, non posso dirlo con certezza.
Allen E. Scharfenberg

4
Sì. Se disassembli Dictionary.FindEntry () con Reflector, vedrai che vengono testati sia l'hashcode che la piena uguaglianza. Il codice hash viene testato per primo e, se fallisce, cortocircuita la condizione senza verificare la piena uguaglianza. Se l'hash viene superato, viene verificata anche l'uguaglianza.
Jason Kleban

1
E sì, anche gli uguali dovrebbero essere sovrascritti per corrispondere. Anche se hai fatto in modo che GetHashCode () restituisse 0 per qualsiasi istanza, Dictionary continuerebbe a funzionare, sarebbe solo più lento.
Jason Kleban

2
Il tipo Tuple incorporato implementa la combinazione hash come '(h1 << 5) + h1 ^ h2' invece di 'h1 ^ h2'. Immagino che lo facciano per evitare collisioni ogni volta che i due oggetti da hash sono uguali allo stesso valore.
Enigma Eldritch

13

Che ne dici Dictionary<int, Dictionary<int, Dictionary<DateTime, MyClass>>>?

Questo ti consentirebbe di fare:

MyClass item = MyData[8][23923][date];

1
questo creerà molti più oggetti che usare una struttura o una classe CompositeKey. e sarà anche più lento poiché verranno utilizzati due livelli di ricerca.
Ian Ringrose

Credo che sia lo stesso numero di confronti - non vedo come ci sarebbero molti più oggetti - la chiave composita ha ancora bisogno di una chiave, e sono i valori dei componenti o gli oggetti e un dict per contenerli. In questo modo annidato, non è necessaria la chiave wrapper per ogni oggetto / valore, un dict aggiuntivo per ogni livello di annidamento aggiuntivo. Cosa pensi?
Jason Kleban

9
In base al mio benchmarking, che ho provato con chiavi con 2 e 3 parti: una soluzione di dizionario annidato è 3-4 volte più veloce rispetto all'utilizzo di un approccio a chiave composta da tupla. Tuttavia, l'approccio della tupla è molto più semplice / ordinato.
RickL

5
@RickL Posso confermare questi benchmark, usiamo un tipo nella nostra base di codice, chiamato CompositeDictionary<TKey1, TKey2, TValue>(ecc.) Che eredita semplicemente da Dictionary<TKey1, Dictionary<TKey2, TValue>>(o comunque sono necessari molti dizionari annidati. Senza implementare l'intero tipo da zero (invece di barare usando dizionari annidati o tipi per contenere le chiavi) questo è il più veloce che otteniamo.
Adam Houldsworth

1
L'approccio dict annidato dovrebbe essere più veloce solo per la metà (?) Dei casi in cui i dati non sono presenti, poiché i dizionari intermedi possono aggirare l'intero calcolo e confronto del codice hash. In presenza di dati, dovrebbe essere più lento poiché le operazioni di base come Aggiungi, Contiene ecc. Dovrebbero essere eseguite tre volte. Sono sicuro che il margine con l'approccio tupla sia battuto in alcuni dei benchmark sopra menzionati riguarda i dettagli di implementazione delle tuple .NET che sono piuttosto scarsi considerando la penalità di boxe che comporta per i tipi di valore. Una tripletta correttamente implementata è ciò con cui andrei, considerando anche la memoria
nawfal

12

Puoi memorizzarli in una struttura e usarla come chiave:

struct CompositeKey
{
  public int value1;
  public int value2;
  public DateTime value3;
}

Link per ottenere il codice hash: http://msdn.microsoft.com/en-us/library/system.valuetype.gethashcode.aspx


Sono bloccato su .NET 3.5, quindi non ho accesso a Tuples quindi questa è una buona soluzione!
aarona

Sono sorpreso che questo non sia più votato. È una soluzione semplice che è più leggibile di una tupla.
Contrassegna il

1
Secondo msdn questo funziona bene, se nessun campo è un tipo di riferimento, altrimenti usa la riflessione per l'uguaglianza.
Gregor Slavec

@Mark Il problema con una struttura è che la sua implementazione predefinita di GetHashCode () in realtà non garantisce di utilizzare tutti i campi della struttura (portando a scarse prestazioni del dizionario), mentre Tuple offre tale garanzia. L'ho provato. Vedi stackoverflow.com/questions/3841602/… per dettagli cruenti.
Eldritch Enundrum

8

Ora che VS2017 / C # 7 è uscito, la risposta migliore è usare ValueTuple:

// declare:
Dictionary<(string, string, int), MyClass> index;

// populate:
foreach (var m in myClassList) {
  index[(m.Name, m.Path, m.JobId)] = m;
}

// retrieve:
var aMyClass = index[("foo", "bar", 15)];

Ho scelto di dichiarare il dizionario con una ValueTuple anonima (string, string, int). Ma avrei potuto dare loro dei nomi (string name, string path, int id).

Perfwise, il nuovo ValueTuple è più veloce di Tuple in GetHashCodema più lento in Equals. Penso che dovresti fare esperimenti end-to-end completi per capire quale sia davvero il più veloce per il tuo scenario. Ma la gentilezza end-to-end e la sintassi del linguaggio per ValueTuple lo fanno vincere.

// Perf from https://gist.github.com/ljw1004/61bc96700d0b03c17cf83dbb51437a69
//
//              Tuple ValueTuple KeyValuePair
//  Allocation:  160   100        110
//    Argument:   75    80         80    
//      Return:   75   210        210
//        Load:  160   170        320
// GetHashCode:  820   420       2700
//      Equals:  280   470       6800

Sì, ho subito una grande riscrittura solo per farmi saltare in aria la soluzione di tipo anonimo (non è possibile confrontare tipi anonimi creati con assembly diversi). ValueTuple sembra essere una soluzione relativamente elegante al problema delle chiavi del dizionario composto.
Quarkly

5

Mi vengono subito in mente due approcci:

  1. Fai come Kevin ha suggerito e scrivi una struttura che servirà da chiave. Assicurati di implementare questa struttura IEquatable<TKey>e di sovrascriverne i metodi Equalse GetHashCode*.

  2. Scrivi una classe che utilizzi internamente dizionari nidificati. Qualcosa di simile: TripleKeyDictionary<TKey1, TKey2, TKey3, TValue>... questa classe avrebbe avuto al suo interno un membro di tipo Dictionary<TKey1, Dictionary<TKey2, Dictionary<TKey3, TValue>>>, e esporrebbe metodi come this[TKey1 k1, TKey2 k2, TKey3 k3], ContainsKeys(TKey1 k1, TKey2 k2, TKey3 k3)e così via

* Una parola su se override del Equalsmetodo è necessaria: se è vero che il Equalsmetodo per una struct confronta il valore di ciascun organo di default, lo fa tramite reflection - che comporta costi intrinsecamente prestazioni - e quindi non molto implementazione appropriata per qualcosa che deve essere usato come chiave in un dizionario (a mio parere, comunque). Secondo la documentazione MSDN su ValueType.Equals:

L'implementazione predefinita del metodo Equals usa la reflection per confrontare i campi corrispondenti di obj e questa istanza. Eseguire l'override del metodo Equals per un tipo particolare per migliorare le prestazioni del metodo e rappresentare più da vicino il concetto di uguaglianza per il tipo.


Per quanto riguarda 1, non penso che sia necessario sovrascrivere Equals e GetHashcode, l'implementazione predefinita di Equals verificherà automaticamente l'uguaglianza su tutti i campi che penso dovrebbero essere ok su questa struttura.
Hans Olsson

@ho: potrebbe non essere necessario , ma consiglio vivamente di farlo per qualsiasi struttura che servirà come chiave. Vedi la mia modifica.
Dan Tao

3

Se la chiave fa parte della classe, usa KeyedCollection.
È un punto in Dictionarycui la chiave viene derivata dall'oggetto.
Sotto le coperte c'è il Dizionario
Non è necessario ripetere la chiave in Keye Value.
Perché rischiare la chiave non è la stessa nel Keycome Value.
Non è necessario duplicare le stesse informazioni in memoria.

KeyedCollection Classe

Indicizzatore per esporre la chiave composta

    using System.Collections.ObjectModel;

    namespace IntIntKeyedCollection
    {
        class Program
        {
            static void Main(string[] args)
            {
                Int32Int32DateO iid1 = new Int32Int32DateO(0, 1, new DateTime(2007, 6, 1, 8, 30, 52));
                Int32Int32DateO iid2 = new Int32Int32DateO(0, 1, new DateTime(2007, 6, 1, 8, 30, 52));
                if (iid1 == iid2) Console.WriteLine("same");
                if (iid1.Equals(iid2)) Console.WriteLine("equals");
                // that are equal but not the same I don't override = so I have both features

                Int32Int32DateCollection int32Int32DateCollection = new Int32Int32DateCollection();
                // dont't have to repeat the key like Dictionary
                int32Int32DateCollection.Add(new Int32Int32DateO(0, 0, new DateTime(2008, 5, 1, 8, 30, 52)));
                int32Int32DateCollection.Add(new Int32Int32DateO(0, 1, new DateTime(2008, 6, 1, 8, 30, 52)));
                int32Int32DateCollection.Add(iid1);
                //this would thow a duplicate key error
                //int32Int32DateCollection.Add(iid2);
                //this would thow a duplicate key error
                //int32Int32DateCollection.Add(new Int32Int32DateO(0, 1, new DateTime(2008, 6, 1, 8, 30, 52)));
                Console.WriteLine("count");
                Console.WriteLine(int32Int32DateCollection.Count.ToString());
                // reference by ordinal postion (note the is not the long key)
                Console.WriteLine("oridinal");
                Console.WriteLine(int32Int32DateCollection[0].GetHashCode().ToString());
                // reference by index
                Console.WriteLine("index");
                Console.WriteLine(int32Int32DateCollection[0, 1, new DateTime(2008, 6, 1, 8, 30, 52)].GetHashCode().ToString());
                Console.WriteLine("foreach");
                foreach (Int32Int32DateO iio in int32Int32DateCollection)
                {
                    Console.WriteLine(string.Format("HashCode {0} Int1 {1} Int2 {2} DateTime {3}", iio.GetHashCode(), iio.Int1, iio.Int2, iio.Date1));
                }
                Console.WriteLine("sorted by date");
                foreach (Int32Int32DateO iio in int32Int32DateCollection.OrderBy(x => x.Date1).ThenBy(x => x.Int1).ThenBy(x => x.Int2))
                {
                    Console.WriteLine(string.Format("HashCode {0} Int1 {1} Int2 {2} DateTime {3}", iio.GetHashCode(), iio.Int1, iio.Int2, iio.Date1));
                }
                Console.ReadLine();
            }
            public class Int32Int32DateCollection : KeyedCollection<Int32Int32DateS, Int32Int32DateO>
            {
                // This parameterless constructor calls the base class constructor 
                // that specifies a dictionary threshold of 0, so that the internal 
                // dictionary is created as soon as an item is added to the  
                // collection. 
                // 
                public Int32Int32DateCollection() : base(null, 0) { }

                // This is the only method that absolutely must be overridden, 
                // because without it the KeyedCollection cannot extract the 
                // keys from the items.  
                // 
                protected override Int32Int32DateS GetKeyForItem(Int32Int32DateO item)
                {
                    // In this example, the key is the part number. 
                    return item.Int32Int32Date;
                }

                //  indexer 
                public Int32Int32DateO this[Int32 Int1, Int32 Int2, DateTime Date1]
                {
                    get { return this[new Int32Int32DateS(Int1, Int2, Date1)]; }
                }
            }

            public struct Int32Int32DateS
            {   // required as KeyCollection Key must be a single item
                // but you don't really need to interact with Int32Int32DateS directly
                public readonly Int32 Int1, Int2;
                public readonly DateTime Date1;
                public Int32Int32DateS(Int32 int1, Int32 int2, DateTime date1)
                { this.Int1 = int1; this.Int2 = int2; this.Date1 = date1; }
            }
            public class Int32Int32DateO : Object
            {
                // implement other properties
                public Int32Int32DateS Int32Int32Date { get; private set; }
                public Int32 Int1 { get { return Int32Int32Date.Int1; } }
                public Int32 Int2 { get { return Int32Int32Date.Int2; } }
                public DateTime Date1 { get { return Int32Int32Date.Date1; } }

                public override bool Equals(Object obj)
                {
                    //Check for null and compare run-time types.
                    if (obj == null || !(obj is Int32Int32DateO)) return false;
                    Int32Int32DateO item = (Int32Int32DateO)obj;
                    return (this.Int32Int32Date.Int1 == item.Int32Int32Date.Int1 &&
                            this.Int32Int32Date.Int2 == item.Int32Int32Date.Int2 &&
                            this.Int32Int32Date.Date1 == item.Int32Int32Date.Date1);
                }
                public override int GetHashCode()
                {
                    return (((Int64)Int32Int32Date.Int1 << 32) + Int32Int32Date.Int2).GetHashCode() ^ Int32Int32Date.GetHashCode();
                }
                public Int32Int32DateO(Int32 Int1, Int32 Int2, DateTime Date1)
                {
                    Int32Int32DateS int32Int32Date = new Int32Int32DateS(Int1, Int2, Date1);
                    this.Int32Int32Date = int32Int32Date;
                }
            }
        }
    }

Per quanto riguarda l'utilizzo del tipo di valore fpr, la chiave che Microsoft consiglia specificamente contro di essa.

ValueType.GetHashCode

Tuple tecnicamente non è un tipo di valore ma soffre dello stesso sintomo (collisioni di hash) e non è un buon candidato per una chiave.


+1 per una risposta più corretta. Nessuno ne ha parlato prima. Infatti, a seconda di come l'OP intende utilizzare la struttura, anche HashSet<T>con un appropriato IEqualityComparer<T>sarebbe un'opzione. A proposito, penso che la tua risposta attirerà voti se puoi cambiare i nomi delle classi e degli altri nomi dei membri :)
nawfal

2

Posso suggerire un'alternativa: un oggetto anonimo. È lo stesso che usiamo nel metodo GroupBy LINQ con più chiavi.

var dictionary = new Dictionary<object, string> ();
dictionary[new { a = 1, b = 2 }] = "value";

Può sembrare strano, ma ho confrontato Tuple.GetHashCode e i nuovi metodi {a = 1, b = 2} .GetHashCode e gli oggetti anonimi vincono sulla mia macchina su .NET 4.5.1:

Oggetto: 89,1732 ms per 10000 chiamate in 1000 cicli

Tupla - 738,4475 ms per 10000 chiamate in 1000 cicli


omg, questa alternativa non è mai stata nella mia mente ... Non so se si comporterà bene se usi un tipo complesso come chiave composta.
Gabriel Espinoza

Se si passa semplicemente un oggetto (invece di uno anonimo) verrà utilizzato il risultato del metodo GetHashCode di questo oggetto. Se lo usi in questo modo, dictionary[new { a = my_obj, b = 2 }]il codice hash risultante sarà una combinazione di my_obj.GetHashCode e ((Int32) 2) .GetHashCode.
Michael Logutov

NON USARE QUESTO METODO! Diversi assembly creano nomi diversi per i tipi anonimi. Anche se ti sembra anonimo, dietro le quinte è stata creata una classe concreta e due oggetti di due classi diverse non saranno uguali all'operatore predefinito.
Quarkly

E che importanza ha in questo caso?
Michael Logutov

0

Un'altra soluzione a quelle già menzionate sarebbe quella di memorizzare una sorta di elenco di tutte le chiavi generate fino ad ora e quando viene generato un nuovo oggetto si genera il suo hashcode (proprio come punto di partenza), controllare se è già nell'elenco, se è è, quindi aggiungi un valore casuale ecc. finché non hai una chiave univoca, quindi memorizza quella chiave nell'oggetto stesso e nell'elenco e restituiscila come chiave in ogni momento.

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.