言語ごとの特徴 · 39
LINQ とストリーム
絞り込み・変換・グループ化・並べ替えを、ループの代わりにひと続きのパイプラインで書きます。
この単元は Java · C# 向けです。
Python は内包表記で同じことをし(Python の単元を参照)、JavaScript・TypeScript は配列メソッド filter・map・reduce をつなぎ、C++20 には std::views::filter のようなレンジがあります。
C と Craykon はループで書きます。
C# の LINQ と Java のストリームは、「選ぶ・変える・まとめる・並べる・合わせる」の段階をメソッドでつないだパイプラインです。
下の例は単語のうち3文字より長いものだけを残し、長さごとにまとめて表示し、残った文字数をすべて足します。
import java.util.*;import java.util.stream.*; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); List<String> words = new ArrayList<>(); while (sc.hasNext()) words.add(sc.next()); List<String> longWords = words.stream() .filter(w -> w.length() > 3) .toList(); // 스트림은 한 번만 쓸 수 있어서 결과를 목록으로 담아 둬요 Map<Integer, List<String>> byLength = longWords.stream() .collect(Collectors.groupingBy(String::length, TreeMap::new, Collectors.toList())); byLength.entrySet().stream() .map(e -> e.getKey() + ": " + e.getValue().stream().sorted().collect(Collectors.joining(" "))) .forEach(System.out::println); int total = longWords.stream().mapToInt(String::length).sum(); System.out.println("total letters: " + total); }}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}"); }}the quick brown fox jumps over the lazy dog quickly
4: lazy over 5: brown jumps quick 7: quickly total letters: 30
ポイント
- リストを扱う仕事はほとんどが、絞り込み・変換・グループ化・並べ替え・合計の組み合わせです。
ループの代わりに、その段階を順に書きます。 - 段階ごとに名前が対応しています。
絞り込みはWhere/filter、変換はSelect/mapのように。 - パイプラインは作ったときではなく、結果を取り出すときに実行されます。
- 結果を何度も使うなら、リストに入れておきます(
ToList()/toList())。 - グループ化の結果はキーとそのまとまりで出てきます。
キーの順番が必要なら自分で並べ替えます。
言語ごとの書き方
| Java | import 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 も、コンパイルすると同じメソッド呼び出しになります |
よくある間違い
同じストリームを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> はボクシングしません。
LINQ のクエリを作ったあとで元を変えたC#
var q = nums.Where(n => n > 1);
nums.Add(5);
Console.WriteLine(q.Count());→ q は結果ではなく「どう絞り込むか」だけを覚えています(遅延実行)。あとから入れた 5 まで数え、foreach や Count() のたびに最初から実行し直します
一度計算した結果を何度も使うなら ToList() で入れておきます。
パイプラインは「取り出すとき」に実行されます。
C# のクエリは取り出すたびに実行し直され、Java のストリームは一度使えば終わりなので、何度も使う結果は
ToList()/toList() でリストに入れておきましょう。