답변:
.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>());