C#のLINQの関数であるGroupJoin()
の使い方についてです。
複数の配列やリストの要素を結合してグループ化することが出来ます。
この記事には.NET Framework 4.6.1を使用しています。
グループ化して結合だ
LINQには、データを結合するJoin()
、グループ化するGroupBy()
があります。
www.urablog.xyz
www.urablog.xyz
それをまとめておこないたいときには、GroupJoin()
というものがあります。
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, IEnumerable<TInner>, TResult> resultSelector );
Enumerable.GroupJoin(TOuter, TInner, TKey, TResult) メソッド (IEnumerable(TOuter), IEnumerable(TInner), Func(TOuter, TKey), Func(TInner, TKey), Func(TOuter, IEnumerable(TInner), TResult)) (System.Linq)
……引数が多くて、なかなか面倒くさそうな関数ではありますが、一度使ってみましょう。
ではまず、データ用のクラスを二種類ほど用意してみます。
Program.cs
private class PersonData { public string Name { get; set; } public int Age { get; set; } public int ItemID { get; set; } public override string ToString() { return string.Format( "Name:{0}, Age:{1}, ItemID:{2}", Name, Age, ItemID ); } }
Program.cs
private class ItemData { public int ID { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "ID:{0}, Name:{1}", ID, Name ); } }
PersonData
は人の情報クラス。
名前とアイテムIDと年齢を持っています。
ItemData
はアイテムの情報クラス。
IDと名前を持っています。
PersonData
の持つアイテムIDと、ItemData
の持つIDは同じものを指しています。
「ID = 1, Name = "金"」というデータを持つItemData
があり、
「ItemID = 1」というデータを持つPersonData
がある場合、
この「ItemID = 1」は、「ID = 1, Name = "金"」のItemData
を指すことになります。
このデータを用いてGroupJoin()
を使ってみましょう。
えーと、引数が多いので面倒なのですが、
- 第一引数:紐づけたい情報の配列やリスト
- 第二引数:自分自身の紐づけるプロパティの取得処理
- 第三引数:紐づけたい情報の配列やリストの紐づけるプロパティの取得処理
- 第四引数:紐づけられた情報に対して行う処理
を記述する感じです。
……うまく説明できている気がしないので、コード見てみてください。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class PersonData { public string Name { get; set; } public int Age { get; set; } public int ItemID { get; set; } public override string ToString() { return string.Format( "Name:{0}, Age:{1}, ItemID:{2}", Name, Age, ItemID ); } } private class ItemData { public int ID { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "ID:{0}, Name:{1}", ID, Name ); } } private class GroupData { public string ItemName { get; set; } public PersonData[] Persons { get; set; } } static void Main( string[] args ) { // 人物データ PersonData[] persons = new PersonData[] { new PersonData() { Name = "正一郎", Age = 35, ItemID = 0 }, new PersonData() { Name = "清次郎", Age = 27, ItemID = 1 }, new PersonData() { Name = "誠三郎", Age = 27, ItemID = 3 }, new PersonData() { Name = "征史郎", Age = 18, ItemID = 0 }, }; ItemData[] items = new ItemData[] { new ItemData() { ID = 0, Name = "金" }, new ItemData() { ID = 1, Name = "権力" }, new ItemData() { ID = 2, Name = "人望" }, }; // アイテム情報でグループ化するよ。 IEnumerable<GroupData> groupList = items.GroupJoin( persons, item => item.ID, person => person.ItemID, ( item, personList ) => new GroupData() { ItemName = item.Name, Persons = personList.ToArray() } ); // 結果発表 System.Console.WriteLine( "persons:{0}", persons.Text() ); System.Console.WriteLine( "items :{0}", items.Text() ); foreach( var group in groupList ) { System.Console.WriteLine( "item:{0}", group.ItemName ); foreach( var person in group.Persons ) { System.Console.WriteLine( "\t{0}", person.Name ); } } // 入力待ち用 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
persons:[Name:正一郎, Age:35, ItemID:0], [Name:清次郎, Age:27, ItemID:1], [Name:誠三郎, Age:27, ItemID:3], [Name:征史郎, Age:18, ItemID:0],
items :[ID:0, Name:金], [ID:1, Name:権力], [ID:2, Name:人望],
item:金
正一郎
征史郎
item:権力
清次郎
item:人望
アイテムごとに各自分情報をグループ化しています。
今回はGroupData
クラスというグループ化専用のクラスを用意していますが、ほかで使わないのなら匿名型にしてしまってもいいかもしれません。
匿名型を使った例が以下の通りです。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class PersonData { public string Name { get; set; } public int Age { get; set; } public int ItemID { get; set; } public override string ToString() { return string.Format( "Name:{0}, Age:{1}, ItemID:{2}", Name, Age, ItemID ); } } private class ItemData { public int ID { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "ID:{0}, Name:{1}", ID, Name ); } } static void Main( string[] args ) { // 人物データ PersonData[] persons = new PersonData[] { new PersonData() { Name = "正一郎", Age = 35, ItemID = 0 }, new PersonData() { Name = "清次郎", Age = 27, ItemID = 1 }, new PersonData() { Name = "誠三郎", Age = 27, ItemID = 3 }, new PersonData() { Name = "征史郎", Age = 18, ItemID = 0 }, }; ItemData[] items = new ItemData[] { new ItemData() { ID = 0, Name = "金" }, new ItemData() { ID = 1, Name = "権力" }, new ItemData() { ID = 2, Name = "人望" }, }; // アイテム情報でグループ化するよ。 var groupList = items.GroupJoin( persons, item => item.ID, person => person.ItemID, ( item, personList ) => new { ItemName = item.Name, Persons = personList } ); // 結果発表 System.Console.WriteLine( "persons:{0}", persons.Text() ); System.Console.WriteLine( "items :{0}", items.Text() ); foreach( var group in groupList ) { System.Console.WriteLine( "item:{0}", group.ItemName ); foreach( var person in group.Persons ) { System.Console.WriteLine( "\t{0}", person.Name ); } } // 入力待ち用 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
persons:[Name:正一郎, Age:35, ItemID:0], [Name:清次郎, Age:27, ItemID:1], [Name:誠三郎, Age:27, ItemID:3], [Name:征史郎, Age:18, ItemID:0],
items :[ID:0, Name:金], [ID:1, Name:権力], [ID:2, Name:人望],
item:金
正一郎
征史郎
item:権力
清次郎
item:人望
グループ化する際の比較処理を指定する
さて、ここで突然ですが、先ほど使っていたIDを浮動小数型にしてみます。
Program.cs
private class PersonData { public string Name { get; set; } public int Age { get; set; } public float ItemID { get; set; } public override string ToString() { return string.Format( "Name:{0}, Age:{1}, ItemID:{2}", Name, Age, ItemID ); } } private class ItemData { public float ID { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "ID:{0}, Name:{1}", ID, Name ); } }
そしてこのIDに適当な数値を割り振って、同じようにGroupJoin()
を使ってみます。
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class PersonData { public string Name { get; set; } public int Age { get; set; } public float ItemID { get; set; } public override string ToString() { return string.Format( "Name:{0}, Age:{1}, ItemID:{2}", Name, Age, ItemID ); } } private class ItemData { public float ID { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "ID:{0}, Name:{1}", ID, Name ); } } static void Main( string[] args ) { // 人物データ PersonData[] persons = new PersonData[] { new PersonData() { Name = "正一郎", Age = 35, ItemID = 0.5f }, new PersonData() { Name = "清次郎", Age = 27, ItemID = 1.2f }, new PersonData() { Name = "誠三郎", Age = 27, ItemID = 3.3f }, new PersonData() { Name = "征史郎", Age = 18, ItemID = 0.45f }, }; ItemData[] items = new ItemData[] { new ItemData() { ID = 0.2f, Name = "金" }, new ItemData() { ID = 1.1f, Name = "権力" }, new ItemData() { ID = 2.5f, Name = "人望" }, }; // アイテム情報でグループ化するよ。 var groupList = items.GroupJoin( persons, item => item.ID, person => person.ItemID, ( item, personList ) => new { ItemName = item.Name, Persons = personList } ); // 結果発表 System.Console.WriteLine( "persons:{0}", persons.Text() ); System.Console.WriteLine( "items :{0}", items.Text() ); foreach( var group in groupList ) { System.Console.WriteLine( "item:{0}", group.ItemName ); foreach( var person in group.Persons ) { System.Console.WriteLine( "\t{0}", person.Name ); } } // 入力待ち用 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
persons:[Name:正一郎, Age:35, ItemID:0.5], [Name:清次郎, Age:27, ItemID:1.2], [Name:誠三郎, Age:27, ItemID:3.3], [Name: 征史郎, Age:18, ItemID:0.45],
items :[ID:0.2, Name:金], [ID:1.1, Name:権力], [ID:2.5, Name:人望],
item:金
item:権力
item:人望
グループ化がうまくされませんでした。
似たような数値ではありますが、若干値が違うため、当然の結果ではあります。
ではここで、ほとんど値が同じものは同じ値扱いになるように比較処理を自作してみましょう。
IEqualityComparer
を継承したクラスに比較処理を記述することでオリジナルの比較処理が作成できます。
IEqualityComparer
IEqualityComparer(T) インターフェイス (System.Collections.Generic)
こんな感じに作ってみました。
Program.cs
private class GroupComparer : IEqualityComparer<float> { public bool Equals( float i_lhs, float i_rhs ) { return (int)System.Math.Floor( i_lhs ) == (int)System.Math.Floor( i_rhs ); } public int GetHashCode( float i_value ) { int value = (int)System.Math.Floor( i_value ); return value.GetHashCode(); } }
この比較クラスをGroupJoin()
に指定してあげましょう。
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, IEnumerable<TInner>, TResult> resultSelector, IEqualityComparer<TKey> comparer );
Enumerable.GroupJoin(TOuter, TInner, TKey, TResult) メソッド (IEnumerable(TOuter), IEnumerable(TInner), Func(TOuter, TKey), Func(TInner, TKey), Func(TOuter, IEnumerable(TInner), TResult), IEqualityComparer(TKey)) (System.Linq)
Program.cs
using System.Linq; using System.Collections; using System.Collections.Generic; public static class Program { private class PersonData { public string Name { get; set; } public int Age { get; set; } public float ItemID { get; set; } public override string ToString() { return string.Format( "Name:{0}, Age:{1}, ItemID:{2}", Name, Age, ItemID ); } } private class ItemData { public float ID { get; set; } public string Name { get; set; } public override string ToString() { return string.Format( "ID:{0}, Name:{1}", ID, Name ); } } private class GroupComparer : IEqualityComparer<float> { public bool Equals( float i_lhs, float i_rhs ) { return (int)System.Math.Floor( i_lhs ) == (int)System.Math.Floor( i_rhs ); } public int GetHashCode( float i_value ) { int value = (int)System.Math.Floor( i_value ); return value.GetHashCode(); } } static void Main( string[] args ) { // 人物データ PersonData[] persons = new PersonData[] { new PersonData() { Name = "正一郎", Age = 35, ItemID = 0.5f }, new PersonData() { Name = "清次郎", Age = 27, ItemID = 1.2f }, new PersonData() { Name = "誠三郎", Age = 27, ItemID = 3.3f }, new PersonData() { Name = "征史郎", Age = 18, ItemID = 0.45f }, }; ItemData[] items = new ItemData[] { new ItemData() { ID = 0.2f, Name = "金" }, new ItemData() { ID = 1.1f, Name = "権力" }, new ItemData() { ID = 2.5f, Name = "人望" }, }; // アイテム情報でグループ化するよ。 GroupComparer compare = new GroupComparer(); var groupList = items.GroupJoin( persons, item => item.ID, person => person.ItemID, ( item, personList ) => new { ItemName = item.Name, Persons = personList }, compare ); // 結果発表 System.Console.WriteLine( "persons:{0}", persons.Text() ); System.Console.WriteLine( "items :{0}", items.Text() ); foreach( var group in groupList ) { System.Console.WriteLine( "item:{0}", group.ItemName ); foreach( var person in group.Persons ) { System.Console.WriteLine( "\t{0}", person.Name ); } } // 入力待ち用 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
persons:[Name:正一郎, Age:35, ItemID:0.5], [Name:清次郎, Age:27, ItemID:1.2], [Name:誠三郎, Age:27, ItemID:3.3], [Name: 征史郎, Age:18, ItemID:0.45],
items :[ID:0.2, Name:金], [ID:1.1, Name:権力], [ID:2.5, Name:人望],
item:金
正一郎
征史郎
item:権力
清次郎
item:人望
この通り、IDの小数点以下は無視した形でグループ化されるようになりました。
こんな感じに独自の比較処理でグループ化することもできます。
GroupJoin()
を七兆回ほど使って、いろいろグループ化してみてください。
LINQのリンク
- LINQ一覧
www.urablog.xyz