§1 Dictionary

System.Collections.Generic.Dictionaryクラスはインデックスではなくキーによってコレクション内の要素にアクセスできるコレクションで、ハッシュテーブル連想配列と呼ばれるものに相当します。

コレクション内の要素数を動的に増減できる点はListと同じですが、Dictionaryは常にキーとそれに対応する値の対(ペア)を格納します。 格納した値にアクセスする場合は、インデックスの代わりにキーを指定します。 このコレクションではキー(名前)から目的の値を引くことができるため、Dictionary(辞書)という名前が付いています。 Dictionaryでは、任意の型(厳密にはGetHashCodeメソッドが一意なハッシュ値を返すオブジェクト)をキーとして指定することができます。

DictionaryクラスはSystem.Collections.Hashtableに相当するジェネリックコレクションですが、ただ単にジェネリックなHashtableというわけではなく、Hashtableと比べるとより高度な操作が可能になっています。



§2 基本操作

早速、Dictionaryクラスを使った基本的な操作について見ていきます。

§2.1 Dictionaryの作成・要素の取得と設定

Dictionaryを使う場合には、まずキーの型と値の型を決める必要があります。 Dictionaryを作成する際には、キー・値の順で型パラメータを指定します。 例えば、キーの型がstring/String、値の型がint/IntegerのDictionaryであれば、

キーがstring、値がintのDictionary
Dictionary<string, int> dict;

となります。

Dictionaryを使う場合には、最初に格納する要素を指定する(§.コレクション初期化子)ことも、空のDictionaryを作成しておいて後から要素を追加することも出来ます。 以下の例では空のDictionaryを作成して使うことにします。

Dictionaryに格納されている値を取得(参照)するには、インデックスを指定するかわりに配列と同様の記法でキーを指定します。 そうすることにより、Dictionaryに格納されているペアの中からキーに対応する値を取得することが出来ます。 また、取得だけでなく、キーに対応する特定の要素を設定(値の上書き)をすることも出来ます。

C#ではインデクサによって、VB.NETでは既定のプロパティItemによって、配列と同様にインデックスによる取得・設定ができるようになっています。 インデクサについてはプロパティ §.インデクサを参照してください。

なお、現在Dictionary内に含まれている要素の数を取得するにはLengthプロパティではなくCountプロパティを参照します。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    // キーの型にstring、値の型にintを使用するDictionaryを作成
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16; // キー"foo"に対応する値として16を設定
    dict["bar"] = 72; // キー"bar"に対応する値として72を設定
    dict["baz"] = 42; // キー"baz"に対応する値として42を設定

    // 格納されている要素(ペア)の数を取得
    Console.WriteLine("Count = {0}", dict.Count);

    // キー"foo"に対応する値を取得
    Console.WriteLine("foo = {0}", dict["foo"]);

    // キー"bar"に対応する値を取得
    Console.WriteLine("bar = {0}", dict["bar"]);
  }
}
実行結果
Count = 3
foo = 16
bar = 72

上記サンプルで作成されるDictionaryの内容を表にすると次のようになります。

dictの内容
キー
foo 16
bar 72
baz 42

Dictionaryに対して存在しないキーを指定した場合は例外KeyNotFoundExceptionがスローされます。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;

    // 存在するキーに対応する値を取得
    Console.WriteLine("foo = {0}", dict["foo"]);
    Console.WriteLine("bar = {0}", dict["bar"]);

    // 存在しないキー"hoge"に対応する値を取得
    Console.WriteLine("hoge = {0}", dict["hoge"]);
  }
}
実行結果
foo = 16
bar = 72

ハンドルされていない例外: System.Collections.Generic.KeyNotFoundException: 指定されたキーはディレクトリ内に存在しませんでした。
   場所 System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   場所 Sample.Main()

この動作は、存在しないキーに対してnull/Nothingが返されるHashtableとは異なるので注意してください。 Dictionaryでは、インデクサで取得した値からキーが存在するかどうかを確認することはできません。 事前にキーが存在するかどうかを確かめるにはContainsKeyメソッドを使うことが出来ます。

§2.2 要素の追加

既に例に挙げた通りインデクサでキーを指定することでDictionaryに要素を設定(追加)することができますが、これとは別にAddメソッドを使うことでもDictionaryに要素を追加することが出来ます。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    // キーの型にstring、値の型にintを使用するDictionaryを作成
    Dictionary<string, int> dict = new Dictionary<string, int>();

    // Addメソッドを使って要素を追加
    dict.Add("foo", 16);
    dict.Add("bar", 72);
    dict.Add("baz", 42);

    // キーに対応する値を取得
    Console.WriteLine("foo = {0}", dict["foo"]);
    Console.WriteLine("bar = {0}", dict["bar"]);
    Console.WriteLine("baz = {0}", dict["baz"]);
  }
}
実行結果
foo = 16
bar = 72
baz = 42

一見するとインデクサを使う場合とAddメソッドを使う場合ではどちらも変わらないように見えますが、既にキーが存在する場合の動作が異なります。 インデクサでは既にキーが存在している場合は値が上書きされるのに対し、Addメソッドでは既にキーが存在している場合は例外ArgumentExceptionがスローされます

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;

    Console.WriteLine("foo = {0}", dict["foo"]);

    // 既に存在するキーを指定して値を上書き
    dict["foo"] = 72;

    Console.WriteLine("foo = {0}", dict["foo"]);

    // 既に存在するキーを指定して値を追加
    dict.Add("foo", 42);

    Console.WriteLine("foo = {0}", dict["foo"]);
  }
}
実行結果
foo = 16
foo = 72

ハンドルされていない例外: System.ArgumentException: 同一のキーを含む項目が既に追加されています。
   場所 System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   場所 Sample.Main()

このように、インデクサでは指定されたキーが存在しているかどうかに関わらず値が上書きされて設定されるのに対し、Addメソッドではキーが既に存在しているかを検査した上で値の追加が行われます。 事前にキーが存在するかどうかを確かめるにはContainsKeyメソッドを使います。

§2.3 コレクション初期化子

Dictionaryを使用する場合、インスタンスの作成後に要素の追加を行う場合が多々あります。 コレクション初期化子を使うと、インスタンスの作成と要素の追加を同時に行うことができます。 コレクション初期化子はC# 3.0(Visual C# 2008)以降、VB 10(Visual Basic 2010)以降でサポートされます。

コレクション初期化子を使ったインスタンスの初期化
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    // コレクション初期化子を使ってインスタンスの作成と要素の追加を行う
    Dictionary<string, int> dict = new Dictionary<string, int>() {
      {"foo", 16},
      {"bar", 72},
      {"baz", 42},
    };

    Console.WriteLine("foo = {0}", dict["foo"]);
    Console.WriteLine("Count = {0}", dict.Count);
  }
}
実行結果
foo = 16
Count = 3

コレクション初期化子では、{ {キー1, 値1}, {キー2, 値2}, ... }のように追加する要素をペア毎に中括弧で括って記述します。 RubyやPerlのハッシュで使われる{キー1 => 値1, キー2 => 値2, ...}といった形式はコレクション初期化子ではサポートされません。

§2.4 要素の削除

Dictionaryから要素を削除する場合は、Removeメソッドを使います。 このメソッドでは、指定されたキーを持つ要素をDictionaryから削除し、戻り値として実際に削除に成功したかどうかがbool/Boolean型で返されます。 Addメソッドとは異なり、Removeメソッドに指定したキーを持つ要素が存在しない場合でも例外はスローされません(この場合、戻り値としてfalseが返されます)。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;

    Console.WriteLine("Count = {0}", dict.Count);

    // 存在するキー"bar"の要素を削除
    bool ret;

    ret = dict.Remove("bar");

    Console.WriteLine("キー'bar'の要素を削除" + (ret ? "しました" : "できませんでした"));

    Console.WriteLine("Count = {0}", dict.Count);

    // 存在しないキー"baz"の要素を削除
    ret = dict.Remove("baz");

    Console.WriteLine("キー'baz'の要素を削除" + (ret ? "しました" : "できませんでした"));

    Console.WriteLine("Count = {0}", dict.Count);
  }
}
実行結果
Count = 2
キー'bar'の要素を削除しました
Count = 1
キー'baz'の要素を削除できませんでした
Count = 1

Dictionary内のすべての要素を削除するにはClearメソッドを使うことが出来ます。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;

    Console.WriteLine("Count = {0}", dict.Count);

    // Dictionary内のすべての要素を削除
    dict.Clear();

    Console.WriteLine("Count = {0}", dict.Count);
  }
}
実行結果
Count = 3
Count = 0

なお、インデクサを使って値にnull/Nothingを設定しても要素自体を削除することはできません。 この場合、指定されたキーに対応する値としてnull/Nothingが上書き設定されるだけで、要素自体は削除されません。 特にVBでは、Dictionaryの値の型が値型の場合、値としてNothingを設定すると、デフォルト値が設定される点に注意が必要です(型の種類・サイズ・精度・値域 §.型のデフォルト値値型と参照型)。

値にNothingを設定した場合の動作
Imports System
Imports System.Collections.Generic

Class Sample
  Shared Sub Main()
    Dim dict As New Dictionary(Of String, Integer)()

    dict("foo") = 16
    dict("bar") = 72

    Console.WriteLine("Count = {0}", dict.Count)

    ' キー"bar"の要素にNothingを設定
    dict("bar") = Nothing

    ' キー"bar"に設定されている値と要素数を表示
    Console.WriteLine("bar = {0}", dict("bar"))
    Console.WriteLine("Count = {0}", dict.Count)
  End Sub
End Class
実行結果
Count = 2
bar = 0
Count = 2

§2.5 値に対する変更

値に構造体を格納するようにしたDictionaryで構造体のプロパティやフィールドだけを変更しようとする場合は注意が必要です。 例えば、次のようなコードはコンパイルエラーとなります。

Dictionaryに格納されている構造体のフィールドを直接変更しようとした場合
using System;
using System.Collections.Generic;

// Dictionaryに格納する構造体
struct S {
  public int F;

  public S(int f)
  {
    F = f;
  }
}

class Sample {
  static void Main()
  {
    Dictionary<string, S> dict = new Dictionary<string, S>();

    // フィールドFに値3を設定した構造体を格納
    dict["foo"] = new S(3);

    // キー"foo"の構造体フィールドの値を変更したいが、コンパイルエラーとなる
    dict["foo"].F = 16;
    // error CS1612: 変数ではないため、'System.Collections.Generic.Dictionary<string,S>.this[string]'の戻り値を変更できません。
  }
}

このような操作を行いたい場合は、いったんDictionaryから一時変数に構造体をコピーし、値を変更してから再度Dictionaryに格納するようにします。

Dictionaryに格納されている構造体のフィールドを変更する方法
using System;
using System.Collections.Generic;

// Dictionaryに格納する構造体
struct S {
  public int F;

  public S(int f)
  {
    F = f;
  }
}

class Sample {
  static void Main()
  {
    Dictionary<string, S> dict = new Dictionary<string, S>();

    // フィールドFに値3を設定した構造体を格納
    dict["foo"] = new S(3);

    // Dictionaryに格納した構造体フィールドの値を参照
    Console.WriteLine("dict[\"foo\"].F = {0}", dict["foo"].F);

    // フィールドの値を変更したい構造体を一時変数にコピーする
    S tmp = dict["foo"];

    // 一時変数に代入した構造体のフィールドの値を変更する
    tmp.F = 16;

    // 変更した構造体を再度Dictionaryに格納する
    dict["foo"] = tmp;

    // Dictionaryに格納した構造体フィールドの値を参照
    Console.WriteLine("dict[\"foo\"].F = {0}", dict["foo"].F);
  }
}
実行結果
dict("foo").F = 3
dict("foo").F = 16

構造体ではなくクラスのインスタンスを格納する場合はこのような操作を行う必要はありません。 この違いは型が値型か参照型かによって決まります。 詳しくは値型と参照型 §.値型のプロパティ・インデクサを参照してください。

§2.6 要素の列挙

§2.6.1 DictionaryとKeyValuePair

列挙について解説する前に、Dictionaryの内部構造とKeyValuePair構造体について触れておきます。 Dictionaryでは、要素を格納する際、キーと値の組み合わせをKeyValuePairとして格納します。 KeyValuePairはその名の通りキーとそれに対応する値のペアを表す構造体で、キーを格納するプロパティKeyと値を格納するプロパティValueの二つのメンバを持ちます。 Dictionaryの内部では、キーと値の組み合わせはKeyValuePairで管理されます。

Dictionaryに格納されるデータ
キー
"foo" 16
"bar" 72
"baz" 42
:
:
:
:
... ...
Dictionaryの内部構造のイメージ
KeyValuePair[0] Key = "foo"Value = 16
KeyValuePair[1] Key = "bar"Value = 72
KeyValuePair[2] Key = "baz"Value = 42
:
:
KeyValuePair[n] Key = ...Value = ...

なお、Dictionaryは順序を持たないコレクションであるため、実際には上図のように各KeyValuePairに対してインデックスが振られる訳ではありません。

また、DictionaryはインターフェイスICollection<KeyValuePair<TKey, TValue>>を実装しています。 つまりDictionaryはKeyValuePairのコレクションと見ることが出来ます。

§2.6.2 要素の列挙

List等と同様にDictionaryもforeach文によって要素を列挙することができますが、Dictionaryではキー・値のペアはKeyValuePair構造体として格納されているため、列挙する場合もKeyValuePair構造体の形で列挙されます。 列挙されるKeyValuePairのKeyプロパティを参照することでキーを、Valueプロパティを参照することで値を取得することが出来ます。

Dictionaryに格納されている要素の列挙
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;

    // Dictionaryに格納されているキーと値のペアを列挙
    foreach (KeyValuePair<string, int> pair in dict) {
      Console.WriteLine("{0} => {1}", pair.Key, pair.Value);
    }
  }
}
実行結果
foo => 16
bar => 72
baz => 42

単にDictionaryを列挙した場合はキーと値のペアで取得されることになりますが、キーだけ・値だけをそれぞれ個別に列挙することも出来ます。 Keysプロパティを参照することでDictionaryに格納されているすべてのキーを、Valuesプロパティを参照することですべての値をそれぞれ取得・列挙することが出来ます。

キーのみ・値のみの列挙
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;

    // Dictionaryに格納されているすべてのキーを列挙
    Console.WriteLine("[Keys]");

    foreach (string key in dict.Keys) {
      Console.WriteLine(key);
    }

    // Dictionaryに格納されているすべての値を列挙
    Console.WriteLine("[Values]");

    foreach (int val in dict.Values) {
      Console.WriteLine(val);
    }
  }
}
実行結果
[Keys]
foo
bar
baz
[Values]
16
72
42

§2.6.3 要素の列挙と順序

Dictionaryは順序を持たないコレクションであるため、List等とは異なり要素を列挙する場合は必ずしも追加した順で列挙されるとは限らない点に注意が必要です。 このことは、ドキュメント上では以下のように記述されています。

列挙処理のために、ディクショナリ内の各アイテムは、値とそのキーを表す KeyValuePair(Of TKey, TValue) 構造体として処理されます。 アイテムが返される順序は未定義です。
For purposes of enumeration, each item in the dictionary is treated as a KeyValuePair(Of TKey, TValue) structure representing a value and its key. The order in which the items are returned is undefined.

Dictionary(Of TKey, TValue) クラス

そのため、foreach文でDictionaryの列挙を行う場合は、列挙される順序に依存した処理を記述しないようにする必要があります。

列挙される順序が意味を持つ場合は、Dictionaryの代わりにSortedListクラス・SortedDictionaryクラスを使うことができます。 SortedList・SortedDictionaryでは、常に一定の順序で列挙されることが保証されます。 また、非ジェネリックコレクションであるOrderedDictionaryクラスでは、要素を追加した順序が維持され、列挙の際も追加された順となることが保証されます。

このほか、LINQの拡張メソッドOrderByを使うことにより、順序を定義して要素の列挙を行うこともできます。

OrderByメソッドを使って列挙順を定義する例
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;

    // 値の小さい順(昇順)でDictionaryに格納されている要素を列挙
    foreach (KeyValuePair<string, int> pair in dict.OrderBy(p => p.Value)) {
      Console.WriteLine("{0} => {1}", pair.Key, pair.Value);
    }
  }
}
実行結果
foo => 16
baz => 42
bar => 72

OrderByメソッドについて詳しくは基本型のソートと昇順・降順でのソート §.Enumerable.OrderByで解説しています。

OrderedDictionaryクラスについてはOrderedDictionaryクラスを、またこれに関連してジェネリック版OrderedDictionaryを参照してください。

§2.6.4 列挙中の要素の追加・削除

Dictionaryをforeach文で列挙している最中に要素の追加や削除などの変更を行おうとすると、例外InvalidOperationExceptionがスローされます。

列挙中の要素の削除
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;
    dict["hoge"] = 3;
    dict["huga"] = 42;

    foreach (KeyValuePair<string, int> pair in dict) {
      // 値が42の場合は、削除する
      if (pair.Value == 42) dict.Remove(pair.Key);
    }
  }
}
実行結果
ハンドルされていない例外: System.InvalidOperationException: コレクションが変更されました。列挙操作は実行されない可能性があります。
   場所 System.Collections.Generic.Dictionary`2.Enumerator.MoveNext()
   場所 Sample.Main()

例外が発生する理由や回避する方法については列挙操作中のコレクションの変更でも解説していますが、Dictionaryクラスではインデックスを指定した列挙操作は行えないため、ここで紹介している方法は適用できません。

先の例のように条件に合う要素を検索して削除するといった場合は、列挙による要素の検索と削除を別々のforeach文で行うようにすることでInvalidOperationExceptionの発生を回避することが出来ます。

要素の検索と削除を別々に行う例
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;
    dict["hoge"] = 3;
    dict["huga"] = 42;

    // 削除対象のキーを格納するList
    List<string> removeKeys = new List<string>();

    foreach (KeyValuePair<string, int> pair in dict) {
      // 値が42の場合は、削除対象としてListにキーを追加
      if (pair.Value == 42) removeKeys.Add(pair.Key);
    }

    // 対象となるキーを持つ要素を削除
    foreach (string key in removeKeys) {
      dict.Remove(key);
    }

    // 削除後のDictionaryの内容を列挙
    foreach (KeyValuePair<string, int> pair in dict) {
      Console.WriteLine("{0} => {1}", pair.Key, pair.Value);
    }
  }
}
実行結果
foo => 16
bar => 72
hoge => 3

また別の方法として、次の例のように別のDictionaryを用意し、元のDictionaryから条件に合う要素だけを別のDictionaryコピーすることで要素を削除した場合と同等のDictionaryを得るという方法もあります。

特定の要素だけを除外して新しいDictionaryを作成する例
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;
    dict["hoge"] = 3;
    dict["huga"] = 42;

    Dictionary<string, int> newdict = new Dictionary<string, int>();

    // 値が42の要素を除いて新しいDictionaryに内容をコピーする
    foreach (KeyValuePair<string, int> pair in dict) {
      // 値が42以外の場合のみ、新しいDictionaryに格納する
      if (pair.Value != 42) newdict[pair.Key] = pair.Value;
    }

    // 新しいDictionaryの内容を列挙
    foreach (KeyValuePair<string, int> pair in newdict) {
      Console.WriteLine("{0} => {1}", pair.Key, pair.Value);
    }
  }
}
実行結果
foo => 16
bar => 72
hoge => 3

特定要素を除外した列挙が行えればよいのであれば、LINQの拡張メソッドWhereを使って次のようにすることもできます。

Whereメソッドを使用して特定の要素だけを除外して要素を列挙する例
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;
    dict["hoge"] = 3;
    dict["huga"] = 42;

    // 値が42以外の要素のみを列挙
    foreach (KeyValuePair<string, int> pair in dict.Where(p => p.Value != 42)) {
      Console.WriteLine("{0} => {1}", pair.Key, pair.Value);
    }
  }
}
実行結果
foo => 16
bar => 72
hoge => 3

§2.7 要素の並べ替え (ソート)

Dictionaryは順序を持たないことから、Dictionaryをソートされた状態にすることはできません。 従ってSort()のようなメソッドも用意されていません。 Dictionary内の各ペアをソートされた状態にしたい場合は、次の例のように一旦Listに変換し、List.Sortメソッドを使って並べ替えを行う、といった方法をとる必要があります。

DictionaryをListに変換してソートする例
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;

    // DictionaryをList<KeyValuePair>に変換
    List<KeyValuePair<string, int>> list = new List<KeyValuePair<string, int>>(dict);

    // List.Sortメソッドを使い、キーによってペアをソートする
    Console.WriteLine("[sort by key]");

    list.Sort((x, y) => string.Compare(x.Key, y.Key));

    foreach (KeyValuePair<string, int> pair in list) {
      Console.WriteLine("{0} => {1}", pair.Key, pair.Value);
    }

    // List.Sortメソッドを使い、値によってペアをソートする
    Console.WriteLine("[sort by value]");

    list.Sort((x, y) => x.Value - y.Value);

    foreach (KeyValuePair<string, int> pair in list) {
      Console.WriteLine("{0} => {1}", pair.Key, pair.Value);
    }
  }
}
実行結果
[sort by key]
bar => 72
baz => 42
foo => 16
[sort by value]
foo => 16
baz => 42
bar => 72

このほか、Dictionaryをソートする方法については基本型のソートと昇順・降順でのソート §.Dictionaryのソートで詳しく解説しています。

要素を常に並べ替えた状態にしておきたい場合や、格納される要素の順序が重要になる場合は、Dictionaryの代わりにSortedListやSortedDictionaryを使うことも出来ます。

§2.8 要素の検索

§2.8.1 キー・値の有無チェック (ContainsKey, ContainsValue)

Dictionaryに指定したキーが存在するかどうかを調べるにはContainsKeyメソッド、値が存在するかどうかを調べるにはContainsValueメソッドを使います。

要素の検索
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;

    // キー"foo"を持つ要素があるかどうか
    Console.WriteLine("ContainsKey(foo) = {0}", dict.ContainsKey("foo"));

    // キー"hoge"を持つ要素があるかどうか
    Console.WriteLine("ContainsKey(hoge) = {0}", dict.ContainsKey("hoge"));

    // 値16を持つ要素があるかどうか
    Console.WriteLine("ContainsValue(16) = {0}", dict.ContainsValue(16));

    // 値9を持つ要素があるかどうか
    Console.WriteLine("ContainsValue(9) = {0}", dict.ContainsValue(9));
  }
}
実行結果
ContainsKey(foo) = True
ContainsKey(hoge) = False
ContainsValue(16) = True
ContainsValue(9) = False

§2.8.2 キーの有無チェックと値の取得 (TryGetValue)

Dictionaryでは、キーが存在しない場合は例外KeyNotFoundExceptionがスローされるため、事前にキーがあるかどうか調べるなどする必要があります。 しかし、TryGetValueメソッドを使うとキーの有無チェックと値の取得を同時に行えるため、キーの有無チェックやKeyNotFoundExceptionのキャッチをする必要が無くなります。

このメソッドは、ContainsKeyメソッドと値の取得を組み合わせた動作をするメソッドと言えます。 キーの有無はTryGetValueメソッドの戻り値で取得でき、該当するキーが存在する場合はキーに対応する値がoutパラメータに格納されます。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;

    // Dictionaryからキー"foo"に対応する値を取得
    int val;

    if (dict.TryGetValue("foo", out val)) {
      // 取得できたら表示
      Console.WriteLine("foo => {0}", val);
    }

    // Dictionaryからキー"hoge"に対応する値を取得
    if (dict.TryGetValue("hoge", out val)) {
      // 取得できたら表示
      Console.WriteLine("hoge => {0}", val);
    }
    else {
      // 存在するキーが無い場合
      Console.WriteLine("key 'hoge' not found");
    }
  }
}
実行結果
foo => 16
key 'hoge' not found

§2.8.3 値からキーの逆引き検索

Dictionaryには値から対応するキーを逆引きするメソッドは用意されていません。 そのため、次の例のようにDictionaryの全要素を列挙して該当するKeyValuePairを検索するよう独自に実装する必要があります。

値から対応するキーを逆引きする例
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;

    // 値に42を持つ要素を検索する
    int val = 42;
    bool found = false;
    KeyValuePair<string, int> match = new KeyValuePair<string, int>();

    foreach (KeyValuePair<string, int> pair in dict) {
      if (pair.Value == val) {
        // 該当する値を持つ要素が見つかった場合
        found = true;
        match = pair;
        break;
      }
    }

    if (found) {
      Console.WriteLine("値{0}を持つ要素のキーは'{1}'です", match.Value, match.Key);
    }
    else {
      Console.WriteLine("値{0}を持つ要素は見つかりませんでした", val);
    }
  }
}
実行結果
値42を持つ要素のキーは'baz'です

LINQの拡張メソッドFirstを使って逆引きする場合は次のように記述できます。

Firstメソッドを使って値から対応するキーを逆引きする例
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;

    // 値に42を持つ要素を検索する
    int val = 42;

    try {
      KeyValuePair<string, int> match = dict.First(pair => pair.Value == val);

      Console.WriteLine("値{0}を持つ要素のキーは'{1}'です", match.Value, match.Key);
    }
    catch (InvalidOperationException) {
      Console.WriteLine("値{0}を持つ要素は見つかりませんでした", val);
    }
  }
}
実行結果
値42を持つ要素のキーは'baz'です

§2.9 最初の要素・最後の要素

§.要素の列挙と順序でも述べたようにDictionaryに格納される要素には順序の概念が無いため、最初の要素・最後の要素は定義することができず、そういった要素を取得するプロパティ等も提供されません。

LINQの拡張メソッドOrderByで並べ替えを行い、その中から最初の要素・最後の要素を取得したい場合には同じく拡張メソッドのFirstメソッドLastメソッドを使うことができます。

Firstメソッド・Lastメソッドを使って最初・最後に列挙される要素を取得する例
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;

    // キーで並べ替えた場合に最初に位置する要素を取得する
    KeyValuePair<string, int> first = dict.OrderBy(pair => pair.Key).First();

    Console.WriteLine("first of order by key: {0} => {1}", first.Key, first.Value);

    // キーで並べ替えた状態で最後に列挙される要素を取得する
    KeyValuePair<string, int> last = dict.OrderBy(pair => pair.Key).Last();

    Console.WriteLine("last of order by key: {0} => {1}", last.Key, last.Value);

    // 値で並べ替えた場合に最初に位置する要素を取得する
    first = dict.OrderBy(pair => pair.Value).First();

    Console.WriteLine("first of order by value: {0} => {1}", first.Key, first.Value);

    // 値で並べ替えた場合に最後に位置する要素を取得する
    last = dict.OrderBy(pair => pair.Value).Last();

    Console.WriteLine("last of order by value: {0} => {1}", last.Key, last.Value);
  }
}
実行結果
first of order by key: bar => 72
last of order by key: foo => 16
first of order by value: foo => 16
last of order by value: bar => 72

§3 Dictionaryとキー

§3.1 キー比較のカスタマイズ(大文字小文字の違いの無視)

Hashtableと同様、コンストラクタで適切なIEqualityComparer<T>インターフェイスを指定することで、Dictionaryのキー比較時の動作をカスタマイズ出来ます。 例えば、文字列をキーとした場合に大文字小文字を無視するようにするといったことが出来ます。 以下は、StringComparerクラスを使って、大文字小文字を無視するDictionaryを作成する例です。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    // 大文字小文字の違いを無視しないDictionaryを作成
    // (キーの比較にStringComparer.CurrentCultureを使用)
    Dictionary<string, int> caseSensitiveDictionary = new Dictionary<string, int>(StringComparer.CurrentCulture);

    caseSensitiveDictionary["foo"] = 1;
    caseSensitiveDictionary["bar"] = 2;
    caseSensitiveDictionary["FOO"] = 3;
    caseSensitiveDictionary["BAR"] = 4;

    // 大文字小文字の違いを無視するDictionaryを作成
    // (キーの比較にStringComparer.CurrentCultureIgnoreCaseを使用)
    Dictionary<string, int> caseInsensitiveDictionary = new Dictionary<string, int>(StringComparer.CurrentCultureIgnoreCase);

    caseInsensitiveDictionary["foo"] = 1;
    caseInsensitiveDictionary["bar"] = 2;
    caseInsensitiveDictionary["FOO"] = 3;
    caseInsensitiveDictionary["BAR"] = 4;

    Console.WriteLine("caseSensitiveDictionary");

    foreach (KeyValuePair<string, int> entry in caseSensitiveDictionary) {
      Console.WriteLine("{0} => {1}", entry.Key, entry.Value);
    }

    Console.WriteLine("caseInsensitiveDictionary");

    foreach (KeyValuePair<string, int> entry in caseInsensitiveDictionary) {
      Console.WriteLine("{0} => {1}", entry.Key, entry.Value);
    }
  }
}
実行結果
caseSensitiveDictionary
foo => 1
bar => 2
FOO => 3
BAR => 4
caseInsensitiveDictionary
foo => 3
bar => 4

§3.2 キーと複合型

Dictionaryクラスでは、キーとなる型がEqualsメソッドGetHashCodeメソッドをオーバーライドしていて、かつ適切な値を返すように実装されていないと正しく動作しません。 キーに独自に定義した型を指定してDictionaryを使用したい場合は、これらのメソッドをオーバーライドして適切に実装するか、DictionaryのコンストラクタにIEqualityComparer<T>インターフェイスを指定する必要があります。

IEqualityComparer<T>インターフェイスの実装方法や、独自に定義した型をキーにする例については等価性の定義と比較で解説しています。

§4 Dictionaryとインデックス

Dictionaryでは格納される要素はキーによってのみ管理されます。 また順序を持たないコレクションであるためインデックスという概念を持たず、インデックスを指定したキー・値の参照・設定などの操作も提供されません。 これに従って、for文による列挙も行えません。 当然、IndexOfKey・IndexOfValueといったキー・値のインデックスを取得するような操作も提供されません。

§4.1 インデックスを指定した操作

インデックスを使った操作を行いたい場合は、次の例のように一旦DictionaryからListに変換する、といった方法を取る必要があります。

DictionaryからListに変換して列挙する
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;

    // Dictionaryと同じ内容を含むListを作成
    List<KeyValuePair<string, int>> list = new List<KeyValuePair<string, int>>(dict);

    // for文を使って作成したListを列挙
    for (int i = 0; i < list.Count; i++) {
      Console.WriteLine("{0} : {1} => {2}", i, list[i].Key, list[i].Value);
    }
  }
}
実行結果
0 : foo => 16
1 : bar => 72
2 : baz => 42

キーもしくは値を配列にコピーしてから列挙したい場合は、次の例のようにKeysプロパティ・Valuesプロパティを参照してCopyToメソッドによって配列にコピーした上で列挙するという方法がとれます。

Dictionaryのすべてのキーと値を配列にコピーして列挙する
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;

    // キーと値を格納するための配列を作成
    string[] keys = new string[dict.Count];
    int[] values = new int[dict.Count];

    // キーと値を配列にコピー
    dict.Keys.CopyTo(keys, 0);
    dict.Values.CopyTo(values, 0);

    // for文を使ってコピーしたキーと値の配列を列挙
    for (int i = 0; i < keys.Length; i++) {
      Console.WriteLine("{0} : {1} => {2}", i, keys[i], values[i]);
    }
  }
}
実行結果
0 : foo => 16
1 : bar => 72
2 : baz => 42

キーの型をintなどの整数型にすることでインデックスを持たせたDictionaryのように扱うことはできますが、§.要素の列挙と順序でも述べたようにDictionaryに格納された要素をforeach文で列挙する際の順序は不定である点、インデックス(キー)が不連続な値となりうる点などから、あまり推奨できる方法ではありません。

Dictionaryのキーをインデックスとして扱う例
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    // キーをintにしてインデックスとして扱う
    Dictionary<int, string> dict = new Dictionary<int, string>();

    dict[0] = "foo";
    dict[1] = "bar";
    dict[3] = "baz";

    for (int i = 0; i < dict.Count; i++) {
      // キー2の要素は存在しないため、途中で例外が発生する
      Console.WriteLine("{0} => {1}", i, dict[i]);
    }
  }
}
実行結果
0 => foo
1 => bar

ハンドルされていない例外: System.Collections.Generic.KeyNotFoundException: 指定されたキーはディレクトリ内に存在しませんでした。
   場所 System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   場所 Sample.Main()

§4.2 SortedList

SortedListを使うと、キーと値のペアにインデックスを持たせることができます。 ただし、SortedListではインデックスを指定して要素を格納することはできません。 インデックスは常にキーに従って要素を並べ替えた時の順序で自動的に割り振られます。

DictionaryのかわりにSortedListを使う例
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    SortedList<string, int> list = new SortedList<string, int>();

    list["foo"] = 16;
    list["bar"] = 72;
    list["baz"] = 42;

    Console.WriteLine("IndexOfKey(foo) = {0}", list.IndexOfKey("foo"));
    Console.WriteLine("IndexOfValue(42) = {0}", list.IndexOfValue(42));

    for (int i = 0; i < list.Count; i++) {
      Console.WriteLine("{0}: {1} => {2}", i, list.Keys[i], list.Values[i]);
    }
  }
}
実行結果
IndexOfKey(foo) = 2
IndexOfValue(42) = 1
0: bar => 72
1: baz => 42
2: foo => 16

§4.3 OrderedDictionary

System.Collections.Specialized名前空間のOrderedDictionaryクラスでは、要素が格納された順にインデックスが割り振られ、列挙する際も常に格納された順で列挙されます。 ただし、OrderedDictionaryは非ジェネリックなコレクションです。 OrderedDictionaryに相当するジェネリックコレクションは用意されていません。

DictionaryのかわりにOrderedDictionaryを使う例
using System;
using System.Collections;
using System.Collections.Specialized;

class Sample {
  static void Main()
  {
    OrderedDictionary dict = new OrderedDictionary();

    dict["foo"] = 16;
    dict["bar"] = 72;
    dict["baz"] = 42;

    // OrderedDictionaryに格納された要素を列挙
    foreach (DictionaryEntry entry in dict) {
      Console.WriteLine("{0} => {1}", entry.Key, entry.Value);
    }

    // OrderedDictionaryに格納されている値をインデックス順で列挙
    for (int i = 0; i < dict.Count; i++) {
      Console.WriteLine("{0}: {1}", i, dict[i]);
    }
  }
}
実行結果
foo => 16
bar => 72
baz => 42
0: 16
1: 72
2: 42

OrderedDictionaryクラスについてはOrderedDictionaryクラスを、またこれに関連してジェネリック版OrderedDictionaryを参照してください。

§5 入れ子

Listや他のジェネリックコレクション同様、Dictionaryもキー・値ともに任意の型に型付けすることができるため、キー・値に他のジェネリックコレクション型を格納するようにすることも出来ます。 次の例では、Dictionaryクラスを使って文字列の辞書を処理するために入れ子になったDictionary<char, List<string>>を作成し、要素の追加・削除・列挙などの操作を行っています。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    // 値にList<string>を格納するDictionaryを作成
    Dictionary<char, List<string>> dictionary = new Dictionary<char, List<string>>();

    dictionary.Add('a', new List<string>() {"apple", "action", "after"}); // 'a'で始まる単語のListを追加
    dictionary.Add('b', new List<string>() {"big", "best", "bridge"});    // 'b'で始まる単語のListを追加
    dictionary.Add('c', new List<string>() {"cheese", "cat", "connect"}); // 'c'で始まる単語のListを追加

    dictionary['a'].Add("add");         // 'a'で始まる単語のListにアイテムを追加
    dictionary['b'].Add("book");        // 'b'で始まる単語のListにアイテムを追加
    dictionary['c'].Remove("connect");  // 'c'で始まる単語のListからアイテムを削除

    // すべての単語のListをソートする
    foreach (List<string> words in dictionary.Values) {
      words.Sort();
    }

    // 辞書の内容を表示
    foreach (KeyValuePair<char, List<string>> pair in dictionary) {
      Console.Write("'{0}': ", pair.Key);

      // Listの内容を表示
      List<string> words = pair.Value;

      foreach (string word in words) {
        Console.Write("{0} ", word);
      }

      Console.WriteLine();
    }
  }
}
実行結果
'a': action add after apple 
'b': best big book bridge 
'c': cat cheese 

同様に、DictionaryのDictionaryを作成することも可能です。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    // 値にDictionary<string, string[]>を格納するDictionaryを作成
    Dictionary<string, Dictionary<string, string[]>> mimeTypes = new Dictionary<string, Dictionary<string, string[]>>();

    // text/*に該当する拡張子
    mimeTypes["text"] = new Dictionary<string, string[]>();
    mimeTypes["text"]["plain"] = new string[] {".txt"};
    mimeTypes["text"]["html"] = new string[] {".htm", ".html"};

    // image/*に該当する拡張子
    mimeTypes["image"] = new Dictionary<string, string[]>();
    mimeTypes["image"]["gif"] = new string[] {".gif"};
    mimeTypes["image"]["png"] = new string[] {".png"};
    mimeTypes["image"]["jpeg"] = new string[] {".jpg", ".jpeg", ".jpe"};

    // 内容を表示
    foreach (KeyValuePair<string, Dictionary<string, string[]>> mimeType in mimeTypes) {
      Console.WriteLine("{0}/*", mimeType.Key);

      foreach (KeyValuePair<string, string[]> mimeSubType in mimeType.Value) {
        Console.Write("  {0}/{1} : ", mimeType.Key, mimeSubType.Key);

        foreach (string extension in mimeSubType.Value) {
          Console.Write("{0} ", extension);
        }

        Console.WriteLine();
      }
    }
  }
}
実行結果
text/*
  text/plain : .txt 
  text/html : .htm .html 
image/*
  image/gif : .gif 
  image/png : .png 
  image/jpeg : .jpg .jpeg .jpe 

§6 読み取り専用

DictionaryにはList.AsReadOnlyメソッドのようなメソッドが存在しないため、読み取り専用のDictionaryを作成する手段が存在しません。 .NET Framework 4.5からはReadOnlyDictionaryが導入されましたが、それ以前のバージョンでは使用することができないため、読み取り専用のDictionaryが必要な場合は自前で実装する必要があります。

次の例はそのような読み取り専用のクラスReadOnlyDictionaryを実装した例です。 動作やスローする例外等はReadOnlyCollectionに近いものとなるようにしています。 また、IDictionaryに対して拡張メソッドAsReadOnlyを追加するためのクラスIDictionaryExtensionsも作成しています。 なお、この例で実装しているReadOnlyDictionaryクラスでは元のDictionaryを読み取り専用にする訳ではないので注意してください。 元になったDictionaryは、依然として要素の追加・削除・変更が可能です。

ReadOnlyDictionaryの実装例
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, int> dict = new Dictionary<string, int>();

    dict["foo"] = 16;
    dict["bar"] = 72;

    // 読み取り専用のIDictionaryを作成する
    IDictionary<string, int> readOnlyDict = dict.AsReadOnly();

    foreach (KeyValuePair<string, int> pair in readOnlyDict) {
      Console.WriteLine("{0} => {1}", pair.Key, pair.Value);
    }

    // 既に存在するキーの値の変更を試みる
    readOnlyDict["foo"] = 42;
  }
}

// IDictionaryに拡張メソッドAsReadOnlyを追加するためのクラス
public static class IDictionaryExtensions {
  public static IDictionary<TKey, TValue> AsReadOnly<TKey, TValue>(this IDictionary<TKey, TValue> dictionary)
  {
    if (dictionary == null)
      throw new ArgumentNullException("dictionary");
    else if (dictionary.IsReadOnly)
      return dictionary;
    else
      return new ReadOnlyDictionary<TKey, TValue>(dictionary);
  }
}

// IDictionaryをラップして読み取りアクセスのみ許可するようにIDictionaryを実装したクラス
public class ReadOnlyDictionary<TKey, TValue> : IDictionary<TKey, TValue> {
  // ラップするIDictionary
  private IDictionary<TKey, TValue> dictionary;

  public TValue this[TKey key] {
    get { return dictionary[key]; }
    set { throw ReadOnlyException(); }
  }

  public ICollection<TKey> Keys {
    get { return dictionary.Keys; }
  }

  public ICollection<TValue> Values {
    get { return dictionary.Values; }
  }

  public int Count {
    get { return dictionary.Count; }
  }

  public bool IsReadOnly {
    get { return true; }
  }

  public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
  {
    if (dictionary == null)
      throw new ArgumentNullException("dictionary");

    this.dictionary = dictionary;
  }

  /*
   * 元のDictionaryに対する読み取りのみが行われる操作
   */
  public bool Contains(KeyValuePair<TKey, TValue> item)
  {
    return dictionary.Contains(item);
  }

  public bool ContainsKey(TKey key)
  {
    return dictionary.ContainsKey(key);
  }

  public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
  {
    dictionary.CopyTo(array, arrayIndex);
  }

  public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
  {
    return dictionary.GetEnumerator();
  }

  System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
  {
    return dictionary.GetEnumerator();
  }

  public bool TryGetValue(TKey key, out TValue @value)
  {
    return dictionary.TryGetValue(key, out @value);
  }

  /*
   * 元のDictionaryに対する変更が行われる操作
   */
  public void Add(KeyValuePair<TKey, TValue> item)
  {
    throw ReadOnlyException();
  }

  public void Add(TKey key, TValue @value)
  {
    throw ReadOnlyException();
  }

  public void Clear()
  {
    throw ReadOnlyException();
  }

  public bool Remove(KeyValuePair<TKey, TValue> item)
  {
    throw ReadOnlyException();
  }

  public bool Remove(TKey key)
  {
    throw ReadOnlyException();
  }

  private Exception ReadOnlyException()
  {
    return new NotSupportedException("コレクションは読み取り専用です");
  }
}
実行結果
foo => 16
bar => 72

ハンドルされていない例外: System.NotSupportedException: コレクションは読み取り専用です
   場所 ReadOnlyDictionary`2.set_Item(TKey key, TValue val)
   場所 Sample.Main()