PG日誌

読者です 読者をやめる 読者になる 読者になる

PG日誌

主にc#の事を書いています

c# の Dictionary<TKey, TValue> の 使い方


f:id:Takachan:20170115150341j:plain

c#連想配列型のデータ構造を利用する場合 Dictionary を使います。割と使用頻度が高いですが安易な利用は後で痛い目に合います。そこで基本的な使い方と痛い目に合わないプラクティスをちょっと紹介したいと思います。

1. 宣言編/使用編

c# はこの手のサンプルが少し乏しいみたいなので備忘録的に使い方を紹介したいと思います。

// Dictionaryを使うために必要な名前空間の宣言
using System.Collections.Generic;

// ◆宣言
// 宣言が長くなるので左辺をvarで受けます
//  → Tkey, TValue に使いたい型を宣言します。
var hogeTable = new Dictionary<TKey, TValue>();

// ◆値の代入

// 既に値があっても上書き(例外発生しない)
hogeTable["aaa"] = 10;
// 値があると ArgumentException が発生
hogeTable.Add("aaa", 10);

// ◆値の取り出し
// 値が Dictionaryに無いとKeyNotFoundExceptionが発生
int value = hogeTable["aaa"];

// ◆存在確認
// trueが取れたら存在する
bool contains = hogeTable.ContainsKey("aaa");

// ◆内容を全部削除
hogeTable.Clear();

2. 命名編

カタカナで書くとディクショナリーですがこの名前に引きずられないようにしましょう。

// 悪い例 (1)
IDictionary<TKey, TValue> hogehogeDic = new Dictionary<TKey, TValue>();

// 悪い例 (2)
IDictionary<TKey, TValue> hogehogeDictionary = new Dictionary<TKey, TValue>();

引きずられてディクショナリを名前に含めないほうがいいです。
伝統的にこの手のデータ構造は テーブル(Table) とか マップ(Map) とか呼んでいるようなのでそれに従います。

// 良い例
IDictionary<TKey, TValue> hogehogeTable = new Dictionary<TKey, TValue>();

3. コメント編

ジェネリックの型はクラスを切らないで任意の値をペアにできるので便利なんですが…
3年後に見た時に TKey, TValue の意味を覚えてる可能性は低いのでコメントを書きましょう

// APIコメントが使える場所での書き方

/// <summary>
/// hogehogeの対応関係を表すテーブル
/// TKey : キーに設定される型の意味や説明を記述する
/// TValue : 値に設定される型の意味や説明を記述する
/// </summary>
var hogehogeTable = new Dictionary<TKey, TValue>();

// ローカル変数でもコメントは書きましょう

// hogehogeの対応関係を表すテーブル
// TKey : キーに設定される型の意味や説明を記述する
// TValue : 値に設定される型の意味や説明を記述する
var hogehogeTable = new Dictionary<TKey, TValue>();

4. 引数編

拡張性を保証するために、引数ので Dictionary を受け取るときはインターフェースで受けましょう
以下の悪い例だと、Dictionary の型が SortedDictionary に変わっても変更する必要がありません。

// 良い例
public void Foo(IDictionary<TKey, TValue> hogehogeTable) { ... }

// 悪い例
public void Foo(Dictionary<TKey, TValue> hogehogeTable) { ... }

5. 列挙編

Dictionary を foreach 構文で回す時は 3種類の方法があります。

IDictionary<int , string> theTable = new Dictionary<int, string>() {
    { 0, "None" },
    { 1, "One" },
    { 2, "Tow" },
};

// Key と Value を両方受ける方式
foreach(KeyValuePair<TKey, TValue> item in theTable) {
    Console.WriteLine(item,Key, Item.Value); // 0None, 1One, 2Tow と表示される
}

// キー値のみ受け取る方式
foreach(int item in theTable.Keys) {
    Console.WriteLine(item); // 0, 1, 2 と表示される
}

// 値のみ受け取る方式
foreach(string item in theTable.Values) {
    Console.WriteLine(item); // None, One, Tow と表示される
}

5. 暗のテクニック編

多重辞書構造

Dictionary をネストするのだけは避けましょう。短絡的に動くかもしれませんが全ての記憶が失われた後で苦労するのは自分です。

// 2重辞書(しかも宣言が具体的)
Dictionary<TKey, Dictionary<TKey, TValue>> hogehogeTable = 
    new Dictionary<TKey, Dictionary<TKey, Dictionary>>();

// 3重辞書(しかも宣言が具体的)
Dictionary<TKey, Dictionary<TKey, Dictionary<TKey, TValue>>> hogehogeTable = 
    new Dictionary<TKey, Dictionary<TKey, Dictionary<TKey, TValue>>>();

自分は4重辞書まで見たことありますが、最早なんだか全く分からないですね…
これなら自分でデータ構造をクラスで宣言した方がいいですね。

公開プロパティの宣言間違い

以下例に限らずよくあるのですがこうやって宣言されて想定外のオブジェクトが設定されて処理が崩壊することが有ります。

// 外から設定できる Dictionaryオブジェクト
public class Sample {
    public IDictionary<Tkey, TValue> HogeHogeTable { get; set; } // 外から Set できる!!!
    public Sample() { this.HogeHogeTable = new Dictionary<TKey, TValue>();
}

// で、こういう風に設定されて後でバグります。

var s = new Sample();
s.HogeHogeTable = new SortedDictionary<TKey, TValue>(); // この操作実は想定されていない!!

IDictionary や IList の set は private set にするか、削除しましょう。

6. まとめ

できれば Dictionary を生で使わない方がいいです。
特に、公開メソッドの引数で受けるのは避けたましょう。

Dictionary の制約(TValue は null を許容する or しないとか)とか意味(何かに関連するものでないと実は設定してはいけないとか…)は、文章化されない場合がほとんどないので往々にして乱用すると後で痛い目にあいます。リスト構造より複雑なので痛い目の度合いが高いのがポイントです。

なので、データ構造は複雑な場合クラスを切って管理した方が後々幸せです。
自分で Dictionary から派生クラスを切る場合でも、名前を付けるだけが目的の場合、派生クラスでなく自作のクラスを作成する方がいいです。Dictionaryのつもりで使ったのに何かの値を設定する時だけ例外が出る(ドキュメント無し)だと利用者が混乱します。

なので、

  • 使うならスコープ内限定で短く効果的に
  • 外部公開をむやみにしない
  • ラップして制約を明示したオブジェクトの内部利用にとどめる
  • コメントでもいいのでちゃんと文書化する

だと思います。