{ }codelper

学ぶ › C#

言語ごとの特徴 · 39

LINQ とストリーム

絞り込み・変換・グループ化・並べ替えを、ループの代わりにひと続きのパイプラインで書きます。

この単元は Java · C# 向けです。
Python は内包表記で同じことをし(Python の単元を参照)、JavaScript・TypeScript は配列メソッド filter・map・reduce をつなぎ、C++20 には std::views::filter のようなレンジがあります。
C と Craykon はループで書きます。

C# の LINQ と Java のストリームは、「選ぶ・変える・まとめる・並べる・合わせる」の段階をメソッドでつないだパイプラインです。
下の例は単語のうち3文字より長いものだけを残し、長さごとにまとめて表示し、残った文字数をすべて足します。

using System;using System.Linq; class Program{    static void Main()    {        string[] words = Console.In.ReadToEnd()            .Split((char[])null, StringSplitOptions.RemoveEmptyEntries);         var longWords = words            .Where(w => w.Length > 3)            .ToList(); // 여기서 한 번 실행해 결과를 담아 둬요         var lines = longWords            .GroupBy(w => w.Length)            .OrderBy(g => g.Key)            .Select(g => $"{g.Key}: {string.Join(" ", g.Order())}");        foreach (string line in lines) Console.WriteLine(line);         int total = longWords.Sum(w => w.Length);        Console.WriteLine($"total letters: {total}");    }}
stdin
the quick brown fox jumps over the lazy dog quickly
stdout
4: lazy over
5: brown jumps quick
7: quickly
total letters: 30

ポイント

  • リストを扱う仕事はほとんどが、絞り込み・変換・グループ化・並べ替え・合計の組み合わせです。
    ループの代わりに、その段階を順に書きます。
  • 段階ごとに名前が対応しています。
    絞り込みは Where/filter、変換は Select/map のように。
  • パイプラインは作ったときではなく、結果を取り出すときに実行されます。
  • 結果を何度も使うなら、リストに入れておきます(ToList()/toList())。
  • グループ化の結果はキーとそのまとまりで出てきます。
    キーの順番が必要なら自分で並べ替えます。

言語ごとの書き方

Javaimport java.util.stream.*; — まずリストから words.stream() でストリームを取り出し、filter、map、sorted、Collectors.groupingBy、mapToInt(...).sum()、toList()(または collect(Collectors.toList()))。
groupingBy は既定が HashMap で順番が決まらないので、例では groupingBy(String::length, TreeMap::new, Collectors.toList()) で TreeMap を使わせます
C#using System.Linq; — Where(絞り込み)、Select(変換)、OrderBy・ThenBy(値そのもので並べるなら Order())、GroupBy、Sum、ToList()。
GroupBy は Key を持つグループを最初に出てきた順で返すので、例では OrderBy(g => g.Key) で並べます。
SQL のようなクエリ構文 from w in words where w.Length > 3 group w by w.Length も、コンパイルすると同じメソッド呼び出しになります

よくある間違い

LINQ のクエリを作ったあとで元を変えたC#

var q = nums.Where(n => n > 1);
nums.Add(5);
Console.WriteLine(q.Count());

→ q は結果ではなく「どう絞り込むか」だけを覚えています(遅延実行)。あとから入れた 5 まで数え、foreach や Count() のたびに最初から実行し直します

一度計算した結果を何度も使うなら ToList() で入れておきます。

同じストリームを2回使ったJava

Stream<String> s = words.stream();
s.forEach(System.out::println);
long n = s.count();

→ IllegalStateException: stream has already been operated upon or closed — ストリームの終端操作(forEach、collect、sum など)は一度しかできません

また使うデータはリストに集めておき、必要なたびに stream() を新しく取り出します。

数を Stream<Integer> で扱ったJava

int sum = nums.stream().reduce(0, Integer::sum);

→ 数の1つ1つが Integer オブジェクトに包まれて(ボクシング)遅く、sum() メソッドもありません

mapToInt で IntStream にすれば sum()・average()・max() がすぐ使えます(オブジェクトに戻すなら boxed())。
C# の IEnumerable<int> はボクシングしません。

言語ごとの違い
パイプラインは「取り出すとき」に実行されます。
C# のクエリは取り出すたびに実行し直され、Java のストリームは一度使えば終わりなので、何度も使う結果は ToList()/toList() でリストに入れておきましょう。

練習問題