迭代器
Iterator抽象、适配器与消费器全表、自定义迭代器与「边遍历边改」。
迭代器:Iterator 与 IntoIterator
Iterator trait 的形状
text
// std::iter::Iterator 的核心(简化)
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// …… 70+ 个默认方法(map/filter/fold/...)
}三件事必须一次记住:
next(&mut self):迭代是可变状态推进,所以迭代器变量要能改(for帮你处理了);Option<Self::Item>:None表示结束,因此「空迭代器」和「已耗尽」是同一个表示;Item是关联类型:每个迭代器只产出一种元素类型,编译期完全确定。
rust
fn main() {
let v = vec![1, 2, 3];
let mut it = v.iter(); // Iter<'_, i32>,Item = &i32
assert_eq!(it.next(), Some(&1));
assert_eq!(it.next(), Some(&2));
assert_eq!(it.next(), Some(&3));
assert_eq!(it.next(), None);
assert_eq!(it.next(), None); // 耗尽后再调还是 None(约定:耗尽后必须一直返回 None)
}for 循环的脱糖(desugaring)是理解一切的关键:
text
// for x in thing { body }
// 展开为:
{
let mut iter = IntoIterator::into_iter(thing);
loop {
match Iterator::next(&mut iter) {
Some(x) => body,
None => break,
}
}
}IntoIterator 与三种迭代方式
for 循环要的不是 Iterator,而是 IntoIterator(「能变成迭代器」):
text
trait IntoIterator {
type Item;
type IntoIter: Iterator<Item = Self::Item>;
fn into_iter(self) -> Self::IntoIter;
}对 Vec<T> 而言存在三个实现,构成三种迭代方式;for 的写法与方法调用是一一对应的:
for 写法 | 等价的方法链 | Item | 是否移动容器 | 元素可否修改 |
|---|---|---|---|---|
for x in &v | for x in v.iter() | &T | 否(共享借用) | ❌ 只能读 |
for x in &mut v | for x in v.iter_mut() | &mut T | 否(独占借用) | ✅ 就地改 |
for x in v | for x in v.into_iter() | T | 是(v 被消费) | ✅ 拿到所有权 |
rust
fn main() {
let mut v = vec![String::from("a"), String::from("b")];
for s in &v { // Item = &String
println!("读: {s}");
}
for s in &mut v { // Item = &mut String
s.push('!'); // 就地修改,不改变长度
}
println!("{v:?}"); // 输出:["a!", "b!"]
for s in v { // Item = String,v 被移动
println!("拥有: {s}");
}
// println!("{v:?}"); // E0382:v 已经被 into_iter 消费
}其他常见集体的三种方式:
| 类型 | &x 的 Item | &mut x 的 Item | x 的 Item |
|---|---|---|---|
Vec<T> / [T; N] | &T | &mut T | T |
HashMap<K, V> | (&K, &V) | (&K, &mut V) | (K, V) |
HashSet<T> / BTreeSet<T> | &T | ❌ 无 &mut 实现 | T |
String | ❌ | ❌ | char |
&str | ❌ | ❌ | char |
BTreeMap<K, V> | (&K, &V) | (&K, &mut V) | (K, V) |
⚠️ 陷阱:
String和&str没有&String/&&str的IntoIterator:for c in &s编译失败。想遍历字符写for c in s.chars()(借用)或for c in s.chars()。
🧠 原理:
v.iter()之所以存在,是因为Vec<T>通过Deref拿到[T]的iter方法 ——Vec自己没定义iter。(&v).into_iter()与v.iter()结果完全一样(标准库的impl IntoIterator for &Vec<T>内部就是self.iter()), 但&v形式更简短,且能用于任何IntoIterator的场合(如函数参数impl IntoIterator)。
惰性求值:适配器不执行,直到被消费
迭代器适配器(iterator adapter)返回的是一个新的迭代器,本身什么都不做。
rust
fn main() {
let v = vec![1, 2, 3];
let pipeline = v.iter().map(|x| {
println!("map 被调用: {x}");
x * 2
});
println!("—— 到这里为止,一行 map 都没执行 ——");
let total: i32 = pipeline.sum(); // 消费时才逐元素驱动
println!("total = {total}");
}输出:
—— 到这里为止,一行 map 都没执行 —— map 被调用: 1 map 被调用: 2 map 被调用: 3 total = 12
这个性质叫惰性求值(lazy evaluation),带来两个直接好处:
- 没有中间容器:
v.iter().map(f).filter(g).sum()不会先建Vec再建Vec再求和, 而是一个循环走完(对比 JS 的arr.map(f).filter(g)会创建两个新数组)。 - 可以表达无限序列:
(1..).filter(|x| x % 3 == 0).take(5)是安全的, 因为take(5)会在取够后停止驱动整个链。
rust
fn main() {
let first_five: Vec<u32> = (1u32..).filter(|x| x % 3 == 0).take(5).collect();
println!("{first_five:?}"); // 输出:[3, 6, 9, 12, 15]
// 反面:无限迭代器 + 立即消费 = 永不结束
// let boom: Vec<u32> = (1u32..).collect(); // 会一直分配内存直到 OOM
}⚠️ 陷阱:迭代器适配器不执行,所以「忘了消费」是静默 bug:
v.iter().map(|x| do_something(x));编译会有unused_must_use警告 (iterator adapters 标了#[must_use]),但警告不等于错误 —— 请把cargo clippy -D warnings加进 CI。 需要「立即执行副作用」的场合就用for_each(消费器,见「迭代器消费器全表」)。
迭代器 vs 索引循环:为什么迭代器常常更快
rust
fn sum_index(v: &[i32]) -> i64 {
let mut acc = 0i64;
for i in 0..v.len() {
acc += v[i] as i64; // 每次索引都要做一次「i < len」边界检查
}
acc
}
fn sum_iter(v: &[i32]) -> i64 {
v.iter().map(|&x| x as i64).sum() // 迭代器把「游标 + 末尾指针」交给优化器
}
fn main() {
let v: Vec<i32> = (0..100).collect();
assert_eq!(sum_index(&v), sum_iter(&v));
println!("{}", sum_iter(&v)); // 输出:4950
}两者通常生成同样的机器码,但迭代器版本更容易保持在最优点:
- 边界检查消除(bounds check elision):
v[i]的检查是运行期分支, LLVM 需要证明i < len才能删掉它。slice::Iter内部维护的「当前指针 < 末尾指针」 是循环条件本身,检查因此不存在(不是被删掉,而是根本不需要)。 索引循环里 LLVM 也常能推导出来,但一旦循环体里有函数调用、提前break、 或索引表达式复杂化,推导就可能失败。 - 不需要重新加载长度:
for i in 0..v.len()里v.len()只在循环外算一次, 但如果有v.push/v.truncate这类可能改变长度的调用混在里面, 编译器就必须保守地重新加载len并重做检查;迭代器借用整个切片,结构上不可能改长度。 - 用
zip对齐两个切片:for i in 0..a.len() { a[i] + b[i] }每次要做两个检查, 而a.iter().zip(&b)只比较两个指针,检查为 0 次,且天然处理长度不等。 - 单态化(monomorphization):泛型的
map/filter会为每个具体闭包类型 生成专用代码,闭包被内联,整条链融成一个循环,没有虚函数调用、没有装箱。 这是「零成本抽象(zero-cost abstraction)」在迭代器上最直观的体现。
v.iter().map(f).filter(g).sum::<i32>()
你写的: iter ──► map(f) ──► filter(g) ──► sum()
编译后: ┌───────────────────────────────────┐
│ 一个循环: 取元素 → 内联 f → 内联 g → 累加 │
└───────────────────────────────────┘
(没有中间的 Iter/Map/Filter 对象,没有分配,
没有动态分派 —— 全部被内联并优化掉)🧠 原理:「零成本」指的是运行期零成本,代价在编译期: 每种
Iterator + 闭包组合都会生成一份代码,链越长、用到的类型越多, 编译时间与二进制体积越大。若代码体积敏感,可以用Box<dyn Iterator<Item = T>>换回动态分派(代价:每次next一次间接调用、无法内联)。
🚀 进阶:
collect()能一次分配精确容量,是因为标准库为切片/range 等迭代器 保留了精确的size_hint(内部用TrustedLen之类的机制做特化)。 自定义迭代器如实实现size_hint,你的collect也会自动受益(见「自定义迭代器」)。
迭代器适配器全表
适配器(adapter)= 接收迭代器、返回迭代器的方法。它们都是惰性的。
| 适配器 | 作用 | 短示例 |
|---|---|---|
map | 一对一变换,元素个数不变 | (1..=3).map(|x| x * 2) → 2, 4, 6 |
filter | 保留谓词为真的元素 | (1..=5).filter(|x| x % 2 == 1) → 1, 3, 5 |
filter_map | 「变换 + 过滤」合一:闭包返回 Option,None 被丢弃 | ["1","x"].iter().filter_map(|s| s.parse::<i32>().ok()) → 1 |
map_while | 类似 map,但遇到 None 就永久停止(1.57 起) | [1,2,0,3].iter().map_while(|x| 10i32.checked_div(*x)) → 10, 5 |
flat_map | 每个元素展开成一个迭代器,再首尾相接 | vec![vec![1,2], vec![3]].into_iter().flat_map(|v| v) → 1, 2, 3 |
flatten | 摊平「迭代器的迭代器」,等价于 flat_map(|x| x);对 Option/Result 同样有效 | [Some(1), None, Some(3)].into_iter().flatten() → 1, 3 |
take | 最多取前 n 个,不足则提前结束 | (1..).take(3) → 1, 2, 3 |
take_while | 取到谓词首次为假为止(之后即使为真也不再取) | [1,2,9,1].into_iter().take_while(|&x| x < 5) → 1, 2 |
skip | 跳过前 n 个 | [1,2,3,4].into_iter().skip(2) → 3, 4 |
skip_while | 跳过开头满足谓词的部分(中间的不跳) | [0,0,1,0].into_iter().skip_while(|&x| x == 0) → 1, 0 |
chain | 两个迭代器首尾相接 | [1,2].into_iter().chain([3,4]) → 1, 2, 3, 4 |
zip | 配对推进,任一耗尽即停(不会 panic) | [1,2,3].into_iter().zip("ab".chars()) → (1,'a'), (2,'b') |
enumerate | 附带从 0 开始的下标 | "ab".chars().enumerate() → (0,'a'), (1,'b') |
peekable | 允许 peek() 预看下一个而不消费 | let mut it = "ab".chars().peekable(); it.peek() → Some(&'a') |
rev | 反向迭代(要求 DoubleEndedIterator) | [1,2,3].into_iter().rev() → 3, 2, 1 |
cloned | &T → T,要求 T: Clone | vec![String::from("a")].iter().cloned() → String |
copied | &T → T,要求 T: Copy(比 cloned 更省) | [1,2,3].iter().copied() → 1, 2, 3 |
step_by | 每 n 个取 1 个(n == 0 panic) | (0..10).step_by(3) → 0, 3, 6, 9 |
inspect | 旁路观察每个元素,不改变元素 | (1..=3).inspect(|x| println!("看: {x}")) |
scan | 带可变状态的有状态 map;返回 None 可提前结束 | (1..=4).scan(0, |acc, x| { *acc += x; Some(*acc) }) → 1, 3, 6, 10 |
cycle | 无限重复整个迭代器(要求 Self: Clone) | [1,2].into_iter().cycle().take(5) → 1, 2, 1, 2, 1 |
by_ref | 借用迭代器而非消费它,便于「先用一部分,再继续用」 | let head: Vec<_> = it.by_ref().take(2).collect(); |
slice::windows(n) | 切片方法:长度固定为 n 的滑动窗口 | [1,2,3].windows(2) → [1,2], [2,3] |
slice::chunks(n) | 切片方法:每 n 个一组,最后一组可能更短 | [1,2,3,4,5].chunks(2) → [1,2], [3,4], [5] |
⚠️ 陷阱:
windows/chunks不是迭代器适配器,而是[T]的方法。 它们返回的Windows/Chunks确实是迭代器,但只能从切片上调用 (所以v.windows(2)走的是Deref到[T]),不能写成v.iter().windows(2)。
几个容易混的适配器,展开看差别:
rust
fn main() {
let data = [1, 2, 9, 1, 2];
// filter:整个序列都检查,9 后面的 1、2 仍会被保留
let f: Vec<i32> = data.iter().copied().filter(|&x| x < 5).collect();
println!("{f:?}"); // 输出:[1, 2, 1, 2]
// take_while:9 一出现就永久停止
let t: Vec<i32> = data.iter().copied().take_while(|&x| x < 5).collect();
println!("{t:?}"); // 输出:[1, 2]
// skip_while:只跳过开头连续的 0,中间的不跳
let s: Vec<i32> = [0, 0, 1, 0].into_iter().skip_while(|&x| x == 0).collect();
println!("{s:?}"); // 输出:[1, 0]
// flat_map vs flatten:flatten 是 flat_map 的恒等特化
let fm: Vec<i32> = vec![vec![1, 2], vec![3]].into_iter().flat_map(|v| v).collect();
let fl: Vec<i32> = vec![vec![1, 2], vec![3]].into_iter().flatten().collect();
println!("{fm:?} == {fl:?}"); // 输出:[1, 2, 3] == [1, 2, 3]
// flatten 对 Option 的妙用:一次丢掉所有 None 并取出值
let oks: Vec<i32> = ["1", "x", "3"].iter().map(|s| s.parse::<i32>().ok()).flatten().collect();
println!("{oks:?}"); // 输出:[1, 3]
}peekable / scan / inspect 各来一个完整例子:
rust
use std::iter::Peekable;
use std::str::Chars;
// peekable:需要「看一个再决定要不要消费」时(词法分析的经典形状)
fn count_doubled(text: &str) -> usize {
let mut it: Peekable<Chars<'_>> = text.chars().peekable();
let mut count = 0;
while let Some(c) = it.next() {
if it.peek() == Some(&c) {
count += 1;
it.next(); // 把重复的那个吃掉,避免 "aaa" 数成 2 次
}
}
count
}
fn main() {
println!("{}", count_doubled("aabbc")); // 输出:2
println!("{}", count_doubled("aaa")); // 输出:1
// scan:需要「有状态 + 可提前终止」时
let running: Vec<i32> = (1..=5).scan(0, |acc, x| {
*acc += x;
if *acc > 10 { None } else { Some(*acc) } // 返回 None 之后迭代器永久结束
}).collect();
println!("{running:?}"); // 输出:[1, 3, 6, 10]
// inspect:调试管道时打点,不影响数据流
let total: i32 = (1..=4)
.inspect(|x| println!("进入管道: {x}"))
.filter(|x| x % 2 == 0)
.inspect(|x| println!("通过 filter: {x}"))
.sum();
println!("total = {total}"); // 输出:6(进入 1~4,通过 2、4)
}💡 对照:Python 3 的
map/filter也是惰性的,但没有方法链(要嵌成filter(g, map(f, xs))或生成器表达式),且每个元素都是 Python 对象、每次调用都有 解释器开销。JS 的Array.prototype.map/filter是立即求值且创建新数组。 Java 的Stream惰性、也有方法链,最接近 Rust;但 Java 的流操作是 接口上的虚调用,Rust 的单态化能做到完全内联 —— 这就是「Rust 迭代器和手写循环一样快」 而 Java Stream 常常比手写循环慢的来源。
迭代器消费器全表
消费器(consumer)= 驱动迭代器并产出最终结果的方法。调用消费器才会真正开始循环。
| 消费器 | 作用 | 短示例 / 返回类型 |
|---|---|---|
collect | 收集成任意 FromIterator 类型 | (1..=3).collect::<Vec<_>>() → vec![1,2,3] |
collect::<Result<Vec<_>, _>>() | 短路收集:遇到第一个 Err 立即返回它 | ["1","x"].iter().map(|s| s.parse::<i32>()).collect::<Result<Vec<i32>,_>>() → Err(..) |
collect::<String>() | 把 char / &str 拼成 String | "abc".chars().collect::<String>() → "abc" |
sum / product | 求和 / 求积(要求 T: Sum / T: Product) | (1..=4).sum::<i32>() → 10;(1..=4).product::<i32>() → 24 |
count | 消耗整个迭代器,返回元素个数 | "a b c".split(' ').count() → 3 |
min / max | 最小 / 最大(要求 Ord),返回 Option<Item> | [3,1,2].iter().max() → Some(&3) |
min_by_key / max_by_key | 按 key 比较,返回 Option<Item> | ["aa","b"].iter().min_by_key(|s| s.len()) → Some(&"b") |
min_by / max_by | 自定义比较(返回 Ordering) | v.iter().max_by(|a, b| a.len().cmp(&b.len())) |
fold | 显式携带累加器,最通用 | (1..=3).fold(0, |a, x| a + x) → 6 |
reduce | 以第一个元素为初值的 fold,返回 Option | (1..=3).reduce(|a, x| a + x) → Some(6);空迭代器给 None |
try_fold | 可提前返回 Err(或 None)的 fold | 见下方示例 |
any / all | 短路布尔判断 | (1..5).any(|x| x == 3) → true;(1..5).all(|x| x > 0) → true |
find | 第一个满足条件的元素 | [1,2,3].iter().find(|&&x| x > 1) → Some(&2) |
position | 第一个满足条件的下标 | [1,2,3].iter().position(|&x| x == 2) → Some(1) |
rposition / rfind | 从右往左找(要求 DoubleEndedIterator) | 从末尾方向找 |
for_each | 终结式遍历,等价于 for 循环 | v.iter().for_each(|x| print!("{x}")) |
last | 最后一个元素(会遍历全部) | (1..=3).last() → Some(3) |
nth | 跳过并取第 n 个(消费前 n+1 个) | (10..).nth(2) → Some(12) |
partition | 一次遍历分进两个集合 | (1..=5).partition::<Vec<_>, _>(|x| x % 2 == 0) → ([2,4], [1,3,5]) |
unzip | (A, B) 流拆成两个集合 | [(1,'a'),(2,'b')].into_iter().unzip::<i32,char,Vec<_>,String>() |
is_sorted / is_sorted_by | 是否已排序(1.82 起) | [1,2,3].iter().is_sorted() → true |
⚠️ 陷阱:
min_by_key/max_by_key在多个元素 key 相同时的取舍不对称:min_by_key返回第一个最小值,max_by_key返回最后一个最大值。 要绝对确定就自己写min_by/max_by,或先enumerate带上序号。
⚠️ 陷阱:
sum::<i32>()在 debug 构建下溢出会 panic(attempt to add with overflow), 在 release 构建下静默回绕(wrapping)。想两种构建行为一致,用fold(0i64, |a, x| a + x as i64)提升到i64,或显式用checked_sum风格(try_fold+checked_add)。
collect 的三种重要用法:
rust
fn main() {
// 1) 普通收集:需要能推断出目标类型,否则 E0283/E0282
let squares: Vec<i32> = (1..=4).map(|x| x * x).collect();
println!("{squares:?}"); // 输出:[1, 4, 9, 16]
// 2) Result 收集(短路):任一失败就整体失败
let good = ["1", "2", "3"];
let mixed = ["1", "oops", "3"];
let a: Result<Vec<i32>, _> = good.iter().map(|s| s.parse::<i32>()).collect();
let b: Result<Vec<i32>, _> = mixed.iter().map(|s| s.parse::<i32>()).collect();
println!("{a:?}"); // 输出:Ok([1, 2, 3])
println!("{:?}", b.map_err(|e| e.to_string()));
// 输出:Err("invalid digit found in string")
// 3) 拼成 String / HashMap / HashSet,都是同一个 collect
let joined: String = ["a", "b", "c"].iter().copied().collect();
let set: std::collections::HashSet<char> = "hello".chars().collect();
println!("{joined} / {}", set.len()); // 输出:abc / 4
}fold / reduce / try_fold 的层次关系:
rust
fn main() {
// fold:任意初值 + 任意累加器类型(可以与 Item 不同)
let csv: String = ["a", "b", "c"]
.iter()
.fold(String::new(), |mut acc, s| {
if !acc.is_empty() { acc.push(','); }
acc.push_str(s);
acc
});
println!("{csv}"); // 输出:a,b,c
// reduce:要求 Item 就是累加器类型,空迭代器返回 None
let max_even: Option<i32> = (1..=10).filter(|x| x % 2 == 0).reduce(|a, b| a.max(b));
println!("{max_even:?}"); // 输出:Some(10)
let nothing: Option<i32> = std::iter::empty::<i32>().reduce(|a, b| a + b);
println!("{nothing:?}"); // 输出:None
// try_fold:累加函数返回 Result/Option,出现「失败」立即短路
let parsed: Result<Vec<i32>, std::num::ParseIntError> = ["1", "2", "3"]
.iter()
.try_fold(Vec::new(), |mut acc, s| {
acc.push(s.parse::<i32>()?); // ? 在闭包里短路整个 try_fold
Ok(acc)
});
println!("{parsed:?}"); // 输出:Ok([1, 2, 3])
}partition / unzip 一次遍历分流:
rust
fn main() {
let (evens, odds): (Vec<i32>, Vec<i32>) = (1..=6).partition(|x| x % 2 == 0);
println!("{evens:?} / {odds:?}"); // 输出:[2, 4, 6] / [1, 3, 5]
let pairs = [(1, 'a'), (2, 'b'), (3, 'c')];
let (nums, letters): (Vec<i32>, String) = pairs.into_iter().unzip();
println!("{nums:?} / {letters}"); // 输出:[1, 2, 3] / abc
}自定义迭代器
实现 Iterator for Counter(含 size_hint)
rust
/// 依次产出 1, 2, 3, 4, 5,然后结束。
struct Counter {
count: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Self {
Counter { count: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None // 一旦返回 None,之后必须一直是 None
}
}
/// 上下界都精确:collect 能一次分配到位,take/skip 也能走快路径
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = (self.max - self.count) as usize;
(remaining, Some(remaining))
}
}
fn main() {
// 自定义迭代器自动获得全部适配器与消费器(默认方法)
let doubled: Vec<u32> = Counter::new(5).map(|x| x * 2).collect();
println!("{doubled:?}"); // 输出:[2, 4, 6, 8, 10]
// Iterator 有 blanket impl:impl<I: Iterator> IntoIterator for I,
// 所以自定义迭代器直接就能用 for 循环
let mut sum = 0;
for x in Counter::new(4) {
sum += x;
}
println!("{sum}"); // 输出:10
let mut it = Counter::new(5);
println!("{:?}", it.size_hint()); // 输出:(5, Some(5))
it.next();
println!("{:?}", it.size_hint()); // 输出:(4, Some(4))
println!("{}", Counter::new(5).count()); // 输出:5
println!("{:?}", Counter::new(5).zip("abc".chars()).collect::<Vec<_>>());
// 输出:[(1, 'a'), (2, 'b'), (3, 'c')]
}关于 size_hint 的契约(必须准确,否则是逻辑 bug):
- 返回值是
(下界, 上界),表示next()还能产出多少个元素; - 下界必须 ≤ 真实数量,上界必须 ≥ 真实数量(估计过小/过大都是错误);
- 上界是
None表示「不知道上限」(比如无限迭代器); - 默认实现是
(0, None)—— 保守但会让collect失去预分配优化; - 耗尽后(
next返回None)必须返回(0, Some(0)),否则collect可能分配出错误容量。
⚠️ 陷阱:不要为了性能在
size_hint里「撒小谎」。上界报小了会让标准库 依赖它的优化(如Vec::with_capacity后的unsafe写入路径)产生未定义行为; 这是安全相关的契约,不是提示。
实现 IntoIterator for &MyCollection
Iterator 是「自己能被推进」,IntoIterator 是「能被 for 消费」。 自定义容器要支持三种遍历,就要写三个 impl:
rust
struct Playlist {
tracks: Vec<String>,
}
impl Playlist {
fn new(tracks: &[&str]) -> Self {
Playlist { tracks: tracks.iter().map(|s| s.to_string()).collect() }
}
}
// &Playlist:for t in &playlist(Item = &String)
impl<'a> IntoIterator for &'a Playlist {
type Item = &'a String;
type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.tracks.iter()
}
}
// &mut Playlist:for t in &mut playlist(Item = &mut String)
impl<'a> IntoIterator for &'a mut Playlist {
type Item = &'a mut String;
type IntoIter = std::slice::IterMut<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.tracks.iter_mut()
}
}
// Playlist:for t in playlist(Item = String,容器被消费)
impl IntoIterator for Playlist {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.tracks.into_iter()
}
}
fn main() {
let mut pl = Playlist::new(&["intro", "verse", "outro"]);
for t in &pl {
println!("读: {t}");
}
for t in &mut pl {
t.push_str(" (live)");
}
for t in &pl {
println!("改后: {t}"); // 输出:intro (live) / verse (live) / outro (live)
}
let owned: Vec<String> = pl.into_iter().collect(); // pl 被消费
println!("{}", owned.len()); // 输出:3
}🧠 原理:
for x in &collection之所以能用,是因为存在impl IntoIterator for &Playlist。标准库对Vec/HashMap等都预置了这三个实现, 所以你的自定义容器也应该提供三个,用户才感觉「和内置集合一样」。
impl Iterator<Item = ...> 作为返回类型
不想为每个管道写一个命名结构体,就直接返回 impl Iterator:
rust
// edition 2024:不写 + '_ 也合法(RPIT 默认捕获所有在作用域内的生命周期)
fn evens(values: &[i32]) -> impl Iterator<Item = i32> {
values.iter().copied().filter(|x| x % 2 == 0)
}
// 返回借用型迭代器:生命周期在 2024 里自动被捕获
fn words(text: &str) -> impl Iterator<Item = &str> {
text.split_whitespace()
}
// 需要「根据条件返回两种不同迭代器」时,要么用 Box<dyn Iterator>,要么用 Either 风格
fn numbers(only_positive: bool, data: &[i32]) -> Box<dyn Iterator<Item = i32> + '_> {
if only_positive {
Box::new(data.iter().copied().filter(|x| *x > 0))
} else {
Box::new(data.iter().copied())
}
}
fn main() {
println!("{:?}", evens(&[1, 2, 3, 4, 5]).collect::<Vec<_>>()); // 输出:[2, 4]
println!("{:?}", words("a b c").collect::<Vec<_>>()); // 输出:["a", "b", "c"]
println!("{:?}", numbers(true, &[-1, 2]).collect::<Vec<_>>()); // 输出:[2]
println!("{:?}", numbers(false, &[-1, 2]).collect::<Vec<_>>()); // 输出:[-1, 2]
}| 返回迭代器的三种方式 | 优点 | 缺点 |
|---|---|---|
impl Iterator<Item = T> | 零成本、可内联 | 只能返回一种具体类型;不能用作结构体字段(需泛型参数) |
impl Iterator<Item = T> + '_ | 明确借用来源(2021 及更早必须写) | 语法噪音 |
Box<dyn Iterator<Item = T> + '_> | 可以返回不同类型、可作为字段 | 每次 next 一次虚调用,无法内联 |
🚀 进阶:Rust 2024 起,
impl Trait在返回位置默认捕获所有在作用域内的生命周期 (RPIT capture rules 2024)。因此在 2024 edition 下fn evens(values: &[i32]) -> impl Iterator<Item = i32>合法; 同一份代码在 2021 edition 下需要写成-> impl Iterator<Item = i32> + '_。 想精确控制捕获哪些生命周期,可以用 1.82 起稳定的use<>语法:-> impl Iterator<Item = i32> + use<'_>这类写法在需要「不捕获」时使用。
生成器式技巧:from_fn / successors / once / repeat
没有 yield 语法,但标准库给了三个构造迭代器的函数,覆盖绝大多数「手写生成器」的需求。
rust
fn main() {
// 1) from_fn:每次 next() 调用一次闭包,闭包用捕获的外部状态推进
let mut n = 0;
let squares: Vec<u32> = std::iter::from_fn(|| {
n += 1;
if n <= 5 { Some(n * n) } else { None }
}).collect();
println!("{squares:?}"); // 输出:[1, 4, 9, 16, 25]
// 2) successors:给定起点 + 「下一步」函数,天然表达递推序列
let collatz: Vec<u64> = std::iter::successors(Some(27u64), |&x| {
if x == 1 { None }
else if x % 2 == 0 { Some(x / 2) }
else { Some(3 * x + 1) }
}).collect();
println!("{} / 首项 {}", collatz.len(), collatz[0]); // 输出:112 / 首项 27
// 3) once / empty / repeat:构造「常量流」
let single: Vec<i32> = std::iter::once(42).collect();
let none: Vec<i32> = std::iter::empty().collect();
let ones: Vec<i32> = std::iter::repeat(1).take(3).collect();
let twos: Vec<i32> = std::iter::repeat_n(2, 3).collect(); // 1.82 起
println!("{single:?} {none:?} {ones:?} {twos:?}");
// 输出:[42] [] [1, 1, 1] [2, 2, 2]
// once 常用于「把单个值接进管道」
let all: Vec<i32> = std::iter::once(0).chain(1..=3).collect();
println!("{all:?}"); // 输出:[0, 1, 2, 3]
}collect 到 Vec<Vec<_>>(嵌套收集):
rust
fn main() -> Result<(), std::num::ParseIntError> {
let text = "1 2 3\n4 5\n6";
// 外层 collect 收集行,内层 collect 收集每行的数字
let rows: Vec<Vec<i32>> = text
.lines()
.map(|line| line.split_whitespace().map(str::parse::<i32>).collect())
.collect::<Result<Vec<Vec<i32>>, _>>()?; // ? 把内层的 Result 短路出来
println!("{rows:?}"); // 输出:[[1, 2, 3], [4, 5], [6]]
// 想「压平」成一层,把内层 collect 换成直接产出迭代器
let flat: Vec<i32> = text
.lines()
.flat_map(|line| line.split_whitespace())
.map(str::parse::<i32>)
.collect::<Result<Vec<i32>, _>>()?;
println!("{flat:?}"); // 输出:[1, 2, 3, 4, 5, 6]
Ok(())
}惰性管道 vs for 循环:什么时候该退回循环?
| 判据 | 用迭代器链 | 用 for 循环 |
|---|---|---|
| 逻辑长度 | ≤ 5~6 个阶段,每段一行 | 超过 6 段、或每段逻辑复杂 |
| 控制流 | 无 break / 无多层 continue、无提前 return | 需要 break 'outer、需要 return 提前退出 |
| 副作用 | 纯转换,或副作用可用 for_each 表达 | 多处副作用、日志与业务交织 |
| 错误处理 | 每个元素返回 Result/Option(collect/try_fold) | 需要一个共享的可变状态、部分成功的中间态 |
| 可读性 | 类型能一眼看出(Vec<i32> → Vec<String>) | 类型复杂到需要给每步写注释 |
| 性能 | 通常与手写循环等价,甚至更好 | 需要「边遍历边改」时反而必须用循环 + retain/drain |
经验法则:先写 for 循环把逻辑跑通,再把它重写成链;如果重写后更难读, 就留在循环版本。 迭代器链的价值是「让读者一眼看到数据流向」,不是「显得高级」。
⚠️ 陷阱:把带
break的循环硬改成链,常见错误是用take_while代替break—— 但take_while只看元素本身,无法访问循环外的状态;而scan或try_fold可以。 选错适配器会让语义从「提前退出」变成「过滤掉某个元素」。
借用型迭代器与「边遍历边改」
v.iter() 借走了 v
rust
fn main() {
let v = vec![1, 2, 3];
let it = v.iter(); // Iter<'_, i32> 内部持有 &[i32],等价于 &v 被借出
println!("{}", it.len()); // 输出:3
println!("{v:?}"); // 只读访问没问题(共享借用可以并存)
// v.clear(); // ❌ E0502:it 还活着,v 不能改
}Iter<'a, T> 的类型里带着 'a,这个 'a 就是 &v 的生命周期。 只要迭代器还有可能被使用,v 就被共享借用,任何 &mut v 都会被拒绝。
E0502 的经典案例:迭代中修改集合
rust
// ❌ 此代码无法编译(E0502)
fn main() {
let mut v = vec![1, 2, 3];
for x in &v {
if *x == 2 {
v.push(9); // 想边遍历边追加
}
}
}error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src\main.rs:4:9
|
3 | for x in &v {
| - immutable borrow occurs here
4 | if *x == 2 {
5 | v.push(9);
| ^^^^^^^^^ mutable borrow occurs here
6 | }
7 | }
| - immutable borrow later used here同样的错误在别处会换一张脸,但本质完全一样:
| 症状 | 报错码 | 本质 |
|---|---|---|
for x in &v { v.push(..); } | E0502 | 遍历持有共享借用,push 要独占借用 |
for x in &mut v { v.remove(0); } | E0499 | iter_mut 已独占,remove 又要独占 |
for (k, _) in &map { map.insert(..); } | E0502 | 同上,哈希表也一样 |
let it = v.iter(); v.clear(); it.next(); | E0502 | 迭代器的最后一次使用在 clear 之后 |
根本原因:Vec 的迭代器是一个「指向堆缓冲区的游标」。 push/remove/clear 都可能重新分配或搬移缓冲区, 迭代器持有的指针立刻失效 —— 这是 C++ 里 vector 迭代器失效(iterator invalidation)的 同名问题,只是 Rust 把它变成了编译错误。
迭代器 + 重新分配 = 悬垂游标
it ──► [1][2][3] v.push(9) 触发扩容
┌──────────────────────┐
it ──► (旧地址,已 free) │ [1][2][3][9] 新地址 │
└──────────────────────┘
⇒ C++:UB;Java:ConcurrentModificationException;
Python:RuntimeError: dictionary changed size during iteration;
Rust:编译错误(最省钱的那种失败)正解:retain / drain / mem::take / 先收集后修改
rust
use std::mem;
fn main() {
// 正解 1:只是「按条件删元素」⇒ retain(一次遍历、原地压缩,O(n))
let mut v = vec![1, 2, 3, 4, 5, 6];
v.retain(|x| x % 2 == 0);
println!("{v:?}"); // 输出:[2, 4, 6]
// 正解 2:需要「按条件改元素」⇒ iter_mut(不改变长度,借用安全)
let mut v = vec![1, 2, 3];
v.iter_mut().for_each(|x| *x *= 10);
println!("{v:?}"); // 输出:[10, 20, 30]
// 正解 3:需要「边遍历边消费原集合」⇒ drain(拿走所有权的同时清空原 Vec)
let mut v = vec!["a".to_string(), "b".to_string()];
let mut out = Vec::new();
for s in v.drain(..) { // 元素被移动出来,v 变成空 Vec
out.push(s.to_uppercase());
}
println!("{out:?} / v 现在有 {} 个", v.len()); // 输出:["A", "B"] / v 现在有 0 个
// 正解 4:需要「遍历 A 的同时往 B 里写」(B 就是 A 时)⇒ mem::take 换出空壳
let mut v = vec![1, 2, 3];
for x in mem::take(&mut v) { // v 被换成一个空 Vec,可以自由写
if x < 3 {
v.push(x * 100);
}
}
println!("{v:?}"); // 输出:[100, 200]
// 正解 5:先收集决策、再统一执行(最通用,代价是一次中间 Vec)
let mut v = vec![1, 2, 3];
let to_add: Vec<i32> = v.iter().filter(|&&x| x >= 2).map(|x| x * 3).collect();
v.extend(to_add);
println!("{v:?}"); // 输出:[1, 2, 3, 6, 9]
}| 需求 | 正解 | 复杂度 | 说明 |
|---|---|---|---|
| 按条件删除元素 | retain(f) | O(n) | f: FnMut(&T) -> bool;retain_mut 给 &mut T(1.61 起) |
| 按条件修改元素 | iter_mut().for_each(..) | O(n) | 不改变长度 |
| 遍历并消费元素 | drain(range) | O(n) | 返回迭代器,可复用原容量;Vec::drain 1.6 起 |
| 遍历并写入同一个变量 | mem::take(&mut v) | O(1) 换出 | 把容器换成 Default(通常是空),旧值拥有所有权 |
| 需要复杂决策 | 先 collect 到临时 Vec,再统一改 | O(n) + 一次分配 | 最通用、最好读;中间 Vec 可 with_capacity |
| 按 key 删除哈希表项 | HashMap::retain(f) | O(n) | 1.18 起,f: FnMut(&K, &mut V) -> bool |
| 队列两端进出 | VecDeque | 两端 O(1) | 见「速查表」 速查表 |
⚠️ 陷阱:
mem::take(&mut v)之后v是Vec::default(),容量是 0(不是保留原容量), 所以紧接着的大量push会重新分配。要保留容量用let mut tmp = Vec::with_capacity(v.capacity()); std::mem::swap(&mut v, &mut tmp);或者直接用drain(..)(它保留容量)。
🚀 进阶:
drain的返回值在被drop时才算「把剩余元素删干净」, 所以let it = v.drain(..);之后如果只是部分消费,剩余的照样会被删除。 想「取出来几个又放回去」,用drain(..n)拿前 n 个,或Vec::split_off(n)。
与其他语言的对照
容器对照表
| 需求 | Python | JS / TS | Java | C++ | Rust |
|---|---|---|---|---|---|
| 动态数组 | list | Array | ArrayList<T> | std::vector<T> | Vec<T> |
| 哈希表 / 字典 | dict | Map / 对象字面量 | HashMap<K,V> | std::unordered_map | HashMap<K,V> |
| 有序映射 | 无(dict 保插入序) | 无 | TreeMap | std::map | BTreeMap<K,V> |
| 集合 | set / frozenset | Set | HashSet / TreeSet | unordered_set / set | HashSet / BTreeSet |
| 双端队列 | collections.deque | 无(用数组) | ArrayDeque | std::deque | VecDeque |
| 优先队列 / 堆 | heapq(最小堆) | 无 | PriorityQueue | priority_queue(最大堆) | BinaryHeap(最大堆,最小堆用 Reverse) |
| 字符串 | str(不可变) | string | String(不可变) | std::string | String(拥有)/ &str(借用) |
| 字符串切片 | s[1:3](复制) | s.slice(1,3)(复制) | s.substring(1,3)(复制) | std::string_view(视图,易悬垂) | &s[1..3](视图,编译期保证不悬垂) |
| 越界访问 | IndexError(运行期) | undefined / 静默 | IndexOutOfBoundsException | UB | v[i] panic,v.get(i) 给 Option |
| 哈希表缺 key 取默认 | d[k] = d.get(k, 0) + 1 | m.get(k) ?? 0 | getOrDefault / merge | m[k] 会自动插入 0 | *m.entry(k).or_insert(0) += 1 |
| 迭代中修改容器 | RuntimeError | 行为不定 | ConcurrentModificationException | UB | 编译错误(E0502) |
| 遍历顺序 | dict 保插入序 | Map 保插入序 | HashMap 不保证 | unordered_map 不保证 | HashMap 不保证;BTreeMap 按键升序 |
| 自定义 key 的要求 | __hash__ + __eq__ | 任意值(SameValueZero) | hashCode + equals | std::hash 特化 + operator== | #[derive(Hash, PartialEq, Eq)] |
迭代器链对照
| 操作 | Python | JS | Java Stream | C++20 Ranges | Rust |
|---|---|---|---|---|---|
| 变换 | [f(x) for x in xs] | xs.map(f) | xs.stream().map(f) | xs | views::transform(f) | xs.iter().map(f) |
| 过滤 | [x for x in xs if p(x)] | xs.filter(p) | .filter(p) | views::filter(p) | .filter(p) |
| 变换 + 过滤合一 | 生成器表达式 + if | flatMap | mapMulti | transform + filter | .filter_map(f) |
| 求和 | sum(xs) | xs.reduce((a,b)=>a+b,0) | .mapToInt(..).sum() | std::accumulate | .sum::<i32>() |
| 折叠 | functools.reduce(f, xs, init) | reduce | .reduce(init, f) | std::accumulate | .fold(init, f) |
| 收集结果 | list(...) | (立即返回数组) | .collect(Collectors.toList()) | std::vector(rng) | .collect::<Vec<_>>() |
| 取前 n 个 | itertools.islice(xs, n) | xs.slice(0, n) | .limit(n) | views::take(n) | .take(n) |
| 带下标 | enumerate(xs) | xs.entries() | IntStream.range | views::enumerate(C++23) | .enumerate() |
| 相邻窗口 | zip(xs, xs[1:]) | 手写循环 | 手写循环 | views::adjacent<2>(C++23) | xs.windows(2) |
| 惰性求值 | 生成器表达式惰性,列表推导立即 | map/filter 立即并新建数组 | ✅ 惰性 | ✅ 惰性 | ✅ 惰性 |
| 短路找到 | next((x for x in xs if p(x)), None) | xs.find(p) | .filter(p).findFirst() | ranges::find_if | .find(p) |
| 是否全部满足 | all(p(x) for x in xs) | xs.every(p) | .allMatch(p) | ranges::all_of | .all(p) |
| 分组 | itertools.groupby | Object.groupBy | Collectors.groupingBy | 手写 | 手写 entry().or_default().push(..) |
| 链的中间容器 | 列表推导会建新 list | map 建新数组 | 不建(流式) | 不建(视图) | 不建(迭代器) |
零成本抽象:单态化让迭代器「免费」
Rust 的泛型在编译期单态化(monomorphization):编译器为每个具体类型组合 生成一份专用代码。于是 v.iter().map(|x| x * 2).filter(|x| x > &3) 展开后是:
source:
v.iter().map(closure1).filter(closure2).collect::<Vec<_>>()
monomorphization 之后(概念上):
struct MapIter<'a> { inner: slice::Iter<'a, i32>, f: Closure1 }
struct FilterIter<I> { inner: I, p: Closure2 }
impl Iterator for FilterIter<MapIter<'a, Closure1>> { ... }
内联之后:
let mut out = Vec::with_capacity(v.len());
for &x in v { // 一个循环搞定
let y = x * 2; // closure1 内联
if y > 3 { out.push(y); } // closure2 内联
}| 语言 | 迭代抽象的代价 | 原因 |
|---|---|---|
| Rust | 通常 0(与手写循环同汇编) | 单态化 + 内联,闭包是零大小类型,适配器结构体全部被优化掉 |
| C++ | 通常 0(模板同理) | 模板实例化 + 内联;但 std::function 会引入间接调用 |
| Java | 每次 next() 一次虚调用 + 可能的装箱 | Stream 是接口,JIT 有时能内联但没保证 |
| Python | 每个元素一个 Python 对象 + 解释器循环 | 一切皆为对象,map/filter 也要调用 Python 函数 |
| JS | map/filter 是内置(快),但会创建中间数组;回调也是函数调用 | V8 有优化,但语义上必须立刻产出数组 |
| Go | for range 无抽象;想链式就得手写或用泛型函数 | 没有方法链式适配器 |
代价在编译期与二进制体积:每种迭代器 + 闭包组合都会生成一份代码。 一条链上 5 个闭包,就是 5 个新类型。若一个泛型函数被 100 种类型实例化, 二进制会明显变大(这是 Rust 编译慢的重要原因之一)。
🧠 原理:「零成本抽象」的完整含义是:你不用的东西不付出代价,你用的东西不比手写差。 前半句指 Rust 没有 GC、没有运行期反射;后半句指迭代器/泛型在优化后与手写循环等价。 但它不承诺编译时间:抽象越多,单态化出的代码越多,编译越慢。
💡 对照:Java 的
Stream需要「先.stream()再.collect()」是因为它必须 从集合切换到流、再切回来;Rust 的iter()/collect()是同一件事的对称操作, 但Vec、数组、&[T]、Range全都是迭代器来源,不需要中间适配层。
常见坑与编译错误
for x in v 移动了 v
rust
// ❌ 此代码无法编译(E0382)
fn main() {
let v = vec![String::from("a")];
for s in v { // v 被 into_iter() 消费
println!("{s}");
}
println!("{}", v.len()); // 这里 v 已经没了
}error[E0382]: borrow of moved value: `v`
--> src\main.rs:6:20
|
3 | let v = vec![String::from("a")];
| - move occurs because `v` has type `Vec<String>`,
| which does not implement the `Copy` trait
4 | for s in v {
| - `v` moved due to this implicit call to `.into_iter()`
...
6 | println!("{}", v.len());
| ^ value borrowed here after move修法:遍历用 &v(要读)或 &mut v(要改);确实要消费就接受 v 消失。 元素是 Copy 类型时也别偷懒 —— for x in v 对 Vec<i32> 同样会移动 v, 因为 Vec<i32> 本身不是 Copy。
collect 类型推断失败需要 turbofish
rust
// ❌ 此代码无法编译(E0283)
fn main() {
let v = (1..=3).collect(); // 收集成什么?Vec?HashSet?String?
println!("{v:?}");
}error[E0283]: type annotations needed
--> src\main.rs:3:9
|
3 | let v = (1..=3).collect(); // 收集成什么?Vec?HashSet?String?
| ^ ------- type must be known at this point
|
= note: the type must implement `FromIterator<i32>`
note: required by a bound in `collect`
help: consider giving `v` an explicit type
|
3 | let v: Vec<_> = (1..=3).collect(); // 收集成什么?Vec?HashSet?String?
| ++++++++目标类型完全没线索时报
E0283;有时也会报E0282。 两者含义一样:「我看不出你要收集成什么类型」,处方都是加类型标注或 turbofish。
三种修法,按可读性排序:
rust
use std::collections::HashSet;
fn main() {
let a: Vec<i32> = (1..=3).collect(); // 1) 变量标注类型
let b = (1..=3).collect::<Vec<i32>>(); // 2) turbofish(管道在中间时常用)
let c: HashSet<i32> = (1..=3).collect(); // 3) 目标类型本身就在约束里
println!("{a:?} {b:?} {:?}", {
let mut v: Vec<i32> = c.into_iter().collect();
v.sort();
v
});
// 输出:[1, 2, 3] [1, 2, 3] [1, 2, 3]
}⚠️ 陷阱:
collect::<Result<Vec<_>, _>>()里两个_都是必需的: 第一个让元素类型由迭代器推断,第二个让错误类型由闭包推断。 只写collect::<Result<Vec<i32>, _>>()也行,但错误类型必须能从上下文推断出来。
HashMap 的 key 是 String 时的所有权处理
rust
use std::collections::HashMap;
fn main() {
let mut map: HashMap<String, i32> = HashMap::new();
// 坑 1:为插入而 clone key
let key = String::from("count");
map.insert(key.clone(), 1);
// 坑 2:想「找到就改」却先 get 再 insert,中间的借用已经结束,语义也对但效率差
if map.contains_key(&key) {
let v = map.get_mut(&key).expect("刚确认存在");
*v += 1;
}
// 正解:entry 一次查找,且不用 clone key(key 只在真正需要插入时才被消费)
map.entry(key.clone()).and_modify(|v| *v += 1).or_insert(0);
// 查询时用 &str 就够了:String: Borrow<str>
println!("{:?}", map.get("count")); // 输出:Some(&2)
println!("{map:?}");
}如果要避免为每个 key 分配 String:
| 方案 | 适用 | 代价 |
|---|---|---|
HashMap<&str, V> | key 来自生命周期足够长的数据(如整个输入文本) | 生命周期参数传染到结构体 |
HashMap<Box<str>, V> | 需要拥有但 String 的 capacity 用不上 | 与 String 等价 |
HashMap<u64, V>(先做完美哈希/ID) | 高频查询、key 集合固定 | 需要自己做 ID 映射 |
HashMap<SmolStr, V>(cargo add smol_str@0.3) | 大量短 key | 第三方依赖 |
性能与语义陷阱清单
| 坑 | 症状 | 修法 |
|---|---|---|
s.chars().nth(i) | 循环里用 ⇒ O(n²) | 先 let v: Vec<char> = s.chars().collect();,之后 v[i] 是 O(1) |
Vec::remove(0) / insert(0, x) | 当队列用 ⇒ O(n²) | VecDeque 的 pop_front/push_back |
Vec 不预分配 | 百万级 push 触发多次搬移 + 峰值内存翻倍 | Vec::with_capacity(n) |
or_insert(compute()) | 默认值的构造函数每次都被调用 | or_insert_with(|| compute()) / or_default() |
HashMap 迭代顺序 | 测试偶尔失败、输出顺序每次不同 | 换 BTreeMap,或 collect 后 sort |
binary_search 未排序 | 不 panic,静默返回错误结果 | 先 sort(),或改用 iter().find(..) |
sort_by 的比较器非严格弱序 | 可能 panic(user-provided comparison function does not correctly implement a total order)或排序结果错乱 | 保证 a.cmp(b)、b.cmp(a)、a.cmp(c) 三条性质;别用 <=/>=,别在比较里 unwrap 掉 None |
sort_by 里返回 Ordering::Less/Greater 混用 | 同上 | 用 a.partial_cmp(b).unwrap_or(Ordering::Equal) 或 f64::total_cmp(1.62 起) |
sort_by_key 的 key 借用元素本身 | 生命周期报错 | 用 sort_by 或 sort_by_cached_key |
windows(0) / chunks(0) | 运行期 panic | 先 assert!(n > 0) |
sum::<i32>() 溢出 | debug panic、release 静默回绕 | 用 i64,或 try_fold + checked_add |
map 链忘记消费 | 有 unused_must_use 警告但程序「什么都没发生」 | 收尾加 .collect() / for_each / 赋值;CI 用 -D warnings |
HashMap<f64, V> | E0277: f64: Eq is not satisfied / Hash is not implemented for f64 | 转定点整数,或用 ordered_float |
| 遍历时修改 | E0502 / E0499 | retain / drain / mem::take(见「正解:retain / drain / mem::take / 先收集后修改」) |
&Vec<T> / &String 参数 | clippy::ptr_arg 警告、调用方受限 | 改 &[T] / &str |
sort_by 的严格弱序(strict weak ordering)值得单独强调 —— 它是排序正确性的前提:
rust
fn main() {
let mut v = vec![3, 1, 2];
// ✅ 正确:用 cmp 得到一个真正的全序
v.sort_by(|a, b| a.cmp(b));
println!("{v:?}"); // 输出:[1, 2, 3]
// ✅ 浮点:total_cmp 给全序(-0.0 < 0.0,NaN 被排在最后)
let mut f = vec![2.5, f64::NAN, 1.0];
f.sort_by(|a, b| a.total_cmp(b));
println!("{f:?}"); // 输出:[1.0, 2.5, NaN]
// ❌ 反面:比较器写成 a >= b 会破坏反对称性,1.81+ 的排序实现会主动 panic
// v.sort_by(|a, b| if a >= b { Ordering::Greater } else { Ordering::Less });
}⚠️ 陷阱:
sort_by的比较器必须满足严格弱序,否则 1.81 起的新排序实现 (driftsort / ipnsort)会检测到并 panic:user-provided comparison function does not correctly implement a total order。 在这之前,违反契约是「排序结果错乱」这种更难查的 bug —— 所以这其实是好事。
速查表
| 我想要…… | 写法 | 备注 |
|---|---|---|
| 建可增长数组 | Vec::new() / vec![1,2,3] / Vec::with_capacity(n) | 已知规模就预分配 |
| 增删 | push(x) / pop() / insert(i, x) / remove(i) | 前两个均摊 O(1),后两个 O(n),别当队列用 |
| 安全取值 | get(i) / first() / last() → Option<&T> | 不 panic;v[i] 越界会 panic |
| 取切片 | &v[a..b] / &v[..] / &mut v[a..b] | 零拷贝视图 |
| 函数参数 | &[T] / &mut [T] / &str | 不要 &Vec<T> / &String |
| 分组 / 窗口 | v.chunks(n) / v.windows(n) | 切片方法,n > 0 |
| 排序 / 查找 | v.sort() / v.sort_unstable() / v.binary_search(&x) | binary_search 要求已排序 |
| 按条件删 / 拿走元素 | v.retain(|x| ...) / v.drain(..) / std::mem::take(&mut v) | drain 保留容量 |
| 词频 / 分组累加 | *m.entry(k).or_insert(0) += 1 | 一次查找,别用 contains_key + insert |
| 缺 key 才插 / 存在才改 | .or_insert(v) / .or_insert_with(f) / .or_default() / .and_modify(f).or_insert(v) | _with 才惰性求值 |
| 有序遍历 / 范围查询 | BTreeMap + range(a..b) | HashMap 做不到 |
| 去重 | HashSet::insert 返回 bool | !insert(x) 即重复 |
| 三种遍历 | &v / &mut v / v | 对应 iter / iter_mut / into_iter |
| 收集 / 统计 / 累加 | collect::<Vec<_>>() / collect::<Result<Vec<_>,_>>() / sum::<i32>() / count() / fold / reduce / try_fold | 推断不出就 turbofish;try_fold 可短路 |
| 队列 / 堆 / 迭代器返回 | VecDeque::push_back+pop_front / BinaryHeap(最小堆套 Reverse) / -> impl Iterator<Item = T> | BinaryHeap 是最大堆;size_hint 顺手实现 |