Skip to main content

.NET 配列Dictionary及びIDictionaryの活用

Dictionaryはキー(Key)/値(Value)を1対1ペア形式で保持するジェネリック型の配列です。内部にハッシュ配列アルゴリズムに基づいて実装されており、.NETプログラミングにおける汎用性の高い中堅クラスです。

このクラスを熟知し、様々な場面で活用すれば、効率をよくわかりやすいプログラミングの作成ができます。これらのクラスの継承およびインターフェースの活用について、ネコ技術自身の経験と発想も皆様に共有したいと思います。

基本の使用方法

以下の例では人の名前/年齢というキー/値のオブジェクトを保持します。

必要な引用

DictionaryとIDictionaryの利用には、以下のnamespaceを利用する必要があります。

using System.Collections.Generic;
using System.Linq;

配列の作成

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

キーを利用して値を設定

dict["John"] = 25;
dict["Tom"] = 30;
dict["Mack"] = 45;

キーを利用して値を取得

Console.WriteLine(dict["Tom"]);    // 出力:30

暗黙の型変換

byte age = 10;
dict["Malley"] = age;

但し、大き目のデータから小さめのデータに転換する際に一部のデータが失われる恐れがあり、暗黙の型変換ができない場合もあります。

long oldAge = 87;
dict["Peter"] = oldAge;

キーと値の列挙

配列に全てのキーや値を列挙することも可能です。Keysプロパティは全てのキーを列挙するEnumerator、Valuesプロパティは全ての値を列挙するEnumeratorです。

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

キーの存在チェック

var exist = dict.ContainsKey("John");   // True
var exist = dict.ContainsKey("Smith");  // False

Linqの拡張メソッドによって確認も可能です。

var exist = dict.Keys.Contains("Mack"); // True

値の存在チェック

var exist = dict.ContainsValue(30);     // True
var exist = dict.ContainsValue(-10);    // False

チェックおよび取得

配列に存在しないキーを利用して値を取得するとエラーが起きます。この場合先にチェックしたほうが安全です。

// NG: KeyNotFoundExceptionが発生します
var age = dict["Clark"];

下記のように安全な方法を利用しましょう。

// OK: 安全な方法
int age = 0;
if (dict.ContainsKey("Clark"))
{
  age = dict["Clark"];
}

但し、チェックを付けると配列の検索を2回行いますので性能が落ちます。この場合TryGetValueメソッドを利用します。

int age = 0;
if (dict.TryGetValue("Clark", out age))
{
  // 取得が成功の場合のみ値を出力します
  Console.WriteLine("age = " + age);
}

TryGetValueメソッドを利用すると1回で値を安全に取得できます。また、多くの場合 if を利用しなくてもかまいません。

int age = 0;
dict.TryGetValue("Clark", out age);

キーClarkが存在しなければ、ageは0となります。

元素の数を取得

var keyValueParis = dict.Count;

KeysとValuesのそれぞれの数も取得できますが、キー/値ペアの保持ですので必ずCountと同じ結果になります。

var keys = dict.Keys.Count;
var values = dict.Values.Count;

キーまたは値のList化

var keyList = dict.Keys.ToList();
var valueList = dict.Values.ToList();

初期化

Dictionary配列の初期化

以下のコードは基本的な初期化を示します。

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

dict["John"] = 25;
dict["Tom"] = 30;
dict["Mack"] = 45;
...

簡略した初期化

上記のような基本の初期化は、よりソースコードを簡単化できます。

Dictionary<string, int> dict = new Dictionary<string, int>()
{
  { "John", 25 },
  { "Tom", 30 },
  { "Mack", 45 },
};

作成したdictの内容は同じですが、ソースコードが分かりやすくなりました。

*C# 6.0の新仕様によって、以下の書き方も利用できます。

Dictionary<string, int> dict = new Dictionary<string, int>()
{
  ["John"] = 25,
  ["Tom"] = 30,
  ["Mack"] = 45,
};

拡張

継承して拡張

一部の処理を変えたい、値を監視したい、もしくはログを出力したい場合、Dictionaryを継承して自分のカスタマイズクラスを作成できます。

class ExtendedDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
  public new void Add(TKey key, TValue value)
  {
    base.Add(key, value);
    Console.WriteLine("{0}が追加された", key);
  }

  public new TValue this[TKey key]
  {
    get { return base[key]; }
    set
    {
      base[key] = value;
      Console.WriteLine("{0}が変更された", key);
    }
  }
}

ただ既存のメソッドをoverrideではなく、newキーワードを利用して上書きします。

IDictionaryインターフェース

IDictionaryはDictionaryの基本外観を定義していますので、利用者は自分の実現方法でDictionaryと互換できるクラスを作成できます。

IDictionary と Dictionary の違い

IDictionary はインターフェイス(Interface)ですので、C#の他のインターフェイスと同じく、クラスの外観(外からみる様子)を定義しています。中身は空で、値を保存しません。インターフェイスはそのまま実例化して利用できません。他のクラスの継承先としてしか利用できません。

インターフェース定義

public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable
{
  ICollection<TKey> Keys { get; }
  ICollection<TValue> Values { get; }

  TValue this[TKey key] { get; set; }
  void Add(TKey key, TValue value);

  bool ContainsKey(TKey key);
  bool Remove(TKey key);
  bool TryGetValue(TKey key, out TValue value);
}

カスタマイズ実現

IDictionary<TKey, TValue>に定義しているプロパティ、メソッド以外、IDictionary<TKey, TValue>の継承先のインターフェースに定義しているプロパティ、メソッドの実装も必要です。

class MyDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
  ...
}

マルチ値の対応

学生の各科目の成績をリストとして保持できるDictionaryを作成します。

値の型をListに指定

Dictionary<string, List<int>> listDict = new Dictionary<string, List<int>>();

listDict["John"] = new List<int>(new int[] { 85, 95, 70 });

簡略した初期化

Dictionary<string, List<int>> listDict = new Dictionary<string, List<int>>()
{
  { "John", new List<int>(new int[] { 85, 95, 70 }) },
  { "Tom", new List<int>(new int[] { 98, 76, 85 }) },
};

Dictionaryの継承に基づいて実現

継承先のTValue定義を List<TValue> に変更して、外部からList型を見えないようにします。

class MultiValueDictionary1<TKey, TValue> : Dictionary<TKey, List<TValue>>
{
}

継承したクラスの使用

MultiValueDictionary<string, int> myDictEx1 = new MultiValueDictionary<string, int>();
myDictEx1["John"] = new List<int>() { 85, 95, 70 };
myDictEx1["Tom"] = new List<int>() { 98, 76, 85 };

Johnの値の数を表示、3つの成績が保持されています。

Console.WriteLine(myDictEx1["John"].Count);  // 出力:3

二つ目の成績を出力します。

Console.WriteLine(myDictEx1["John"][1]);     // 出力:95

カスタマイズの初期化方法

TValueをList型にした継承のクラスは、初期化する際にnew List<int>()の記述が必要です。このコードをさらに簡単化できます。

Addメソッドを実装してカスタマイズの初期化方法を拡張できます。(Addメソッドはインターフェースの約束ではなく、C#コンパイラの内蔵機能です)

class MultiValueDictionary1<TKey, TValue> : Dictionary<TKey, List<TValue>>
{
  public void Add(TKey key, params TValue[] values)
  {
    this[key] = new List<TValue>(values);
  }
}

上記Addメソッドの実装の後、以下の簡略した初期化の利用が可能になります。

MultiValueDictionary<string, int> myDictEx2 = new MultiValueDictionary<string, int>()
{
  { "John", 85, 95, 70 },
  { "Tom", 98, 76, 85 },
};

new List<int>()の記述が省略され、ソースコードが大幅に簡略化できました。

型固定のカスタマイズDictionary配列

該当クラスのジェネリック型を隠し、継承元の型を明確に指定すると、型固定のカスタマイズDictionaryを作成できます。

class FixedTypeDictionary : Dictionary<string, List<int>>
{
  public void Add(string key, int score1, int score2, int score3)
  {
    this[key] = new List<int>(new int[] { score1, score2, score3 });
  }
}

型固定のカスタマイズ配列の場合初期化には型の指定がなく、より使いやすくなります。

var fixedDict = new FixedTypeDictionary()
{
  {"John", 85, 95, 70},
  {"Tom", 98, 76, 85},
};

また、実装したAddメソッドには三つのintパラメータを定義していますので、簡略した初期化にも三つのintパラメータの指定が必要です。それ以外の場合コンパイラエラーになります。


(最後更新:2019年3月28日)


Jingwood

北海道の田舎で暮らしているプログラマーです。最近山登りにハマりました。

Leave a Reply

Your email address will not be published. Required fields are marked *