21

私は、C#のジェネリックDictionaryクラスが提供する複数のキーの値を格納および取得する方法を探していました。

Web(およびSO自体)を検索すると、いくつかのオプションが表示されます。

タプルベースの辞書

.NET 4.0を使用すると、汎用のTuple <、>クラスを簡単にサポートできます。これは、任意のタプルから辞書を作成できることを意味します。

  • var myDict = new Dictionary<Tuple<Char, Int>, MyClass>();

ネストされた辞書

ディクショナリ内にディクショナリをネストすることもできることを学びました。これにより、N次元配列にアクセスするのと同じように保存された結果にアクセスできます。例えば:

Dictionary<int, Dictionary<int, Dictionary<Char, MyClass>>>

その後、次のようにアクセスできます。MyClass foo = MyData[8][3]['W'];

区切られた連結キー辞書

どちらも複雑データとカスタムクラスには適していますが、常に必要かどうか疑問に思います。少なくともプリミティブデータの場合、キーを区切り文字で連結することも同様に効果的であるように思われます。

//keys are char + int
Dictionary<string, MyClass> myDict = New Dictionary<string, Myclass>();
String input = myChar + "|" + myInt
MyClass foo = myDict[input]

これらの方法の1つを他の方法よりも優れたものにするシナリオはありますか?彼らは同じようなパフォーマンス時間を持っていますか?または、代わりに、どのメソッドが最もクリーンで保守が容易なコードを提供するかに焦点を当てる必要がありますか?

考え?

4

4 に答える 4

17

区切られた連結キー辞書

このアプローチを避ける理由は少なくとも3つあります。

  • 魔法です。キーのタイプには、キーの作成方法やキーが何を表すかを示すものは何もありません。
  • 区切り文字が誤って値の1つとして表示された場合、アプローチは失敗します。
  • 文字列への変換、およびこれらの文字列の比較は、2つのプリミティブ型を使用するよりも(わずかに)遅くなる可能性があります。

ネストされた辞書

これにより、区切り文字の問題は解決されますが、いくつかの新しい問題が発生します。

  • ネストされたレベルごとに、そのキーがすでに存在するかどうかを確認する必要があるため、新しい値の挿入は困難です。そうでない場合は、値として新しい辞書を作成する必要があります。これにより、辞書の使用がより困難になります。
  • さらにメモリとパフォーマンスのオーバーヘッドが発生します。

タプルベースの辞書

あなたが投稿したアプローチの中で、これはおそらく最高です。

ただし、さらに一歩進んで、キーに名前付きの不変を作成することもできstructます。これにより、キーの一部に便利な名前を付けることができるため、辞書が使いやすくなります。

于 2012-08-10T20:46:44.880 に答える
7

それとも、どのメソッドが最もクリーンで保守しやすいコードを提供するかに焦点を当てる必要がありますか?

悪夢のような威圧的なコードを書くことに焦点を当てていない限り、言うまでもなく悪である文字列の区切りと連結のアプローチを避けるべきです。

タプル ベースのアプローチとネストされた辞書ベースのアプローチのどちらを選択するかは、コンテキストによって異なります。パフォーマンスのために微調整しますか?それとも読みやすくするために微調整しますか?後者について先にお話しします。

保守性の観点から

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

    var myDict = new Dictionary<Tuple<char, int>, MyClass>();
    

    よりも

    var myDict = new Dictionary<char, Dictionary<int, MyClass>>();
    

    呼び出し側から。2 番目のケースでは、各追加、検索、削除などで、複数のディクショナリに対するアクションが必要になります。

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

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

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

  • ネストされたディクショナリの場合、追加、更新、検索、削除などのすべての基本アクションを 2 つのディクショナリで実行する必要があります。現在、ネストされたディクショナリ アプローチの方高速な場合があります。つまり、検索対象のデータが存在しない場合です。これは、中間ディクショナリが完全なハッシュ コードの計算と比較をバイパスできるためです。データが存在する場合は、ルックアップを 2 回 (ネストによっては 3 回) 実行する必要があるため、速度が低下するはずです。

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

全体として、ネストされた辞書アプローチの必要性はほとんどありません。オッズは、それを望まないでしょう。私はタプルベースのアプローチを好みますが、より適切な実装で独自のタプルを作成する必要があります。この場合charintキーの場合は、(不変の) 構造体にすることをお勧めします。

非常に関連する質問: Tuples(or arrays) as Dictionary keys in C#

于 2014-01-13T12:13:08.900 に答える
5

上記の回答に追加したかったのは、入れ子になったディクショナリがメモリ フットプリントの点で複合キー ディクショナリよりもはるかに優れているいくつかのシナリオ (データの分散方法によって異なります) があることです (これにより、パフォーマンスが向上する可能性があります)。全体)。その理由は、ネストにより、キーの重複した値を保存する必要がなくなるためです。これにより、大きな辞書では、余分な辞書のフットプリントが無視できるようになります。

たとえば、(男性/女性)、(赤ちゃん/若い/古い)、(年齢) の複合キーを持つ辞書が必要だとします。

複合キーの辞書でいくつかの値を保存しましょう。

(male, baby, 1)
(male, baby, 2)
(male, baby, 3)
(male, young, 21)
(male, young, 22)
(male, young, 23)
(male, old, 91)
(male, old, 92)
(male, old, 93)
(female, baby, 1)
(female, baby, 2)
(female, baby, 3)
(female, young, 21)
(female, young, 22)
(female, young, 23)
(female, old, 91)
(female, old, 92)
(female, old, 93)

次に、辞書の辞書に同じ値を保存しましょう。

male -> baby ->  1
                 2
                 3
        young -> 21
                 22
                 23
        old  ->  91
                 92
                 93
female -> baby ->1
                 2
                 3
        young -> 21
                 22
                 23
        old  ->  91
                 92
                 93

複合キー アプローチでは、辞書の辞書に 1 つのコピーを保存するのではなく、「男性」と「女性」のコピーを 9 回保存します。実際、26 個のアイテムに対して 54 個のアイテムを保存したので、メモリ使用量が 2 倍になりました。この例は、違いを視覚化するのにも役立ちます。最初のサンプルと比較して 2 番目のサンプルにどれだけの「空の」スペースがあるかを確認してください。これらはすべて保存する必要のない値です。

まだ納得していない人のために、ここにサンプルテストがあります:

    Dictionary<Tuple<int, int, int>, int> map1 = new Dictionary<Tuple<int, int, int>, int>();
    Dictionary<int, Dictionary<int, Dictionary<int, int>>> map2 = new Dictionary<int, Dictionary<int, Dictionary<int, int>>>();

    public void SizeTest()
    {
        for (int x = 0; x < 30; x++)
        {
            for (int y = 0; y < 100; y++)
            {
                for (int z = 0; z < 600; z++)
                {
                    addToMap1(x, y, z, 0);
                    addToMap2(x, y, z, 0);
                }
            }
        }
        int size1 = GetObjectSize(map1);
        int size2 = GetObjectSize(map2);

        Console.WriteLine(size1);
        Console.WriteLine(size2);
    }

    private void addToMap1(int x, int y, int z, int value)
    {
        map1.Add(new Tuple<int, int, int>(x, y, z), value);
    }

    private void addToMap2(int x, int y, int z, int value)
    {
        map2.GetOrAdd(x, _ => new Dictionary<int, Dictionary<int, int>>())
            .GetOrAdd(y, _ => new Dictionary<int, int>())
            .GetOrAdd(z, _ => value);
    }

    private int GetObjectSize(object TestObject)
    {
        BinaryFormatter bf = new BinaryFormatter();
        MemoryStream ms = new MemoryStream();
        byte[] Array;
        bf.Serialize(ms, TestObject);
        Array = ms.ToArray();
        return Array.Length;
    }

    public static TResult GetOrAdd<TKey, TResult>(this Dictionary<TKey, TResult> map, TKey key, Func<TKey, TResult> addIfMissing)
    {
        TResult result;
        if (!map.TryGetValue(key, out result))
        {
            result = addIfMissing(key);
            map[key] = result;
        }
        return result;
    }

このテストは、辞書の辞書を支持して ~30MB に対して ~70MB を返します。

于 2015-07-21T19:32:15.030 に答える
2

説明したすべてのオプションはかなり似ています。パフォーマンスに関しては、特定の使用シナリオについてそれぞれをテストする必要がありますが、小さなコレクションの場合、大きな違いはありません。

それらはすべて読みやすさにも悩まされています-それらを構築し、タイプから意味を引き出すことは困難です。

代わりに、データを直接記述する型を作成することをお勧めします。適切な命名は大いに役立ちます。

于 2012-08-10T20:47:45.850 に答える