Słownik kluczy złożonych


90

Mam kilka obiektów na liście, powiedzmy, List<MyClass>a MyClass ma kilka właściwości. Chciałbym utworzyć indeks listy w oparciu o 3 właściwości MyClass. W tym przypadku 2 właściwości to int, a jedna właściwość to data i godzina.

Zasadniczo chciałbym móc zrobić coś takiego:

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

Czasami tworzę wiele słowników na liście, aby indeksować różne właściwości klas, które posiada. Nie jestem jednak pewien, jak najlepiej obsługiwać klucze kompozytowe. Rozważałem zrobienie sumy kontrolnej trzech wartości, ale grozi to kolizjami.


2
Dlaczego nie używasz krotek? Wszystkie kompozycje wykonują za Ciebie.
Eldritch Conundrum

21
Nie wiem, jak na to odpowiedzieć. Zadajesz to pytanie, jakbyś zakładał, że umyślnie unikam krotek.
AaronLS

6
Przepraszam, przepisałem to jako bardziej szczegółową odpowiedź.
Eldritch Conundrum

1
Przed zaimplementowaniem niestandardowej klasy przeczytaj o Tuple (zgodnie z sugestią Eldritch Conundrum) - msdn.microsoft.com/en-us/library/system.tuple.aspx . Są łatwiejsze do zmiany i oszczędzą ci tworzenia niestandardowych klas.
BHP,

Odpowiedzi:


105

Powinieneś używać krotek. Są one odpowiednikiem klasy CompositeKey, ale Equals () i GetHashCode () zostały już zaimplementowane.

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

Lub za pomocą System.Linq

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

Jeśli nie musisz dostosowywać obliczania skrótu, łatwiej jest używać krotek.

Jeśli istnieje wiele właściwości, które chcesz uwzględnić w kluczu złożonym, nazwa typu krotki może stać się dość długa, ale możesz ją skrócić, tworząc własną klasę wywodzącą się z klasy Tuple <...>.


** wydano w 2017 r. **

Jest nowa opcja zaczynająca się od C # 7: krotki wartości . Pomysł jest ten sam, ale składnia jest inna, lżejsza:

Typ Tuple<int, bool, string>staje się (int, bool, string), a wartość Tuple.Create(4, true, "t")staje się (4, true, "t").

W przypadku krotek wartości możliwe jest również nazwanie elementów. Zwróć uwagę, że wyniki są nieco inne, więc możesz chcieć wykonać pewne testy porównawcze, jeśli mają dla Ciebie znaczenie.


4
Tuple nie jest dobrym kandydatem na klucz, ponieważ powoduje dużą liczbę kolizji z skrótem. stackoverflow.com/questions/12657348/…
paparazzo,

1
@Blam KeyValuePair<K,V>i inne struktury mają domyślną funkcję skrótu, o której wiadomo, że jest zła ( więcej szczegółów na stronie stackoverflow.com/questions/3841602/ ...). Tuple<>jednak nie jest typem wartości, a jego domyślna funkcja skrótu przynajmniej użyje wszystkich pól. Biorąc to pod uwagę, jeśli głównym problemem twojego kodu są kolizje, zaimplementuj optymalizację, GetHashCode()która pasuje do twoich danych.
Eldritch Conundrum

1
Mimo że Tuple nie jest typem ValueType z moich testów, cierpi z powodu wielu kolizji
paparazzo,

5
Myślę, że ta odpowiedź jest nieaktualna teraz, gdy mamy ValueTuples. Mają ładniejszą składnię w C # i wydają się wykonywać GetHashCode dwa razy szybciej niż krotki
Lucian Wischik

3
@LucianWischik Dziękuję, zaktualizowałem odpowiedź, aby o nich wspomnieć.
Eldritch Conundrum

22

Najlepszym sposobem, jaki mogłem wymyślić, jest utworzenie struktury CompositeKey i upewnienie się, że nadpisujesz metody GetHashCode () i Equals (), aby zapewnić szybkość i dokładność podczas pracy z kolekcją:

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

Artykuł MSDN na temat GetHashCode ():

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


Nie sądzę, żeby to był w 100% pewien unikalny kod skrótu, po prostu bardzo prawdopodobne.
Hans Olsson

To może być prawda! Zgodnie z połączonym artykułem MSDN jest to zalecany sposób zastąpienia GetHashCode (). Ponieważ jednak w mojej codziennej pracy nie używam wielu kluczy kompozytowych, nie mogę powiedzieć na pewno.
Allen E. Scharfenberg

4
Tak. Jeśli zdemontujesz Dictionary.FindEntry () z Reflektorem, zobaczysz, że testowane są zarówno kod skrótu, jak i pełna równość. Kod skrótu jest najpierw testowany, a jeśli się nie powiedzie, zwiera warunek bez sprawdzania pełnej równości. Jeśli hash przejdzie pomyślnie, równość również jest testowana.
Jason Kleban

1
I tak, wartości równe również powinny zostać zastąpione w celu dopasowania. Nawet jeśli sprawisz, że GetHashCode () zwróci 0 dla dowolnej instancji, słownik nadal będzie działał, po prostu będzie wolniejszy.
Jason Kleban

2
Wbudowany typ Tuple implementuje kombinację skrótu jako '(h1 << 5) + h1 ^ h2' zamiast 'h1 ^ h2'. Sądzę, że robią to, aby uniknąć kolizji za każdym razem, gdy dwa obiekty do skrótu są równe tej samej wartości.
Eldritch Conundrum

13

A co powiesz Dictionary<int, Dictionary<int, Dictionary<DateTime, MyClass>>>?

Umożliwiłoby to:

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

1
stworzy to o wiele więcej obiektów niż użycie struktury lub klasy CompositeKey. i będzie również wolniejszy, ponieważ używane będą dwa poziomy wyszukiwania.
Ian Ringrose

Uważam, że jest to ta sama liczba porównań - nie widzę, jak byłoby dużo więcej obiektów - klucz złożony nadal wymaga klucza, a jego wartości składowe lub obiekty i jeden dykt mają je trzymać. W ten zagnieżdżony sposób nie potrzebujesz klucza opakowania dla każdego obiektu / wartości, jeden dodatkowy dykt dla każdego dodatkowego poziomu zagnieżdżenia. Co myślisz?
Jason Kleban,

9
Na podstawie mojego testu porównawczego, który wypróbowałem z kluczami z 2 i 3 częściami: rozwiązanie ze słownikiem zagnieżdżonym jest 3-4 razy szybsze niż użycie metody klucza złożonego krotki. Jednak podejście krotki jest o wiele łatwiejsze / uporządkowane.
RickL

5
@RickL Mogę potwierdzić te testy porównawcze, używamy w naszej bazie kodu typu o nazwie CompositeDictionary<TKey1, TKey2, TValue>(itp.), Który po prostu dziedziczy po Dictionary<TKey1, Dictionary<TKey2, TValue>>(lub ile potrzeba zagnieżdżonych słowników). Bez implementowania całego typu od podstaw samodzielnie (zamiast oszukiwać używając zagnieżdżone słowniki lub typy zawierające klucze) to jest najszybszy, jaki otrzymujemy.
Adam Houldsworth,

1
Podejście zagnieżdżonego dyktowania powinno być szybsze tylko w przypadku połowy (?) Przypadków, w których nie ma danych, ponieważ słowniki pośrednie mogą ominąć pełne obliczanie i porównywanie kodu skrótu. W przypadku obecności danych powinno być wolniejsze, ponieważ podstawowe operacje, takie jak Dodaj, Zawiera itp., Powinny być wykonywane trzykrotnie. Jestem pewien, że marża w podejściu krotkowym została pokonana w niektórych z wyżej wymienionych testów porównawczych, jeśli chodzi o szczegóły implementacji krotek .NET, które są dość słabe, biorąc pod uwagę karę bokserską, jaką przynosi ona typom wartości. Właściwie zaimplementowana trójka jest tym, z czym bym poszedł, biorąc pod uwagę również pamięć
nawfal

12

Możesz przechowywać je w strukturze i używać tego jako klucza:

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

Link do kodu skrótu: http://msdn.microsoft.com/en-us/library/system.valuetype.gethashcode.aspx


Utknąłem na .NET 3.5, więc nie mam dostępu do Tuples, więc jest to dobre rozwiązanie!
aarona

Dziwię się, że nie jest to bardziej przychylne. To proste rozwiązanie, które jest bardziej czytelne niż krotka.
Mark

1
Według msdn działa to dobrze, jeśli żadne pola nie są typami referencyjnymi, w przeciwnym razie używa odbicia dla równości.
Gregor Slavec

@Mark Problem ze strukturą polega na tym, że jej domyślna implementacja GetHashCode () w rzeczywistości nie gwarantuje wykorzystania wszystkich pól struktury (co prowadzi do słabej wydajności słownika), podczas gdy Tuple oferuje taką gwarancję. Przetestowałem to. Więcej szczegółów na stronie stackoverflow.com/questions/3841602/… .
Eldritch Conundrum

8

Teraz, gdy pojawił się VS2017 / C # 7, najlepszą odpowiedzią jest użycie 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)];

Zdecydowałem się zadeklarować słownik za pomocą anonimowej ValueTuple (string, string, int). Ale mogłem nadać im imiona (string name, string path, int id).

Perfekcyjnie, nowa ValueTuple jest szybsza niż Tuple w, GetHashCodeale wolniejsza w Equals. Myślę, że musiałbyś przeprowadzić kompletne eksperymenty od końca do końca, aby dowiedzieć się, który jest naprawdę najszybszy w Twoim scenariuszu. Ale kompleksowa wygoda i składnia języka dla ValueTuple sprawiają, że wygrywa.

// 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

Tak, przeszedłem duży przepis, aby rozwiązanie typu anonimowego wybuchło mi w twarz (nie mogę porównać anonimowych typów utworzonych z różnymi zespołami). ValueTuple wydaje się być stosunkowo eleganckim rozwiązaniem problemu kluczy ze słownika złożonego.
Quarkly

5

Natychmiast przychodzą na myśl dwa podejścia:

  1. Zrób tak, jak zasugerował Kevin i napisz strukturę, która będzie służyć jako twój klucz. Upewnij się, że ta struktura została zaimplementowana IEquatable<TKey>i nadpisano jej Equalsi GetHashCodemetody *.

  2. Napisz klasę, która wewnętrznie korzysta z zagnieżdżonych słowników. Coś jak: TripleKeyDictionary<TKey1, TKey2, TKey3, TValue>... ta klasa będzie mieć wewnętrznie członek rodzaju Dictionary<TKey1, Dictionary<TKey2, Dictionary<TKey3, TValue>>>i naraziłoby metod, takich jak this[TKey1 k1, TKey2 k2, TKey3 k3], ContainsKeys(TKey1 k1, TKey2 k2, TKey3 k3)itp

* Słowo o tym, czy nadpisywanie Equalsmetody jest konieczne: podczas gdy prawdą jest, żeEquals metoda dla struktury domyślnie porównuje wartość każdego elementu członkowskiego, robi to przy użyciu refleksji - która z natury wiąże się z kosztami wydajności - i dlatego nie jest bardzo odpowiednia implementacja czegoś, co ma być używane jako klucz w słowniku (w każdym razie moim zdaniem). Zgodnie z dokumentacją MSDN dotyczącą ValueType.Equals:

Domyślna implementacja metody Equals używa odbicia do porównania odpowiednich pól obj i tego wystąpienia. Zastąp metodę Equals dla określonego typu, aby poprawić wydajność metody i dokładniej reprezentować koncepcję równości dla typu.


Jeśli chodzi o 1, nie sądzę, abyś musiał zastąpić Equals i GetHashcode, domyślna implementacja Equals automatycznie sprawdzi równość we wszystkich polach, które moim zdaniem powinny być w porządku w tej strukturze.
Hans Olsson

@ho: Może to nie być konieczne , ale zdecydowanie radziłbym zrobić to dla każdej struktury, która ma służyć jako klucz. Zobacz moją edycję.
Dan Tao

3

Jeśli klucz jest częścią klasy, użyj KeyedCollection.
Jest to miejsce, w Dictionaryktórym klucz pochodzi z obiektu.
Pod okładkami jest słownik.
Nie trzeba powtarzać klucza w Keyi Value.
Po co ryzykować, klucz nie jest taki sam Keyjak w Value.
Nie musisz kopiować tych samych informacji w pamięci.

KeyedCollection Class

Indeksator, aby uwidocznić klucz złożony

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

Jeśli chodzi o używanie typu wartości fpr, klucz, który Microsoft wyraźnie odradza.

ValueType.GetHashCode

Tuple z technicznego punktu widzenia nie jest typem wartości, ale cierpi na ten sam objaw (kolizje skrótów) i nie jest dobrym kandydatem na klucz.


+1 za bardziej poprawną odpowiedź. Zaskoczony, nikt o tym wcześniej nie wspomniał. W rzeczywistości, w zależności od tego, jak OP zamierza używać struktury, HashSet<T>odpowiednia IEqualityComparer<T>byłaby również opcja. Przy okazji, myślę, że twoja odpowiedź przyciągnie głosy, jeśli możesz zmienić nazwy swoich klas i innych członków :)
nawfal

2

Czy mogę zasugerować alternatywę - anonimowy przedmiot. To jest to samo, którego używamy w metodzie GroupBy LINQ z wieloma kluczami.

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

Może to wyglądać dziwnie, ale przetestowałem Tuple.GetHashCode i nowe metody {a = 1, b = 2} .GetHashCode, a anonimowe obiekty wygrywają na moim komputerze w .NET 4.5.1:

Obiekt - 89,1732 ms dla 10000 wywołań w 1000 cykli

Tuple - 738,4475 ms dla 10000 wywołań w 1000 cykli


omg, nigdy nie myślałem o tej alternatywie ... Nie wiem, czy będzie się dobrze zachowywać, jeśli użyjesz typu złożonego jako klucza złożonego.
Gabriel Espinoza

Jeśli po prostu przekażesz obiekt (zamiast anonimowego), zostanie użyty wynik metody GetHashCode tego obiektu. Jeśli użyjesz go w dictionary[new { a = my_obj, b = 2 }]ten sposób, wynikowy kod skrótu będzie kombinacją my_obj.GetHashCode i ((Int32) 2) .GetHashCode.
Michael Logutov

NIE UŻYWAJ TEJ METODY! Różne zestawy tworzą różne nazwy dla typów anonimowych. Chociaż wygląda to na anonimowe, za kulisami została utworzona konkretna klasa i dwa obiekty dwóch różnych klas nie będą równe operatorowi domyślnemu.
Quarkly,

A jakie to ma znaczenie w tym przypadku?
Michael Logutov

0

Innym rozwiązaniem do tych już wymienionych byłoby przechowywanie jakiejś listy wszystkich kluczy wygenerowanych do tej pory, a gdy generowany jest nowy obiekt, generujesz jego hashcode (jako punkt wyjścia), sprawdź, czy jest już na liście, czy tak jest, następnie dodaj do niego jakąś losową wartość itp., aż uzyskasz unikalny klucz, a następnie przechowuj ten klucz w samym obiekcie i na liście i zwracaj go jako klucz przez cały czas.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.