各语言特有 · 39
LINQ 与流
把筛选、转换、分组、排序连成一条管道,代替手写循环。
本单元适用于 Java · C#。
Python 用推导式做同样的事(见 Python 一课),JavaScript 和 TypeScript 串联数组方法 filter、map、reduce,C++20 有 std::views::filter 这样的范围(ranges)。
C 和 Craykon 用循环来写。
C# 的 LINQ 和 Java 的流把“筛选、转换、分组、排序、汇总”这些步骤用方法串起来,写成一条流水线。
下面的示例只留下长于三个字母的单词,按长度分组输出,再把剩下单词的字母数加起来。
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 编译后也是同样的方法调用 |
常见错误
同一个流用了两次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);→ 每个数字都被包装成 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 流用一次就结束,所以要多次使用的结果请用
流水线在“取出结果时”才执行。
C# 查询每次取出都会重新执行,Java 流用一次就结束,所以要多次使用的结果请用
ToList()/toList() 存进列表。