복합 키 사전


90

List에 몇 가지 개체가 List<MyClass>있고 MyClass에는 여러 속성이 있습니다. MyClass의 3 가지 속성을 기반으로 목록의 인덱스를 만들고 싶습니다. 이 경우 속성 중 2 개는 int이고 한 속성은 datetime입니다.

기본적으로 다음과 같은 작업을 수행하고 싶습니다.

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

나는 때때로 목록에 여러 사전을 만들어 보유하고있는 클래스의 다른 속성을 인덱싱합니다. 그래도 복합 키를 처리하는 가장 좋은 방법을 모르겠습니다. 세 가지 값의 체크섬을 고려했지만 충돌 위험이 있습니다.


2
튜플을 사용하지 않는 이유는 무엇입니까? 그들은 당신을 위해 모든 합성을 수행합니다.
Eldritch Conundrum 2012

21
어떻게 대응해야할지 모르겠습니다. 내가 의도적으로 튜플을 피하고 있다고 가정 한 것처럼 그 질문을합니다.
AaronLS 2012

6
죄송합니다. 더 자세한 답변으로 다시 작성했습니다.
엘드 수수께끼

1
사용자 지정 클래스를 구현하기 전에 Tuple (Eldritch Conundrum에서 제안한대로)에 대해 읽어보십시오 -msdn.microsoft.com/en-us/library/system.tuple.aspx . 변경하기가 더 쉬우 며 사용자 정의 클래스 생성을 줄일 수 있습니다.
OSH

답변:


105

튜플을 사용해야합니다. 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 형식 이름이 상당히 길어질 수 있지만 Tuple <...>에서 파생 된 고유 한 클래스를 만들어 이름을 짧게 만들 수 있습니다.


** 2017 년 편집 **

C # 7로 시작하는 새로운 옵션이 있습니다. 값 튜플 . . 아이디어는 동일하지만 구문이 다르고 가볍습니다.

유형 Tuple<int, bool, string>(int, bool, string)이고 값 Tuple.Create(4, true, "t")(4, true, "t") .

값 튜플을 사용하면 요소의 이름을 지정할 수도 있습니다. 성능은 약간 다르므로 중요한 경우 벤치마킹을 수행하는 것이 좋습니다.


4
튜플은 많은 수의 해시 충돌을 생성하므로 키에 적합한 후보가 아닙니다. stackoverflow.com/questions/12657348/…
paparazzo

1
@Blam KeyValuePair<K,V>및 기타 구조체에는 잘못된 것으로 알려진 기본 해시 함수가 있습니다 ( 자세한 내용 은 stackoverflow.com/questions/3841602/… 참조). Tuple<>그러나 ValueType이 아니며 기본 해시 함수는 적어도 모든 필드를 사용합니다. 즉, 코드의 주요 문제가 충돌 인 경우 GetHashCode()데이터에 적합한 최적화 를 구현 하십시오.
Eldritch Conundrum 2014 년

1
튜플 내이 테스트에서 치형는 아니지만 그것은 충돌의 AA 많이 앓고
파파라치

5
ValueTuples가 있으므로이 답변은 구식이라고 생각합니다. 그들은 C #에서 더 좋은 구문을 가지고 있으며, 튜플보다 두 배 빠른 GetHashCode를 수행하는 것 같습니다 -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
내장 Tuple 유형은 'h1 ^ h2'대신 '(h1 << 5) + h1 ^ h2'로 해시 조합을 구현합니다. 해시 할 두 개체가 동일한 값과 같을 때마다 충돌을 피하기 위해 그렇게한다고 생각합니다.
Eldritch Conundrum 2012 년

13

어때요 Dictionary<int, Dictionary<int, Dictionary<DateTime, MyClass>>>?

이렇게하면 다음을 수행 할 수 있습니다.

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

1
이렇게하면 CompositeKey 구조체 또는 클래스를 사용하는 것보다 훨씬 더 많은 객체가 생성됩니다. 또한 두 가지 수준의 조회가 사용되므로 속도가 느려집니다.
이안 Ringrose

나는 그것이 동일한 수의 비교라고 믿습니다. 객체가 더 많이 있을지는 모르겠습니다. 복합 키 방식에는 여전히 키가 필요하며 구성 요소 값 또는 객체와이를 보유하는 하나의 사전입니다. 이렇게 중첩 된 방식으로 각 개체 / 값에 대한 래퍼 키가 필요하지 않으며 각 추가 중첩 수준에 대해 하나의 추가 dict가 필요합니다. 어떻게 생각해?
Jason Kleban

9
내 벤치마킹을 기반으로 두 부분과 세 부분으로 된 키로 시도했습니다. 중첩 된 사전 솔루션은 튜플 복합 키 접근 방식을 사용하는 것보다 3-4 배 빠릅니다. 그러나 튜플 접근 방식은 훨씬 쉽고 간단합니다.
RickL

5
@RickL 이러한 벤치 마크를 확인할 수 있습니다. 우리는 코드 기반에서 CompositeDictionary<TKey1, TKey2, TValue>(etc) 라고하는 유형을 사용합니다.이 유형은 단순히 상속 Dictionary<TKey1, Dictionary<TKey2, TValue>>(또는 많은 중첩 사전이 필요합니다. . 중첩 된 사전 또는 유형이 가장 빠른 우리가 얻을입니다) 키를 포함하는
아담 Houldsworth

1
중첩 된 dict 접근 방식은 중간 사전이 전체 해시 코드 계산 및 비교를 우회 할 수 있으므로 데이터가없는 경우의 절반 (?)에 대해서만 더 빠릅니다. 데이터가있는 경우 추가, 포함 등의 기본 작업을 세 번 수행해야하므로 속도가 느려집니다. 위에서 언급 한 일부 벤치 마크에서 튜플 접근 방식의 마진은 .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에 붙어있어서 Tuples에 액세스 할 수 없으므로 좋은 해결책입니다!
aarona

나는 이것이 더 이상 찬성되지 않는다는 것에 놀랐습니다. 튜플보다 더 읽기 쉬운 간단한 솔루션입니다.
Mark

1
msdn에 따르면 이것은 필드가 참조 유형이 아니면 정상을 수행하고 그렇지 않으면 동등성을 위해 리플렉션을 사용합니다.
Gregor Slavec 2013

@Mark 구조체의 문제는 기본 GetHashCode () 구현이 실제로 구조체의 모든 필드를 사용하는 것을 보장하지 않는 (사전 성능 저하로 이어짐) 반면 Tuple은 그러한 보장을 제공한다는 것입니다. 나는 그것을 테스트했다. 자세한 내용은 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은 튜플보다 빠르지 GetHashCodeEquals. 귀하의 시나리오에 가장 빠른 것이 무엇인지 파악하려면 완전한 엔드 투 엔드 실험을 수행해야한다고 생각합니다. 그러나 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은 복합 사전 키 문제에 대한 비교적 우아한 해결책 인 것 같습니다.
Quarkly

5

두 가지 접근 방식이 즉시 떠 오릅니다.

  1. Kevin이 제안한대로 수행하고 키 역할을 할 구조체를 작성합니다. 이 구조체를 구현 IEquatable<TKey>하고 해당 EqualsGetHashCode메서드 * 를 재정의해야 합니다.

  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과 관련하여 Equals 및 GetHashcode를 재정의 할 필요가 없다고 생각합니다. Equals의 기본 구현은이 구조체에서 괜찮다고 생각하는 모든 필드에서 동등성을 자동으로 확인합니다.
Hans Olsson

@ho : 필요 하지 않을 수도 있지만 키 역할을 할 모든 구조체에 대해 그렇게하는 것이 좋습니다. 내 편집을 참조하십시오.
Dan Tao

3

키가 클래스의 일부인 경우 KeyedCollection.
그것은 인 Dictionary키가 개체로부터 유래된다.
내부적으로는 Dictionary 입니다. and
에서 키를 반복 할 필요가 없습니다 . 왜 키가에서 동일하지 않습니다 기회 취할 는 AS를 . 메모리에 동일한 정보를 복제 할 필요가 없습니다. 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 사용에 관해서는 Microsoft가 특별히 권장하는 키입니다.

ValueType.GetHashCode

Tuple 기술적으로는 값 유형이 아니지만 동일한 증상 (해시 충돌)이 발생하고 키에 적합한 후보가 아닙니다.


더 정답을 보려면 +1하세요. 놀랍게도 아무도 앞서 언급하지 않았습니다. 실제로 OP가 구조를 사용하려는 의도에 따라 HashSet<T>적절한 IEqualityComparer<T>옵션도 선택할 수 있습니다. BTW, 당신은 클래스 이름과 다른 멤버 이름 : 변경할 수있는 경우에 당신의 대답은 투표를 끌 것이라고 생각
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,1732ms

튜플-1000주기에서 10000 호출에 대해 738,4475ms


omg,이 대안은 내 마음에 없었습니다 ... 복합 키로 복합 유형을 사용하면 잘 작동할지 모르겠습니다.
Gabriel Espinoza

익명 개체 대신 개체를 전달하면이 개체의 GetHashCode 메서드 결과가 사용됩니다. 이렇게 사용 dictionary[new { a = my_obj, b = 2 }]하면 결과 해시 코드는 my_obj.GetHashCode와 ((Int32) 2) .GetHashCode의 조합이됩니다.
Michael Logutov

이 방법을 사용하지 마십시오! 어셈블리마다 익명 형식에 대해 다른 이름을 만듭니다. 익명으로 보이지만이면에는 구체적인 클래스가 생성되고 두 개의 다른 클래스의 두 개체는 기본 연산자와 동일하지 않습니다.
Quarkly

그리고이 경우 어떻게 중요합니까?
Michael Logutov

0

이미 언급 한 것들에 대한 또 다른 해결책은 지금까지 생성 된 모든 키 목록을 저장하고 새 객체가 생성 될 때 해시 코드를 생성하고 (시작점으로) 이미 목록에 있는지 확인하는 것입니다. 그런 다음 고유 키를 얻을 때까지 임의의 값 등을 추가 한 다음 해당 키를 객체 자체와 목록에 저장하고 항상 키로 반환합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.