こんにちは、働く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活用事例を紹介
最後までお読みいただきありがとうございました。
コメント