複合キー辞書


89

リストにいくつかのオブジェクトList<MyClass>があるとします。たとえば、MyClassにはいくつかのプロパティがあります。MyClassの3つのプロパティに基づいてリストのインデックスを作成したいと思います。この場合、プロパティの2つはintであり、1つのプロパティは日時です。

基本的に私は次のようなことができるようになりたいです:

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

リストに複数の辞書を作成して、それが保持するクラスのさまざまなプロパティにインデックスを付けることがあります。しかし、複合キーをどのように処理するのが最善かわかりません。3つの値のチェックサムを実行することを検討しましたが、これにより衝突のリスクが生じます。


2
タプルを使ってみませんか?彼らはあなたのためにすべての合成を行います。
Eldritch Conundrum、2012

20
どう対応したらいいかわかりません。あなたはあたかも私が故意にタプルを避けていると想定しているかのようにその質問をします。
AaronLS

6
申し訳ありませんが、詳細な回答として書き直しました。
Eldritch Conundrum 2012年

1
カスタムクラスを実装する前に、タプルについてお読みください(Eldritch Conundrumが推奨)-msdn.microsoft.com/en-us/library/system.tuple.aspx。変更が簡単で、カスタムクラスの作成を節約できます。
OSH 2012年

回答:


103

タプルを使用する必要があります。これらはCompositeKeyクラスと同等ですが、Equals()およびGetHashCode()はすでに実装されています。

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

またはSystem.Linqを使用する

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

ハッシュの計算をカスタマイズする必要がない限り、タプルを使用する方が簡単です。

複合キーに含めたいプロパティがたくさんある場合、タプル型の名前はかなり長くなる可能性がありますが、Tuple <...>から派生する独自のクラスを作成することにより、名前を短くすることができます。


** 2017年に編集 **

C#7から始まる新しいオプションがあります。値tuplesです。考え方は同じですが、構文は異なり、軽量です。

タイプTuple<int, bool, string>(int, bool, string)になり、値はにTuple.Create(4, true, "t")なり(4, true, "t")ます。

値タプルを使用すると、要素に名前を付けることも可能になります。パフォーマンスは少し異なるので、必要に応じてベンチマークを行うことをお勧めします。


4
タプルはハッシュ衝突の数が多いため、キーの候補としては適していません。 stackoverflow.com/questions/12657348/…–パ
パラッツォ

1
@Blam KeyValuePair<K,V>およびその他の構造体には、不良であることがわかっているデフォルトのハッシュ関数があります(詳細については、stackoverflow.com / questions / 3841602 / … を参照してください)。Tuple<>ただし、ValueTypeではなく、そのデフォルトのハッシュ関数は少なくともすべてのフィールドを使用します。とはいえ、コードの主な問題が衝突である場合はGetHashCode()、データに適した最適化を実装してください。
Eldritch Conundrum 2014年

1
タプルは私のテストのValueTypeではありませんが、多くの衝突の影響を受けます
パパラッツォ

5
ValueTuplesがあるため、この答えは古くなっていると思います。それらはC#でより良い構文を持ち、GetHashCodeをタプルの2倍の速度で実行するようです-gist.github.com/ljw1004/61bc96700d0b03c17cf83dbb51437a69
Lucian Wischik

3
@LucianWischikありがとう、私はそれらを言及するために答えを更新しました。
Eldritch Conundrum

22

私は考えることができる最高の方法は、CompositeKey構造体を作成することです確認してください GetHashCodeメソッド()とequals()のコレクションで作業するときの速度と精度を確保するためのメソッドオーバーライドすること:

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

GetHashCode()に関するMSDNの記事:

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


それが実際に一意のハッシュコードであることは100%確実であるとは私は思いません。
Hans Olsson

それは本当かもしれません!リンクされているMSDNの記事によると、それがGetHashCode()をオーバーライドする推奨方法です。ただし、日常の作業では複合キーをあまり使用しないので、はっきりとは言えません。
Allen E. Scharfenberg

4
はい。Reflectorを使用してDictionary.FindEntry()を逆アセンブルすると、ハッシュコードと完全な等価性の両方がテストされていることがわかります。ハッシュコードが最初にテストされ、失敗した場合、完全な等価性をチェックせずに条件を短絡します。ハッシュがパスすると、同等性もテストされます。
Jason Kleban、

1
そして、はい、等しいもオーバーライドする必要があります。GetHashCode()でインスタンスを0に戻しても、Dictionaryは機能しますが、速度が遅くなるだけです。
Jason Kleban

2
組み込みのタプル型は、ハッシュの組み合わせを '(h1 ^ h2'ではなく '(h1 << 5)+ h1 ^ h2'として実装します。ハッシュする2つのオブジェクトが同じ値に等しくなるたびに衝突を回避するためにそれらを行うと思います。
Eldritch Conundrum、2012年

13

いかがDictionary<int, Dictionary<int, Dictionary<DateTime, MyClass>>>ですか?

これにより、次のことが可能になります。

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

1
これにより、CompositeKey構造体またはクラスを使用した場合よりも多くのオブジェクトが作成されます。また、2レベルのルックアップが使用されるため、速度も遅くなります。
イアンリングローズ2010

私はそれが比較の数と同じだと思います-もっと多くのオブジェクトがあるのか​​わかりません-複合キーウェイはまだキーを必要とし、それはコンポーネント値またはオブジェクトであり、それらを保持するための1つのディクテーションです。このネストされた方法では、各オブジェクト/値のラッパーキーは必要ありません。追加のネストレベルごとに1つの追加の辞書があります。どう思いますか?
Jason Kleban、2010

9
私が2および3パートのキーで試したベンチマークに基づいて、ネストされた辞書ソリューションは、タプル複合キーアプローチを使用するよりも3〜4倍高速です。ただし、タプルアプローチの方がはるかに簡単です。
RickL 2012年

5
@RickLこれらのベンチマークを確認できます。コードベースでCompositeDictionary<TKey1, TKey2, TValue>(など)と呼ばれるタイプを使用します。これは単に継承するDictionary<TKey1, Dictionary<TKey2, TValue>>(または多くのネストされた辞書が必要です。タイプ全体をゼロから実装するのではなく、ネストされた辞書またはキーを含むタイプ)これは私たちが取得する最速です
Adam Houldsworth

1
中間ディクショナリは完全なハッシュコードの計算と比較をバイパスできるため、ネストされたdictアプローチは、データが存在しない場合の半分(?)の場合にのみ高速になるはずです。データが存在する場合、Add、Containsなどの基本的な操作を3回実行する必要があるため、処理が遅くなります。上記のベンチマークのいくつかでタプルアプローチのマージンが打たれたことを確認してください。.NETタプルの実装の詳細についてです。メモリも考慮して、適切に実装されたトリプレットは私が行くものです
nawfal

12

それらを構造体に保存し、それをキーとして使用できます。

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

ハッシュコードを取得するためのリンク:http : //msdn.microsoft.com/en-us/library/system.valuetype.gethashcode.aspx


.NET 3.5で立ち往生しているTupleので、sにアクセスできないので、これは良い解決策です!
aarona

私はこれがもっと賛成されていないことに驚いています。タプルよりも読みやすいシンプルなソリューションです。
マーク

1
msdnによれば、参照型のフィールドがない場合はこれで問題ありません。それ以外の場合は、同等性のためにリフレクションを使用します。
Gregor Slavec 2013

@Mark構造体の問題は、デフォルトのGetHashCode()実装が実際に構造体のすべてのフィールドを使用することを保証しない(ディクショナリのパフォーマンスが低下する)のに対し、タプルはそのような保証を提供することです。私はそれをテストしました。詳細については、stackoverflow.com / questions / 3841602 /…を参照してください。
Eldritch Conundrum 2014年

8

VS2017 / C#7が登場したので、最善の答えは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)];

私は匿名のValueTupleで辞書を宣言することにしました(string, string, int)。しかし、私は彼らに名前を付けることができたでしょう(string name, string path, int id)

Perfwise、新しいValueTupleはTupleよりも高速です GetHashCodeが、では低速Equalsです。私はあなたがあなたのシナリオにとってどれが最も速いかを理解するために完全なエンドツーエンドの実験をする必要があると思います。しかし、ValueTupleのエンドツーエンドの優れた機能と言語構文により、ValueTupleが成功します。

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

ええ、私は匿名型のソリューションを目の前で爆破させるために大幅な書き直しを行いました(異なるアセンブリで作成された匿名型を比較す​​ることはできません)。ValueTupleは、複合辞書キーの問題に対する比較的エレガントなソリューションのようです。
クォークリー

5

2つのアプローチがすぐに思い浮かびます。

  1. Kevinの提案どおりに行い、キーとして機能する構造体を記述します。この構造体を実装しIEquatable<TKey>、そのEqualsand GetHashCodeメソッドをオーバーライドするようにしてください*。

  2. 内部的にネストされた辞書を利用するクラスを記述します。何かのように:TripleKeyDictionary<TKey1, TKey2, TKey3, TValue>...このクラスは、内部型のメンバを持っているでしょうDictionary<TKey1, Dictionary<TKey2, Dictionary<TKey3, TValue>>>、とのような方法で公開するだろうthis[TKey1 k1, TKey2 k2, TKey3 k3]ContainsKeys(TKey1 k1, TKey2 k2, TKey3 k3)など

* Equalsメソッドのオーバーライドが必要かどうかについての言葉:Equals構造体のメソッドはデフォルトで各メンバーの値を比較することは事実ですが、それはリフレクションを使用して行います-これは本質的にパフォーマンスコストを伴いますので、それほどではありません辞書のキーとして使用するための適切な実装(とにかく私の意見では)。上のMSDNドキュメントによるとValueType.Equals

Equalsメソッドのデフォルトの実装では、リフレクションを使用して、objとこのインスタンスの対応するフィールドを比較します。特定の型のEqualsメソッドをオーバーライドして、メソッドのパフォーマンスを向上させ、型の等価性の概念をより厳密に表現します。


1に関して、私はyuoがEqualsとGetHashcodeをオーバーライドする必要はないと思います。Equalsのデフォルトの実装は、この構造体で大丈夫だと思うすべてのフィールドの等価性を自動的にチェックします。
Hans Olsson、

@ho:必要ないかもしれませんが、キーとして機能する構造体にはそうすることを強くお勧めします。私の編集を参照してください。
Dan Tao

3

キーがクラスの一部である場合は、を使用しますKeyedCollection
これはDictionary、キーがオブジェクトから派生する場所です。
裏では、辞書です。とで
キーを繰り返す必要はありません。 なぜキーが同じではありませんチャンス取るように。 同じ情報をメモリに複製する必要はありません。 KeyValue
KeyValue

KeyedCollectionクラス

複合キーを公開するインデクサー

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

値タイプfprの使用に関しては、マイクロソフトが特に推奨しないキー。

ValueType.GetHashCode

Tuple 技術的には値の型ではありませんが、同じ症状(ハッシュの衝突)が発生し、キーの候補にはなりません。


より正確な回答を得るために+1。驚いたことに誰もそれを先に述べなかった。実際、OPが構造を使用する方法に応じHashSet<T>て、適切なIEqualityComparer<T>オプションもオプションになります。ところで、クラス名や他のメンバー名を変更できるなら、あなたの答えが投票を集めると思います:)
nawfal

2

別の方法を提案できますか-匿名オブジェクト。これは、複数のキーを持つGroupBy LINQメソッドで使用するものと同じです。

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

奇妙に見えるかもしれませんが、Tuple.GetHashCodeとnew {a = 1、b = 2} .GetHashCodeメソッドをベンチマークして、匿名オブジェクトが.NET 4.5.1上のマシンで勝っています。

オブジェクト-1000サイクルで10000コールの場合89,1732 ms

タプル-1000サイクルで10000コールの場合738,4475 ms


omg、この代替案は頭に浮かんだことはありません...複合型として複合型を使用した場合にうまく機能するかどうかはわかりません。
Gabriel Espinoza

(匿名オブジェクトではなく)オブジェクトを渡すだけの場合、このオブジェクトのGetHashCodeメソッドの結果が使用されます。このように使用するとdictionary[new { a = my_obj, b = 2 }]、結果のハッシュコードはmy_obj.GetHashCodeと((Int32)2).GetHashCodeの組み合わせになります。
Michael Logutov

この方法は使用しないでください!異なるアセンブリは、匿名型に対して異なる名前を作成します。匿名のように見えますが、舞台裏では具体的なクラスが作成されており、2つの異なるクラスの2つのオブジェクトはデフォルトの演算子と等しくありません。
クォークリー

この場合、それはどのように重要ですか?
Michael Logutov

0

すでに述べたものに対する別の解決策は、これまでに生成されたすべてのキーのある種のリストを保存し、新しいオブジェクトが生成されたときにそのハッシュコードを(開始点として)生成し、それがすでにリストにあるかどうかを確認することです。つまり、一意のキーが得られるまでランダムな値などを追加し、そのキーをオブジェクト自体とリストに格納し、常にそれをキーとして返します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.