{ }codelper

学习 › C#

各语言特有 · 39

LINQ 与流

把筛选、转换、分组、排序连成一条管道,代替手写循环。

本单元适用于 Java · C#。
Python 用推导式做同样的事(见 Python 一课),JavaScript 和 TypeScript 串联数组方法 filter、map、reduce,C++20 有 std::views::filter 这样的范围(ranges)。
C 和 Craykon 用循环来写。

C# 的 LINQ 和 Java 的流把“筛选、转换、分组、排序、汇总”这些步骤用方法串起来,写成一条流水线。
下面的示例只留下长于三个字母的单词,按长度分组输出,再把剩下单词的字母数加起来。

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() 存起来。

同一个流用了两次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> 不装箱。

各语言的差异
流水线在“取出结果时”才执行。
C# 查询每次取出都会重新执行,Java 流用一次就结束,所以要多次使用的结果请用 ToList()/toList() 存进列表。

练习题