for WPF developers
Home Profile Tips 全記事一覧

GroupBy 拡張メソッドで要素をグループ化する

(2017/03/08 17:52:16 created.)

(2017/03/13 8:33:40 modified.)

GroupBy 拡張メソッドは指定されたキーで分類分けします。同様の機能として ToLookup 拡張メソッドがあります。これらの違いに関しては「3.1.49 ToLookup 拡張メソッドで要素をグループ化する」を参照してください。

Program.cs
  1. namespace Tips_Linq
  2. {
  3.     using System;
  4.     using System.Linq;
  5.  
  6.     class Program
  7.     {
  8.         static void Main(string[] args)
  9.         {
  10.             var numbers = new int[] { 1, 2, 3, 1, 3, 2, 4, 2 };
  11.             Console.WriteLine("コレクションの要素は {{ {0} }} です。", string.Join(", ", numbers));
  12.  
  13.             var numberGroups = numbers.GroupBy(x => x);
  14.             foreach (var group in numberGroups)
  15.             {
  16.                 Console.WriteLine(group.Key + " が " + group.Count() + " 個あります。");
  17.             }
  18.  
  19.             Console.ReadKey();
  20.         }
  21.     }
  22. }


次は以下のような Person クラスでグループ化してみましょう。

Person.cs
  1. namespace Tips_Linq
  2. {
  3.     using System;
  4.  
  5.     /// 
  6.     /// 人物データを表します。
  7.     /// 
  8.     public class Person
  9.     {
  10.         /// 
  11.         /// 氏名を取得または設定します。
  12.         /// 
  13.         public string Name { get; set; }
  14.  
  15.         /// 
  16.         /// 更新日付を取得または設定します。
  17.         /// 
  18.         public DateTime Date { get; set; }
  19.     }
  20. }

例えば Date プロパティの年で分類分けする場合は次のようなコードになります。

Program.cs
  1. namespace Tips_Linq
  2. {
  3.     using System;
  4.     using System.Collections.Generic;
  5.     using System.Linq;
  6.  
  7.     class Program
  8.     {
  9.         static void Main(string[] args)
  10.         {
  11.             var people = GetPeople();
  12.             var groups = people.GroupBy(x => x.Date.Year);
  13.             foreach (var group in groups)
  14.             {
  15.                 Console.WriteLine("== {0} 年の人 =====", group.Key);
  16.                 foreach (var person in group)
  17.                 {
  18.                     Console.WriteLine(person.Name);
  19.                 }
  20.             }
  21.  
  22.             Console.ReadKey();
  23.         }
  24.  
  25.         /// 
  26.         /// 人物コレクションの列挙子を取得します。
  27.         /// 
  28.         /// 
  29.         static IEnumerable<Person> GetPeople()
  30.         {
  31.             yield return new Person() { Name = "田中 淳平", Date = new DateTime(2011, 5, 2) };
  32.             yield return new Person() { Name = "鈴木 ほのか", Date = new DateTime(2014, 3, 24) };
  33.             yield return new Person() { Name = "小池 哲司", Date = new DateTime(2015, 6, 13) };
  34.             yield return new Person() { Name = "恩田 進", Date = new DateTime(2011, 7, 28) };
  35.             yield return new Person() { Name = "中津山 亜希子", Date = new DateTime(2015, 9, 9) };
  36.         }
  37.     }
  38. }


グループ化されたあとは Key プロパティで分類の値を取得できます。

ところで、グループ化に指定するキーがカスタムクラスの場合、キーが一致しているかどうか評価するための比較子が必要となります。このことについて調べるために、Person クラスを次のように定義し直します。

Person.cs
  1. namespace Tips_Linq
  2. {
  3.     using System;
  4.  
  5.     /// 
  6.     /// 人物データを表します。
  7.     /// 
  8.     public class Person
  9.     {
  10.         /// 
  11.         /// 氏名を取得または設定します。
  12.         /// 
  13.         public string Name { get; set; }
  14.  
  15.         /// 
  16.         /// 更新日付を取得または設定します。
  17.         /// 
  18.         public DateTime Date { get; set; }
  19.  
  20.         /// 
  21.         /// 子どもを取得または設定します。
  22.         /// 
  23.         public Person Child { get; set; }
  24.     }
  25. }

Person クラスの中に Child という名前の Person クラスのプロパティを追加しました。この子どもの名前が一致しているかどうかで分類分けするために、次のような比較子を定義します。

PersonComparer.cs
  1. namespace Tips_Linq
  2. {
  3.     using System.Collections.Generic;
  4.  
  5.     /// 
  6.     /// Person クラスに対する等値比較子を表します。
  7.     /// 
  8.     public class PersonComparer : IEqualityComparer<Person>
  9.     {
  10.         public static readonly PersonComparer NameComparer = new PersonComparer();
  11.  
  12.         /// 
  13.         /// 指定された Person クラスのオブジェクトが等しいかどうかを確認します。
  14.         /// 
  15.         /// "x">比較基準を指定します。
  16.         /// "y">比較対象を指定します。
  17.         /// Name プロパティが等しい場合に true を返します。
  18.         public bool Equals(Person x, Person y)
  19.         {
  20.             if ((x == null) && (y == null)) return true;
  21.             if ((x == null) || (y == null)) return false;
  22.             return x.Name == y.Name;
  23.         }
  24.  
  25.         /// 
  26.         /// ハッシュ値を取得します。
  27.         /// 
  28.         /// "obj">ハッシュ値を算出するオブジェクトを指定します。
  29.         /// 算出したハッシュ値を返します。
  30.         public int GetHashCode(Person obj)
  31.         {
  32.             return obj.Name.GetHashCode();
  33.         }
  34.     }
  35. }

Child プロパティが null である場合も想定しているため、Equals() メソッド中で null 判定を入れています。

これらを使って GroupBy 拡張メソッドを使ってみましょう。

Program.cs
  1. namespace Tips_Linq
  2. {
  3.     using System;
  4.     using System.Collections.Generic;
  5.     using System.Linq;
  6.  
  7.     class Program
  8.     {
  9.         static void Main(string[] args)
  10.         {
  11.             var people = GetPeople();
  12.             var groups = people.GroupBy(x => x.Child, PersonComparer.NameComparer);
  13.             foreach (var group in groups)
  14.             {
  15.                 var name = group.Key != null ? group.Key.Name : "NULL";
  16.                 Console.WriteLine("== 子どもの名前が {0} の人 =====", name);
  17.                 foreach (var person in group)
  18.                 {
  19.                     Console.WriteLine(person.Name);
  20.                 }
  21.             }
  22.  
  23.             Console.ReadKey();
  24.         }
  25.  
  26.         /// 
  27.         /// 人物コレクションの列挙子を取得します。
  28.         /// 
  29.         /// 
  30.         static IEnumerable<Person> GetPeople()
  31.         {
  32.             yield return new Person() { Name = "田中 淳平", Date = new DateTime(2011, 5, 2), Child = new Person() { Name = "田中 清美" } };
  33.             yield return new Person() { Name = "鈴木 ほのか", Date = new DateTime(2014, 3, 24), Child = new Person() { Name = "田中 清美" } };
  34.             yield return new Person() { Name = "小池 哲司", Date = new DateTime(2015, 6, 13), Child = new Person() { Name = "小池 真司" } };
  35.             yield return new Person() { Name = "恩田 進", Date = new DateTime(2011, 7, 28) };
  36.             yield return new Person() { Name = "中津山 亜希子", Date = new DateTime(2015, 9, 9) };
  37.         }
  38.     }
  39. }


ちなみに GroupBy 拡張メソッドの第 2 引数を省略した場合は次のような結果になります。


「田中 清美」という名前は同じですが、インスタンスが異なるので分類としては同じになりません。裏を返せば、同じインスタンスで定義した Child プロパティであれば comparer を指定することなく分類することができます。分類分けするキーがプロパティ値なのかインスタンスなのかを理解した上で使いましょう。

GroupBy 拡張メソッドには他にもオーバーロードが用意されています。非常に複雑なので少し整理してみましょう。

入力引数にはそれぞれ keySelector、elementSeoector、resultSelector、comparer という名前が付いており、それぞれの処理は次のような順序になります。


elementSelector という入力引数は Func<TSource, TElement> という型の関数を指定します。

resultSelector へ渡すシーケンスを元のシーケンスから射影します。省略した場合は元の型と同じものがシーケンスとなって resultSelector へ渡されます。

resultSelector という入力引数は Func<TKey, IEnumerable<TSource>, TResult> という型の関数を指定します。これまでは resultSelector 入力引数を省略していたため、戻り値は各要素が IGroup<TSource, TSource> インターフェースのシーケンスでした。resultSelector 入力引数を使用することで分類分けしたキーを使用しながら戻り値の各要素を他の型に変換することができます。