【LINQ】GroupByを使ってリストを分類する

C#

こんにちは、働くC#プログラマーのさんさめです。

LINQ、使ってますか?

SelectやWhereは、
まだ直感的に使いやすい方ですが、
GroupByはどうでしょうか。

処理内容はなんとなく知ってるけど
使い時は全然分からないわ…

となっている方もいるかもしれませんね。
かくいう私も、
「あ、こういう時はGroupByを使おう」と
パッと頭の中の引き出しから
出せるようになったのは割と最近のことです。

本記事ではGroupByの処理内容を解説しつつ、
私さんさめがいったいどんな時に使っているか、
簡単な実例を交えながら紹介します。

さんさめ
さんさめ

GroupByを使いこなすと
驚くほどコードがスッキリする…
こともあるかも

スポンサーリンク

GroupByはリストを任意のキーでグルーピングするメソッド

GroupByは、
リストを任意のキーでグルーピングすることができる
LINQ拡張メソッドです。
そのキーはラムダ式で指定できます。

例えば次のように使います。

var list = new[] { 1, 2, 3, 4, 5 };
var groupByResult = list.GroupBy(x => x % 2);
foreach (var group in groupByResult)
{
    Console.WriteLine($"2で割った余りが{group.Key}の数は");
    Console.WriteLine(string.Join(", ", group));
}
実行結果

戻り値の型がやや特殊で、
WhereやSelectなど他の一般的なLINQメソッドと異なり、
ただのIEnumerable<T>ではなく、
IEnumerable<IGrouping<TKey, TSource>>
となります。

え?なんだって??

型だけ見て一発で理解できる人はいないので
安心してください。

順を追って説明します。

まず、グルーピングしたいキーをラムダで指定しています。
「x => x % 2」の部分ですね。

つまり、
「『2で割った余り』で1~5の整数をグルーピングしろ」
と言っていることになります。

要は奇数か偶数かなので、
5つの整数は2つにグルーピングされることになります。

そして、最初のforeachでは、
この2つのグループが列挙されます。

foreach (var group in groupByResult)

そして、列挙された各要素には
Keyというプロパティがあり、
グルーピングに用いられたキーを取得することができます。

Console.WriteLine($"2で割った余りが{group.Key}の数は");

余りが1のグループのキーは1になりますし、
余りが0のグループのキーは0になります。

グルーピングされた値そのものは、
列挙された要素をさらに列挙することで取得できます。

Console.WriteLine(string.Join(", ", group));

以上がGroupByを使うことで起きていることです。

実はオーバーロードが沢山あるのですが、
ややこしい割にあまり使い勝手に差が無いので
このGroupByだけ覚えておけば大丈夫です。

実例

GroupByの処理内容は掴めましたでしょうか。

分かったような、
分からないような…
これ実際いつ使うの?

そうですね。
使ってみないと分からない上に
使いどころが分かりにくいことが
GroupByを
覚えにくい要因の1つかもしれません。

さて、GroupByは一般に、
上記のようなサンプルではなく、
自作クラスを用いて、
解説されることが多いメソッドです。

そのため私も最初、
「でも自作クラスを分類したい時なんて
そんな無いよなぁ」と想像してしまい、
あまり使っていませんでした。

しかし、ラムダでキーを指定できることを意識したり、
グループ化した後にもLINQを使うなどすると
実は非常に味のあるメソッドということに気づかされます。

例えば、次のような使い方があります。

  • ファイルの中身でグルーピングして重複データを探す
  • FirstOrDefaultと組み合わせてDistinctの代わりに使う
  • 拡張子でグルーピングして後段の処理を変える

掘り下げてみていきましょう。

ファイルの中身でグルーピングして重複データを探す

File.ReadAllTextというメソッドがあります。

指定したテキストファイルを開き、
文字列として読み取るメソッドです。

この結果をキーとしてGroupByを使うことで、
大量に作成されたデータの中から、
中身が重複しているファイルをあぶりだし、
データ整理に役立てました。

var files = Directory.GetFiles(
    @"D:\Users\threeshark", " *.txt");
var fileGroups = files.GroupBy(x => File.ReadAllText(x));
foreach (var fileGroup in fileGroups)
{
    Console.WriteLine("-----");
    // 重複しているファイルパスを列挙
    foreach (var path in fileGroup)
    {
        Console.WriteLine(path);
    }
}

FirstOrDefaultと組み合わせてDistinctの代わりに使う

次のようなコードを書くことで、
【LINQ】任意のクラスでDistinctを使う方法
のようなことができます。

var card = new Card() { Mark = "Spade", Number = 1 };
var card2 = new Card() { Mark = "Spade", Number = 8 };
var list = new[] { card, card2 };
var distinctResult = list
    .GroupBy(x => x.Mark) // Markでグルーピングして
    .Select(x => x.FirstOrDefault()); // Selectで先頭の要素だけに変換
Console.WriteLine(distinctResult.Count()); // 結果は1になる

キーを指定してグルーピングした後に、
各グループに対してFirstOrDefaultで先頭の要素を取り出すことで、
まるで重複解消したかのような振る舞いをさせることができます。

さんさめ
さんさめ

私もDistinct代わりによく使ってました

拡張子でグルーピングして後段の処理を変える

Path.GetExtensionを使って、
ファイルパスのリストを拡張子でグルーピングする
という例です。

var list = new[]
{
    "a.txt",
    "b.txt",
    "c.png",
    "d.mp4",
    "e.png",
};
var fileGroups = list.GroupBy(x => Path.GetExtension(x));
foreach (var fileGroup in fileGroups)
{
    switch (fileGroup.Key)
    {
        case ".png":
        case ".jpg":
            // 画像形式のファイル用処理
            break;
        case ".txt":
        case ".json":
            // テキスト形式のファイル用処理
            break;
        default:
            break;
    }
}

ファイル操作という点で、
1つ目の例と少し似ていますね。

このように、
単純な組み込み型のリストでも
GroupByを用いることで
意外なコードがスッキリ書けることがあります。

GroupBy、ぜひ使ってみてください。

まとめ

まとめです。

  • GroupByはリストをグルーピングするためのメソッド
  • 戻り値が少々複雑で列挙を2回行う必要がある
  • 3つのGroupBy活用事例を紹介

最後までお読みいただきありがとうございました。

コメント

タイトルとURLをコピーしました