C #에서 튜플 (또는 배열)을 사전 키로 사용


109

C #에서 사전 조회 테이블을 만들려고합니다. 3 튜플의 값을 하나의 문자열로 해결해야합니다. 배열을 키로 사용해 보았지만 작동하지 않았고 다른 작업을해야할지 모르겠습니다. 이 시점에서 나는 사전 사전을 만드는 것을 고려하고 있지만, 자바 스크립트로하는 방법이지만보기에는보기에 그리 예쁘지 않을 것입니다.

답변:


113

.NET 4.0을 사용하는 경우 Tuple을 사용하십시오.

lookup = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();

그렇지 않은 경우 a를 정의 Tuple하고 키로 사용할 수 있습니다 . Tuple은 GetHashCode, EqualsIEquatable다음 을 재정의해야합니다 .

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

6
이 구조체는 IEquatable <Tuple <T, U, W >>도 구현해야합니다. 이렇게하면 해시 코드 충돌시 Equals ()가 호출 될 때 권투를 피할 수 있습니다.
Dustin Campbell

16
@jerryjvl과 내가 한 것처럼 Google에서 이것을 찾은 다른 모든 사람들은 .NET 4의 Tuple 은 equals를 구현 하여 사전에서 사용할 수 있습니다.
Scott Chamberlain

30
귀하의 GetHashCode구현은 매우 좋지 않다. 필드의 순열에 따라 변하지 않습니다.
CodesInChaos

2
튜플은 구조체가 아니어야합니다. 프레임 워크에서 Tuple은 참조 유형입니다.
Michael Graczyk 2012 년

5
@Thoraot-물론 귀하의 예는 거짓입니다 ... 그럴 것입니다. 왜 new object()다른 것과 같을 new object()까요? 그냥 직선 참조 comarison를 사용하지 않습니다 ... 시도 :bool test = new Tuple<int, string>(1, "foo").Equals(new Tuple<int, string>(1, "Foo".ToLower()));
마이크 Marynowski

35

튜플과 중첩 된 사전 기반 접근 방식 사이에서는 거의 항상 튜플 기반을 사용하는 것이 좋습니다.

유지 관리의 관점에서 ,

  • 다음과 같은 기능을 구현하는 것이 훨씬 쉽습니다.

    var myDict = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();

    ...보다

    var myDict = new Dictionary<TypeA, Dictionary<TypeB, Dictionary<TypeC, string>>>();

    수신자 측에서. 두 번째 경우 각 추가, 조회, 제거 등은 둘 이상의 사전에 대한 조치가 필요합니다.

  • 또한 복합 키에 앞으로 하나 이상의 (또는 더 적은) 필드가 필요한 경우 중첩 된 사전과 후속 검사를 더 추가해야하므로 두 번째 경우 (중첩 된 사전)에서 코드를 상당히 많이 변경해야합니다.

성능 관점 에서 도달 할 수있는 최상의 결론은 직접 측정하는 것입니다. 그러나 미리 고려할 수있는 몇 가지 이론적 한계가 있습니다.

  • 중첩 된 딕셔너리의 경우 모든 키 (외부 및 내부)에 대한 추가 딕셔너리가 있으면 약간의 메모리 오버 헤드가 발생합니다 (튜플을 만드는 것보다 더 많음).

  • 중첩 된 사전의 경우 추가, 업데이트, 조회, 제거 등과 같은 모든 기본 작업을 두 개의 사전에서 수행해야합니다. 이제 중첩 된 사전 접근 방식이 더 빠를 수있는 경우가 있습니다. 즉, 조회되는 데이터가 없을 때 중간 사전이 전체 해시 코드 계산 및 비교를 우회 할 수 있지만 다시 확인해야합니다. 데이터가있는 경우 조회를 두 번 (또는 중첩에 따라 세 번) 수행해야하므로 속도가 느려집니다.

  • 튜플 접근 방식과 관련하여 .NET 튜플은 세트의 키로 사용되어야 할 때 가장 성능이 좋지 않습니다. 그 EqualsGetHashCode구현으로 인해 값 유형에 대한 박싱이 발생하기 때문 입니다.

튜플 기반 사전을 사용하지만 더 많은 성능을 원한다면 더 나은 구현으로 내 튜플을 사용합니다.


참고로 사전을 멋지게 만들 수있는 화장품은 거의 없습니다.

  1. 인덱서 스타일 호출은 훨씬 깔끔하고 직관적 일 수 있습니다. 예를 들어,

    string foo = dict[a, b, c]; //lookup
    dict[a, b, c] = ""; //update/insertion

    따라서 삽입과 조회를 내부적으로 처리하는 사전 클래스에 필요한 인덱서를 노출하십시오.

  2. 또한 적절한 IEnumerable인터페이스를 구현하고 Add(TypeA, TypeB, TypeC, string)다음과 같이 컬렉션 이니셜 라이저 구문을 제공하는 메서드를 제공합니다.

    new MultiKeyDictionary<TypeA, TypeB, TypeC, string> 
    { 
        { a, b, c, null }, 
        ...
    };

중첩 된 사전의 경우 인덱서 구문이 다음과 같지 string foo = dict[a][b][c]않습니까?
Steven Rands

@StevenRands 그렇습니다.
nawfal

1
@nawfal 모두가 아닌 키 중 하나만있는 경우 튜플 사전을 검색 할 수 있습니까? 또는이 dict [a, b] 다음 dict [a, c]처럼 할 수 있습니까?
Khan Engineer

@KhanEngineer 많은 것은 사전의 의도 된 목적이 무엇인지 또는 그것을 사용하려는 방법에 달려 있습니다. 예를 들어, 키의 일부로 값을 되찾고 자합니다 a. 일반 컬렉션과 마찬가지로 사전을 반복하고 키 속성이있는 경우 확인할 수 있습니다 a. 항상 첫 번째 속성으로 dict에 항목을 가져오고 싶다면 내 대답과 같은 쿼리에 표시된 사전 사전으로 사전을 더 잘 디자인 할 수 있습니다 dict[a].
nawfal

"하나의 키로 만 검색"하면 보유한 키로 값을 되 찾는다는 의미라면 사전을 일종의 "모든 키 사전"으로 재 설계하는 것이 좋습니다. 예를 들어 당신이 값을 얻으려면 4두 키 위해 a그리고 b, 당신은 그것을 표준 사전에 확인하고 같은 값을 추가 할 수 있습니다 dict[a] = 4dict[b] = 4. 논리적 경우는 이해가되지 수 ab하나 개의 단위이어야한다. 이러한 경우 IEqualityComparer속성 중 하나라도 같으면 두 개의 키 인스턴스를 동일하게 하는 사용자 지정 을 정의 할 수 있습니다 . 이 모든 것은 일반적으로 refelction으로 수행 할 수 있습니다.
nawfal

30

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();

13

좋고, 깨끗하고, 빠르고, 쉽고, 읽기 쉬운 방법은 다음과 같습니다.

  • 현재 유형에 대해 동등 멤버 (Equals () 및 GetHashCode ()) 를 생성합니다. ReSharper 와 같은 도구 는 메서드를 생성 할뿐만 아니라 동등성 검사 및 / 또는 해시 코드 계산에 필요한 코드도 생성합니다. 생성 된 코드는 Tuple 실현보다 최적입니다.
  • 튜플에서 파생 된 간단한 키 클래스를 만드십시오 .

다음과 비슷한 것을 추가하십시오.

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"}
};
  • 계약서에서도 사용할 수 있습니다.
  • linq에서 조인 또는 그룹화를위한 키로
  • 이렇게하면 Item1, Item2, Item3의 순서를 잘못 입력하지 않습니다.
  • 무언가를 얻기 위해 어디로 가야하는지 이해하기 위해 코드를 기억하거나 살펴볼 필요가 없습니다.
  • IStructuralEquatable, IStructuralComparable, IComparable, ITuple을 모두 재정의 할 필요가 없습니다.

1
이제 표현 바디 멤버를 사용할 수 있습니다. 예public TypeA DataA => Item1;
andrewtatham

7

어떤 이유로 든 자신 만의 Tuple 클래스를 만들거나 .NET 4.0에 내장 된 클래스를 사용하지 않으려는 경우 가능한 다른 방법이 하나 있습니다. 세 개의 키 값을 하나의 값으로 결합 할 수 있습니다.

예를 들어, 세 값이 64 비트를 넘지 않는 정수 유형 인 경우이를 결합하여 ulong.

최악의 경우에는 문자열의 세 구성 요소가 키의 구성 요소 내에서 발생하지 않는 일부 문자 또는 시퀀스로 구분되어 있는지 확인하는 한 항상 문자열을 사용할 수 있습니다.

string.Format("{0}#{1}#{2}", key1, key2, key3)

이 접근 방식에는 분명히 약간의 컴포지션 오버 헤드가 있지만,이를 위해 사용하는 것에 따라 신경 쓰지 않을 정도로 사소 할 수 있습니다.


6
그래도 상황에 따라 크게 달라진다고 말하고 싶습니다. 결합 할 세 가지 정수 유형이 있고 성능이 중요하지 않은 경우 실수 할 가능성을 최소화하면서 완벽하게 작동합니다. 물론,이 모든 것은 .NET 4부터 완전히 중복됩니다. Microsoft는 (아마도 정확할 것입니다!) 튜플 유형을 즉시 제공 할 것입니다.
jerryjvl 2009-06-06

이 메서드를와 함께 사용하여 문자열 및 / 또는 정수 유형 JavaScriptSerializer배열 을 연결할 수도 있습니다. 이렇게하면 구분 문자를 직접 만들 필요가 없습니다.
binki

3
키 (중 경우에 진짜 지저분를 얻을 수 key1, key2, key3)를 deliminator (포함하는 문자열했다 "#")
그렉

4

적절한 GetHashCode로 Tuple을 재정의하고 키로 사용합니다.

적절한 메서드를 오버로드하는 한 괜찮은 성능을 볼 수 있습니다.


1
IComparable은 키가 Dictionary <TKey, TValue>에 저장되거나 배치되는 방식에 영향을주지 않습니다. 모두 GetHashCode () 및 IEqualityComparer <T>를 통해 수행됩니다. IEquatable <T>를 구현하면 Equals (object) 함수로 대체되는 기본 EqualityComparer로 인한 권투가 완화되므로 성능이 향상됩니다.
Dustin Campbell

GetHashCode에 대해 언급하려고했지만 HashCode가 동일한 경우 사전에서 IComparable을 사용한다고 생각했습니다. 제가 틀린 것 같습니다.
John Gietzen

3

다음은 참조 용 .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;
        }
    } 
}

3
해시 코드 ((항목 1 ^ 항목 2) * 33) ^ 항목 3로 구현하기
마이클 그 랙직

2

소비 코드가 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>());
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.