답변:
.NET 4.0을 사용하는 경우 Tuple을 사용하십시오.
lookup = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();
그렇지 않은 경우 a를 정의 Tuple하고 키로 사용할 수 있습니다 . Tuple은 GetHashCode, Equals및 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);
}
}
GetHashCode구현은 매우 좋지 않다. 필드의 순열에 따라 변하지 않습니다.
new object()다른 것과 같을 new object()까요? 그냥 직선 참조 comarison를 사용하지 않습니다 ... 시도 :bool test = new Tuple<int, string>(1, "foo").Equals(new Tuple<int, string>(1, "Foo".ToLower()));
튜플과 중첩 된 사전 기반 접근 방식 사이에서는 거의 항상 튜플 기반을 사용하는 것이 좋습니다.
유지 관리의 관점에서 ,
다음과 같은 기능을 구현하는 것이 훨씬 쉽습니다.
var myDict = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();
...보다
var myDict = new Dictionary<TypeA, Dictionary<TypeB, Dictionary<TypeC, string>>>();
수신자 측에서. 두 번째 경우 각 추가, 조회, 제거 등은 둘 이상의 사전에 대한 조치가 필요합니다.
또한 복합 키에 앞으로 하나 이상의 (또는 더 적은) 필드가 필요한 경우 중첩 된 사전과 후속 검사를 더 추가해야하므로 두 번째 경우 (중첩 된 사전)에서 코드를 상당히 많이 변경해야합니다.
성능 관점 에서 도달 할 수있는 최상의 결론은 직접 측정하는 것입니다. 그러나 미리 고려할 수있는 몇 가지 이론적 한계가 있습니다.
중첩 된 딕셔너리의 경우 모든 키 (외부 및 내부)에 대한 추가 딕셔너리가 있으면 약간의 메모리 오버 헤드가 발생합니다 (튜플을 만드는 것보다 더 많음).
중첩 된 사전의 경우 추가, 업데이트, 조회, 제거 등과 같은 모든 기본 작업을 두 개의 사전에서 수행해야합니다. 이제 중첩 된 사전 접근 방식이 더 빠를 수있는 경우가 있습니다. 즉, 조회되는 데이터가 없을 때 중간 사전이 전체 해시 코드 계산 및 비교를 우회 할 수 있지만 다시 확인해야합니다. 데이터가있는 경우 조회를 두 번 (또는 중첩에 따라 세 번) 수행해야하므로 속도가 느려집니다.
튜플 접근 방식과 관련하여 .NET 튜플은 세트의 키로 사용되어야 할 때 가장 성능이 좋지 않습니다. 그 Equals및 GetHashCode구현으로 인해 값 유형에 대한 박싱이 발생하기 때문 입니다.
튜플 기반 사전을 사용하지만 더 많은 성능을 원한다면 더 나은 구현으로 내 튜플을 사용합니다.
참고로 사전을 멋지게 만들 수있는 화장품은 거의 없습니다.
인덱서 스타일 호출은 훨씬 깔끔하고 직관적 일 수 있습니다. 예를 들어,
string foo = dict[a, b, c]; //lookup
dict[a, b, c] = ""; //update/insertion
따라서 삽입과 조회를 내부적으로 처리하는 사전 클래스에 필요한 인덱서를 노출하십시오.
또한 적절한 IEnumerable인터페이스를 구현하고 Add(TypeA, TypeB, TypeC, string)다음과 같이 컬렉션 이니셜 라이저 구문을 제공하는 메서드를 제공합니다.
new MultiKeyDictionary<TypeA, TypeB, TypeC, string>
{
{ a, b, c, null },
...
};string foo = dict[a][b][c]않습니까?
a. 일반 컬렉션과 마찬가지로 사전을 반복하고 키 속성이있는 경우 확인할 수 있습니다 a. 항상 첫 번째 속성으로 dict에 항목을 가져오고 싶다면 내 대답과 같은 쿼리에 표시된 사전 사전으로 사전을 더 잘 디자인 할 수 있습니다 dict[a].
4두 키 위해 a그리고 b, 당신은 그것을 표준 사전에 확인하고 같은 값을 추가 할 수 있습니다 dict[a] = 4와 dict[b] = 4. 논리적 경우는 이해가되지 수 a및 b하나 개의 단위이어야한다. 이러한 경우 IEqualityComparer속성 중 하나라도 같으면 두 개의 키 인스턴스를 동일하게 하는 사용자 지정 을 정의 할 수 있습니다 . 이 모든 것은 일반적으로 refelction으로 수행 할 수 있습니다.
C # 7을 사용하는 경우 값 튜플을 복합 키로 사용하는 것을 고려해야합니다. 값 튜플은 일반적으로 기존 참조 튜플 ( Tuple<T1, …>) 보다 더 나은 성능을 제공합니다. 값 튜플은 참조 유형이 아닌 값 유형 (구조체)이므로 메모리 할당 및 가비지 수집 비용을 피합니다. 또한 간결하고 직관적 인 구문을 제공하여 원하는 경우 필드 이름을 지정할 수 있습니다. 또한 IEquatable<T>사전에 필요한 인터페이스를 구현합니다 .
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();
좋고, 깨끗하고, 빠르고, 쉽고, 읽기 쉬운 방법은 다음과 같습니다.
다음과 비슷한 것을 추가하십시오.
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;
}
따라서 사전과 함께 사용할 수 있습니다.
var myDictinaryData = new Dictionary<myKey, string>()
{
{new myKey(1, 2, 3), "data123"},
{new myKey(4, 5, 6), "data456"},
{new myKey(7, 8, 9), "data789"}
};
public TypeA DataA => Item1;
어떤 이유로 든 자신 만의 Tuple 클래스를 만들거나 .NET 4.0에 내장 된 클래스를 사용하지 않으려는 경우 가능한 다른 방법이 하나 있습니다. 세 개의 키 값을 하나의 값으로 결합 할 수 있습니다.
예를 들어, 세 값이 64 비트를 넘지 않는 정수 유형 인 경우이를 결합하여 ulong.
최악의 경우에는 문자열의 세 구성 요소가 키의 구성 요소 내에서 발생하지 않는 일부 문자 또는 시퀀스로 구분되어 있는지 확인하는 한 항상 문자열을 사용할 수 있습니다.
string.Format("{0}#{1}#{2}", key1, key2, key3)
이 접근 방식에는 분명히 약간의 컴포지션 오버 헤드가 있지만,이를 위해 사용하는 것에 따라 신경 쓰지 않을 정도로 사소 할 수 있습니다.
JavaScriptSerializer의 배열 을 연결할 수도 있습니다. 이렇게하면 구분 문자를 직접 만들 필요가 없습니다.
key1, key2, key3)를 deliminator (포함하는 문자열했다 "#")
적절한 GetHashCode로 Tuple을 재정의하고 키로 사용합니다.
적절한 메서드를 오버로드하는 한 괜찮은 성능을 볼 수 있습니다.
다음은 참조 용 .NET 튜플입니다.
[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;
}
}
}
소비 코드가 Dictionary 대신 IDictionary <> 인터페이스로 할 수 있다면 내 본능은 사용자 지정 배열 비교기와 함께 SortedDictionary <>를 사용하는 것입니다.
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;
}
}
그리고 (구체적인 예를 위해 int []를 사용하여) 다음과 같이 만듭니다.
var dictionary = new SortedDictionary<int[], string>(new ArrayComparer<int>());