web-dev-qa-db-ja.com

C#の辞書キーとしてのタプル(または配列)

C#で辞書参照テーブルを作成しようとしています。値の3タプルを1つの文字列に解決する必要があります。キーとして配列を使用しようとしましたが、それは機能せず、他に何をすべきかわかりません。この時点で、辞書の辞書の辞書を作成することを検討していますが、それはおそらくJavaScriptでそれを行う方法ですが、見た目はあまりきれいではないでしょう。

100
AlexH

.NET 4.0を使用している場合、タプルを使用します。

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

そうでない場合は、タプルを定義し、それをキーとして使用できます。 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);
    }
}
106
Hallgrim

Tupleとネストされた辞書ベースのアプローチの間では、ほとんどの場合、Tupleベースの方が適しています。

保守性の観点から

  • 次のような機能を実装するのがはるかに簡単です。

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

    より

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

    呼び出し先側から。 2番目のケースでは、追加、検索、削除などのたびに、複数の辞書に対するアクションが必要です。

  • さらに、将来、複合キーにもう1つ(またはそれ以下)のフィールドが必要な場合、2つ目のケース(ネストされた辞書)でコードを大幅に変更する必要があります。ネストされた辞書と後続のチェックを追加する必要があるためです。

パフォーマンスの観点から、到達できる最良の結論は、それを自分で測定することです。ただし、事前に検討できる理論上の制限がいくつかあります。

  • ネストされた辞書の場合、すべてのキー(外部および内部)に追加の辞書があると、メモリのオーバーヘッドが発生します(タプルを作成する場合よりも多くなります)。

  • ネストされた辞書の場合、追加、更新、検索、削除などの基本的なアクションはすべて、2つの辞書で実行する必要があります。中間辞書が完全なハッシュコードの計算と比較をバイパスできるため、ネストされた辞書アプローチがより高速になる場合、つまり、検索されるデータが存在しない場合がありますが、確認するために再度タイミングをとる必要があります。データが存在する場合は、ルックアップを2回(またはネストに応じて3回)実行する必要があるため、速度が遅くなります。

  • タプルアプローチに関して、.NETタプルは、 EqualsおよびGetHashCode実装により値型のボックス化が発生するため、セットのキーとして使用することを意図している場合、最もパフォーマンスが高くありません

Tupleベースのディクショナリを使用しますが、パフォーマンスを向上させたい場合は、実装を改善した独自のTupleを使用します。


ちなみに、辞書をクールにする化粧品はほとんどありません。

  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 }, 
        ...
    };
    _
34
nawfal

優れた、クリーン、高速、簡単で読みやすい方法は次のとおりです。

  • 現在の型の等価メンバー(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をオーバーライドする必要はありません。
12
gabba

何らかの理由で、独自のTupleクラスを作成したり、.NET 4.0に組み込まれたonを使用したりすることを本当に避けたい場合は、他に1つの方法があります。 3つのキー値を組み合わせて1つの値にすることができます。

たとえば、3つの値が64ビットを超えない整数型である場合、それらを組み合わせてulongにできます。

最悪の場合、文字列を使用できるのは、その中の3つのコンポーネントが、キーのコンポーネント内で発生しない文字またはシーケンスで区切られていることを確認する限りです(たとえば、次の3つの数字を試すことができます)。

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

このアプローチには明らかにいくつかの合成オーバーヘッドがありますが、これを使用しているものによっては、気にしないほど些細なことかもしれません。

7
jerryjvl

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

適切なGetHashCodeでTupleをオーバーライドし、それをキーとして使用します。

適切なメソッドをオーバーロードしている限り、適切なパフォーマンスが得られるはずです。

4
John Gietzen

参照用の.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
Michael Graczyk

使用するコードが、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>());
2
mungflesh