C#のLINQの関数であるToDictionary()
の使い方についてです。
配列やリストなどのシーケンスからDictionary
を作ることが出来ます。
この記事には.NET Framework 4.6.1を使用しています。
配列からDictionaryを作ってみよう
Dictionary
が欲しい!
配列なりリストなりのデータは手元にあり、データ内にキーとなりそうなプロパティも所持している。
まあ、あとはForEach
などを使って順番にDictionary
の変数にぶっこんでやれば作れそうです。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class Parameter { public int ID { get; set; } public int Age { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "ID:{0}, Age:{1}, Name:{2}", ID, Age, Name ); } } static void Main( string[] args ) { // 名前データ。 Parameter[] parameters = new Parameter[] { new Parameter() { ID = 0, Age = 52, Name = "正一郎" }, new Parameter() { ID = 8, Age = 28, Name = "清次郎" }, new Parameter() { ID = 3, Age = 20, Name = "誠三郎" }, new Parameter() { ID = 4, Age = 18, Name = "征史郎" }, }; // IDをキーとしたDictionaryを作成する。 Dictionary<int, Parameter> dictionary = new Dictionary<int, Parameter>(); foreach( var parameter in parameters ) { dictionary.Add( parameter.ID, parameter ); } // 結果発表 System.Console.WriteLine( "parameters:{0}", parameters.Text() ); System.Console.WriteLine( "dictionary:{0}", dictionary.Text() ); // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
parameters:[ID:0, Age:52, Name:正一郎], [ID:8, Age:28, Name:清次郎], [ID:3, Age:
20, Name:誠三郎], [ID:4, Age:18, Name:征史郎],
dictionary:0, ID:0, Age:52, Name:正一郎, 8, ID:8, Age:28, Name:清次郎, [
[3, ID:3, Age:20, Name:誠三郎]], 4, ID:4, Age:18, Name:征史郎,
ほい、出来ました。
この方法でもいいのですが、LINQのToDictionary()
を使うことでもう少しコードを簡略化できます。
public static Dictionary<TKey, TSource> ToDictionary<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector );
Enumerable.ToDictionary(TSource, TKey) メソッド (IEnumerable(TSource), Func(TSource, TKey)) (System.Linq)
使い方は、配列やリストからToDictionary()
を呼び、引数にキーとなるプロパティを取得する処理を記述するだけ。
以下の例では、ラムダ式を用いてキーとなるプロパティを取得する処理を記述しています。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class Parameter { public int ID { get; set; } public int Age { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "ID:{0}, Age:{1}, Name:{2}", ID, Age, Name ); } } static void Main( string[] args ) { // 名前データ。 Parameter[] parameters = new Parameter[] { new Parameter() { ID = 0, Age = 52, Name = "正一郎" }, new Parameter() { ID = 8, Age = 28, Name = "清次郎" }, new Parameter() { ID = 3, Age = 20, Name = "誠三郎" }, new Parameter() { ID = 4, Age = 18, Name = "征史郎" }, }; // IDをキーとしたDictionaryを作成する。 Dictionary<int, Parameter> dictionary = parameters.ToDictionary( value => value.ID ); // 結果発表 System.Console.WriteLine( "parameters:{0}", parameters.Text() ); System.Console.WriteLine( "dictionary:{0}", dictionary.Text() ); // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
parameters:[ID:0, Age:52, Name:正一郎], [ID:8, Age:28, Name:清次郎], [ID:3, Age:
20, Name:誠三郎], [ID:4, Age:18, Name:征史郎],
dictionary:0, ID:0, Age:52, Name:正一郎, 8, ID:8, Age:28, Name:清次郎, [
[3, ID:3, Age:20, Name:誠三郎]], 4, ID:4, Age:18, Name:征史郎,
Valueの型も指定しちゃう
さきほどのToDictionary()
では、配列の型となっているクラスをそのままDictionary
のバリューとして使っていました。
でもバリューとして扱う型も変更したい! そんな場合もToDictionary()
は対応しています。
public static Dictionary<TKey, TElement> ToDictionary<TSource, TKey, TElement>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector );
Enumerable.ToDictionary(TSource, TKey, TElement) メソッド (IEnumerable(TSource), Func(TSource, TKey), Func(TSource, TElement)) (System.Linq)
第二引数に、バリューとなるプロパティを取得する処理を記述すればOKです。
以下の例では、ラムダ式を用いてバリューとなるプロパティを取得する処理を記述しています。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class Parameter { public int ID { get; set; } public int Age { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "ID:{0}, Age:{1}, Name:{2}", ID, Age, Name ); } } static void Main( string[] args ) { // 名前データ。 Parameter[] parameters = new Parameter[] { new Parameter() { ID = 0, Age = 52, Name = "正一郎" }, new Parameter() { ID = 8, Age = 28, Name = "清次郎" }, new Parameter() { ID = 3, Age = 20, Name = "誠三郎" }, new Parameter() { ID = 4, Age = 18, Name = "征史郎" }, }; // キーがID、バリューがNameのDictionaryを作成する。 Dictionary<int, string> dictionary = parameters.ToDictionary( value => value.ID, value => value.Name ); // 結果発表 System.Console.WriteLine( "parameters:{0}", parameters.Text() ); System.Console.WriteLine( "dictionary:{0}", dictionary.Text() ); // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
parameters:[ID:0, Age:52, Name:正一郎], [ID:8, Age:28, Name:清次郎], [ID:3, Age:
20, Name:誠三郎], [ID:4, Age:18, Name:征史郎],
dictionary:0, 正一郎, 8, 清次郎, 3, 誠三郎, 4, 征史郎,
このように、元の配列のID
をキーにName
をバリューにしたDictionary
が出来ました。
キーが重複したら例外がやってくる
ToDictionary()
ではDictionary
を作るわけですが、Dictionary
である以上、キーとなるプロパティが重複するわけにはいきません。
じゃあ、ToDictionary()
を使った際に、キーが重複するようなコードを書いた場合はどうなってしまうのでしょうか。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class Parameter { public int ID { get; set; } public int Age { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "ID:{0}, Age:{1}, Name:{2}", ID, Age, Name ); } } static void Main( string[] args ) { // 名前データ。(誠三郎と征史郎のIDが被ってる……) Parameter[] parameters = new Parameter[] { new Parameter() { ID = 0, Age = 52, Name = "正一郎" }, new Parameter() { ID = 8, Age = 28, Name = "清次郎" }, new Parameter() { ID = 4, Age = 20, Name = "誠三郎" }, new Parameter() { ID = 4, Age = 18, Name = "征史郎" }, }; // キーがID、バリューがNameのDictionaryを作成する。 Dictionary<int, Parameter> dictionary = null; try { dictionary = parameters.ToDictionary( value => value.ID ); } catch( System.Exception i_exception ) { System.Console.WriteLine( "{0}", i_exception ); System.Console.ReadKey(); return; } // 結果発表 System.Console.WriteLine( "parameters:{0}", parameters.Text() ); System.Console.WriteLine( "dictionary:{0}", dictionary.Text() ); // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
System.ArgumentException: 同一のキーを含む項目が既に追加されています。
例外が発生しました。
当然っちゃ当然ですね。
参照型をキーとする場合の比較処理について
先ほどまでは値型であるint
型をキーとして使用してきました。
では、参照型の変数をキーとして使用するとどうなるのでしょうか。
以下のようなクラスを用意しました。
Program.cs
private class KeyData { public int ID { get; set; } public string Version { get; set; } public override string ToString() { return string.Format( "{0}_{1}", ID, Version ); } }
このクラスをキーとするDictionary
を作ってみます。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class KeyData { public int ID { get; set; } public string Version { get; set; } public override string ToString() { return string.Format( "{0}_{1}", ID, Version ); } } private class Parameter { public KeyData Key { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "Key:{0}, Name:{1}", Key, Name ); } } static void Main( string[] args ) { // 名前データ(誠三郎と征史郎のKeyDataが被ってる……) Parameter[] parameters = new Parameter[] { new Parameter() { Key = new KeyData() { ID = 0, Version = "A" }, Name = "正一郎" }, new Parameter() { Key = new KeyData() { ID = 0, Version = "B" }, Name = "清次郎" }, new Parameter() { Key = new KeyData() { ID = 2, Version = "C" }, Name = "誠三郎" }, new Parameter() { Key = new KeyData() { ID = 2, Version = "C" }, Name = "征史郎" }, }; // キーをKeyDataのDictionaryを作成する。 Dictionary<KeyData, Parameter> dictionary = null; try { dictionary = parameters.ToDictionary( value => value.Key ); } catch( System.Exception i_exception ) { System.Console.WriteLine( "{0}", i_exception ); System.Console.ReadKey(); return; } // 結果発表 System.Console.WriteLine( "parameters:{0}", parameters.Text() ); System.Console.WriteLine( "dictionary:{0}", dictionary.Text() ); // 指定したデータを取得してみよう try { Parameter result = dictionary[ new KeyData() { ID = 2, Version = "C" } ]; System.Console.WriteLine( "result:{0}", result ); } catch( System.Exception i_exception ) { System.Console.WriteLine( "{0}", i_exception ); System.Console.ReadKey(); return; } // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
parameters:[Key:0_A, Name:正一郎], [Key:0_B, Name:清次郎], [Key:2_C, Name:誠三郎
], [Key:2_C, Name:征史郎],
dictionary:0_A, Key:0_A, Name:正一郎, 0_B, Key:0_B, Name:清次郎, [[2_C,
Key:2_C, Name:誠三郎]], 2_C, Key:2_C, Name:征史郎,
System.Collections.Generic.KeyNotFoundException: 指定されたキーはディレクトリ内に存在しませんでした。
さて、妙な結果になりました。
三番目と四番目のデータのKeyData
の情報は、どちらもID
は2、Version
はCと同じものを使っています。
にもかかわらず、ToDictionary()
では例外が発生していません!
そして、Dictionary
内のデータを取得しようと、同等のKeyData
を作成し取得しようとすると指定されたキーが存在しないという例外が発生しました!
これは参照型の比較の場合は、データの中身が同じかではなく、参照がおなじかどうかで判断されます。
今回の場合は、KeyData
の中身はどれも「ID:2
,Version:C
」ですが、全てのnew
を使用して新たに作られたものなので全て参照しているものが違います。
そのためこのような結果になってしまったのです。
以下の書き方の場合は、同じ参照を使っているので、ToDictionary()
の時点で例外が発生します。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class KeyData { public int ID { get; set; } public string Version { get; set; } public override string ToString() { return string.Format( "{0}_{1}", ID, Version ); } } private class Parameter { public KeyData Key { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "Key:{0}, Name:{1}", Key, Name ); } } static void Main( string[] args ) { KeyData keyA = new KeyData() { ID = 0, Version = "A" }; KeyData keyB = new KeyData() { ID = 0, Version = "B" }; KeyData keyC = new KeyData() { ID = 2, Version = "C" }; // 名前データ(誠三郎と征史郎のKeyDataが被ってる……) Parameter[] parameters = new Parameter[] { new Parameter() { Key = keyA, Name = "正一郎" }, new Parameter() { Key = keyB, Name = "清次郎" }, new Parameter() { Key = keyC, Name = "誠三郎" }, new Parameter() { Key = keyC, Name = "征史郎" }, }; // キーをKeyDataのDictionaryを作成する。 Dictionary<KeyData, Parameter> dictionary = null; try { dictionary = parameters.ToDictionary( value => value.Key ); } catch( System.Exception i_exception ) { System.Console.WriteLine( "{0}", i_exception ); System.Console.ReadKey(); return; } // 結果発表 System.Console.WriteLine( "parameters:{0}", parameters.Text() ); System.Console.WriteLine( "dictionary:{0}", dictionary.Text() ); // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
System.ArgumentException: 同一のキーを含む項目が既に追加されています。
重複しているデータを削除すれば、問題なく動くようになります。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class KeyData { public int ID { get; set; } public string Version { get; set; } public override string ToString() { return string.Format( "{0}_{1}", ID, Version ); } } private class Parameter { public KeyData Key { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "Key:{0}, Name:{1}", Key, Name ); } } static void Main( string[] args ) { KeyData keyA = new KeyData() { ID = 0, Version = "A" }; KeyData keyB = new KeyData() { ID = 0, Version = "B" }; KeyData keyC = new KeyData() { ID = 2, Version = "C" }; // 名前データ(征史郎はコメントアウトだ……) Parameter[] parameters = new Parameter[] { new Parameter() { Key = keyA, Name = "正一郎" }, new Parameter() { Key = keyB, Name = "清次郎" }, new Parameter() { Key = keyC, Name = "誠三郎" }, // new Parameter() { Key = keyC, Name = "征史郎" }, }; // キーをKeyDataのDictionaryを作成する。 Dictionary<KeyData, Parameter> dictionary = null; try { dictionary = parameters.ToDictionary( value => value.Key ); } catch( System.Exception i_exception ) { System.Console.WriteLine( "{0}", i_exception ); System.Console.ReadKey(); return; } // 結果発表 System.Console.WriteLine( "parameters:{0}", parameters.Text() ); System.Console.WriteLine( "dictionary:{0}", dictionary.Text() ); // 指定したデータを取得してみよう try { Parameter result = dictionary[ keyC ]; System.Console.WriteLine( "result:{0}", result ); } catch( System.Exception i_exception ) { System.Console.WriteLine( "{0}", i_exception ); System.Console.ReadKey(); return; } // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
parameters:[Key:0_A, Name:正一郎], [Key:0_B, Name:清次郎], [Key:2_C, Name:誠三郎
],
dictionary:0_A, Key:0_A, Name:正一郎, 0_B, Key:0_B, Name:清次郎, [[2_C,
Key:2_C, Name:誠三郎]],
result:Key:2_C, Name:誠三郎
さて、参照型をキーにした際の挙動は分かったのですが、それでもクラス内のプロパティの値を比較してキーと比較を行うように出来ないものでしょうか。
その場合は以下のオーバーロードされたToDictionary()
を使いましょう。
public static Dictionary<TKey, TSource> ToDictionary<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer );
Enumerable.ToDictionary(TSource, TKey) メソッド (IEnumerable(TSource), Func(TSource, TKey), IEqualityComparer(TKey)) (System.Linq)
public static Dictionary<TKey, TElement> ToDictionary<TSource, TKey, TElement>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer );
Enumerable.ToDictionary(TSource, TKey, TElement) メソッド (IEnumerable(TSource), Func(TSource, TKey), Func(TSource, TElement), IEqualityComparer(TKey)) (System.Linq)
IEqualityComparer
を継承した比較クラスを引数で渡すことにより、独自のキーの比較処理を行うことが出来ます。
IEqualityComparer
IEqualityComparer(T) インターフェイス (System.Collections.Generic)
今回は以下のような比較クラスを作成してみました。
Program.cs
private class KeyDataComparer : IEqualityComparer<KeyData> { public bool Equals( KeyData i_lhs, KeyData i_rhs ) { if( i_lhs.ID == i_rhs.ID && i_lhs.Version == i_rhs.Version ) { return true; } return false; } public int GetHashCode( KeyData i_obj ) { return i_obj.ID ^ i_obj.Version.GetHashCode(); } }
このクラスを使って、キーの比較の際は参照先ではなく、中身のプロパティによって判断されるようにしてみましょう。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class KeyData { public int ID { get; set; } public string Version { get; set; } public override string ToString() { return string.Format( "{0}_{1}", ID, Version ); } } // 比較用のクラス。 // KeyDataクラスに組み込んじゃってもいいけどね。 private class KeyDataComparer : IEqualityComparer<KeyData> { public bool Equals( KeyData i_lhs, KeyData i_rhs ) { if( i_lhs.ID == i_rhs.ID && i_lhs.Version == i_rhs.Version ) { return true; } return false; } public int GetHashCode( KeyData i_obj ) { return i_obj.ID ^ i_obj.Version.GetHashCode(); } } private class Parameter { public KeyData Key { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "Key:{0}, Name:{1}", Key, Name ); } } static void Main( string[] args ) { // 名前データ(誠三郎と征史郎のKeyDataが被ってる……) Parameter[] parameters = new Parameter[] { new Parameter() { Key = new KeyData() { ID = 0, Version = "A" }, Name = "正一郎" }, new Parameter() { Key = new KeyData() { ID = 0, Version = "B" }, Name = "清次郎" }, new Parameter() { Key = new KeyData() { ID = 2, Version = "C" }, Name = "誠三郎" }, new Parameter() { Key = new KeyData() { ID = 2, Version = "C" }, Name = "征史郎" }, }; // キーをKeyDataのDictionaryを作成する。 Dictionary<KeyData, Parameter> dictionary = null; try { KeyDataComparer comparer = new KeyDataComparer(); dictionary = parameters.ToDictionary( value => value.Key, comparer ); } catch( System.Exception i_exception ) { System.Console.WriteLine( "{0}", i_exception ); System.Console.ReadKey(); return; } // 結果発表 System.Console.WriteLine( "parameters:{0}", parameters.Text() ); System.Console.WriteLine( "dictionary:{0}", dictionary.Text() ); // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
System.ArgumentException: 同一のキーを含む項目が既に追加されています。
ToDictionary()
の時点で例外が発生しました。
これは期待できそうです。
キー情報が重複しないようにプロパティを変更して、最後まで見てみましょう。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class KeyData { public int ID { get; set; } public string Version { get; set; } public override string ToString() { return string.Format( "{0}_{1}", ID, Version ); } } // 比較用のクラス。 // KeyDataクラスに組み込んじゃってもいいけどね。 private class KeyDataComparer : IEqualityComparer<KeyData> { public bool Equals( KeyData i_lhs, KeyData i_rhs ) { if( i_lhs.ID == i_rhs.ID && i_lhs.Version == i_rhs.Version ) { return true; } return false; } public int GetHashCode( KeyData i_obj ) { return i_obj.ID ^ i_obj.Version.GetHashCode(); } } private class Parameter { public KeyData Key { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "Key:{0}, Name:{1}", Key, Name ); } } static void Main( string[] args ) { // 名前データ Parameter[] parameters = new Parameter[] { new Parameter() { Key = new KeyData() { ID = 0, Version = "A" }, Name = "正一郎" }, new Parameter() { Key = new KeyData() { ID = 0, Version = "B" }, Name = "清次郎" }, new Parameter() { Key = new KeyData() { ID = 2, Version = "C" }, Name = "誠三郎" }, new Parameter() { Key = new KeyData() { ID = 3, Version = "C" }, Name = "征史郎" }, }; // キーをKeyDataのDictionaryを作成する。 Dictionary<KeyData, Parameter> dictionary = null; try { KeyDataComparer comparer = new KeyDataComparer(); dictionary = parameters.ToDictionary( value => value.Key, comparer ); } catch( System.Exception i_exception ) { System.Console.WriteLine( "{0}", i_exception ); System.Console.ReadKey(); return; } // 結果発表 System.Console.WriteLine( "parameters:{0}", parameters.Text() ); System.Console.WriteLine( "dictionary:{0}", dictionary.Text() ); // 指定したデータを取得してみよう try { Parameter result = dictionary[ new KeyData() { ID = 2, Version = "C" } ]; System.Console.WriteLine( "result:{0}", result ); } catch( System.Exception i_exception ) { System.Console.WriteLine( "{0}", i_exception ); System.Console.ReadKey(); return; } // 入力待ち用 System.Console.ReadKey(); } /// <summary> /// 簡易的なシーケンスのテキスト取得処理 /// </summary> public static string Text( this IEnumerable i_source ) { string text = string.Empty; foreach( var value in i_source ) { text += string.Format( "[{0}], ", value ); } return text; } } // class Program
parameters:[Key:0_A, Name:正一郎], [Key:0_B, Name:清次郎], [Key:2_C, Name:誠三郎
], [Key:3_C, Name:征史郎],
dictionary:0_A, Key:0_A, Name:正一郎, 0_B, Key:0_B, Name:清次郎, [[2_C,
Key:2_C, Name:誠三郎]], 3_C, Key:3_C, Name:征史郎,
result:Key:2_C, Name:誠三郎
参照型をキーとして使用しても、キーの比較は中身のプロパティから判断できるようになりました。
是非ともToDictionary()
を七兆回ほど使って、世の中をDictionary
だらけにしてみてください。
LINQのリンク
LINQ一覧
www.urablog.xyzToArray
配列に変換したい!
www.urablog.xyzToList
List型に変換したい!
www.urablog.xyz