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日)