Iterator 与闭包

本页目录

迭代器把“产生下一个元素”与“怎样处理元素”分开。闭包则将一小段行为及其所需环境一起传给 map、filter 等操作。两者组合后,调用者可以描述数据处理步骤,并由消费端控制需要多少结果。

理解时应同时跟踪三件事:何时真正执行逐元素逻辑、Item 是值还是引用、闭包对捕获对象做了什么。

next 是契约入口,不是所有实现的唯一执行路径

Iterator 的必要成员是关联类型 Item 与 next(&mut self) -> OptionSelf::Item。提供它们就能使用大量有默认实现的方法,但具体迭代器可以覆盖 nth、fold、try_fold 等以优化,不能把所有操作的实际机器执行都说成逐次调用默认 next。

一般 Iterator 在返回 None 后仍可能再次返回 Some。需要一旦结束便永久结束的语义时,使用 fuse 或依赖 FusedIterator 的保证。for 循环在遇到第一次 None 时停止。Iterator

惰性:没有消费就没有逐元素转换

0..10提供整数filter保留偶数map乘以 2take(3)最多三项元素向右流动collect 驱动消费:得到 [0, 4, 8] 后停止构建适配器会创建状态并捕获闭包环境;惰性指逐元素逻辑尚未执行。惰性管道按需求拉取:take(3) 不需要走完源迭代器
fn main() {
    let mut inspected = 0;
    let output: Vec<_> = (0..10)
        .inspect(|_| inspected += 1)
        .filter(|x| x % 2 == 0)
        .map(|x| x * 2)
        .take(3)
        .collect();
    assert_eq!(output, [0, 4, 8]);
    assert_eq!(inspected, 5); // 只读取源中的 0、1、2、3、4
}

上面是从消费端反向请求、从源端正向交付:为了第三个输出 8,filter 需要读到源值 4,后面的 5–9 不再访问。构造闭包时的捕获、构造源迭代器的副作用仍可能立即发生,惰性不等于整条表达式什么都不做。

顺序会改变结果:filter(...).take(3) 是前三个满足条件的元素;take(3).filter(...) 只在源的前三项里筛选。find、any、all、try_fold 等还可能短路,不能把未执行的后半段当作可靠的副作用。

take 放在哪,源就读到哪

这里没有预先生成中间数组,而是按消费需求决定读到哪里。k=3 时,先 filter 会读到源值 4 才凑够三个偶数;先 take 则只检查 0、1、2。把 k 调为 0,逐元素工作完全不发生,但不能据此推断构造阶段没有其他成本。

正在呈现知识画面
take 放在哪,源就读到哪

filter 后 take 得到前 k 个匹配项;take 后 filter 只检查源的前 k 项。

iter、iter_mut 与 into_iter 的所有权

Vec 的常用入口Item对原容器的影响
v.iter() 或 (&v).into_iter()&T借用元素,容器仍归原所有者
v.iter_mut() 或 (&mut v).into_iter()&mut T逐项独占借用
v.into_iter()T消耗 Vec,移交其中元素

IntoIterator 是“能转换成迭代器”的接口,Iterator 是已经可以被推进的状态。for 会使用 IntoIterator;不要看到方法名 into_iter 就脱离接收者类型断定一定移动底层元素。

map 接收当前 Item,filter 的谓词接收 &Item,所以对 iter() 的 filter 参数可能看到双层引用。copied 适用于 Copy 元素,cloned 执行 Clone;后者对 String 等可能分配,不是只让类型检查器满意的无成本操作。

需要暂时消费几项再继续使用同一个迭代器时,可以 by_ref:

fn main() {
    let mut items = 0..5;
    let first: Vec<_> = items.by_ref().take(2).collect();
    assert_eq!(first, [0, 1]);
    assert_eq!(items.next(), Some(2));
}

停下时,那个不满足条件的元素去了哪里

take_while 必须先取出一个元素才能判断谓词。当它遇到第一个不满足条件的元素时,这个元素已经从源迭代器取走,不会留给后面的消费者。解析包含分隔符的数据时,这个位置变化尤其容易误判。

fn main() {
    let mut source = [1, 2, 0, 3].into_iter();
    let prefix: Vec<_> = source.by_ref().take_while(|x| *x != 0).collect();
    assert_eq!(prefix, [1, 2]);
    assert_eq!(source.next(), Some(3)); // 0 已用于失败的谓词判断

    let mut source = [1, 2, 0, 3].into_iter().peekable();
    let mut prefix = Vec::new();
    while let Some(value) = source.next_if(|x| *x != 0) {
        prefix.push(value);
    }
    assert_eq!(prefix, [1, 2]);
    assert_eq!(source.next(), Some(0)); // 判断失败时仍保留该元素
}

Peekable 可以保留逻辑上的下一项,但首次 peek 可能调用底层 next 并缓存元素,所以“尚未取走”不等于“底层没有副作用”。上例 next_if 的失败会保留 0,随后可由专门处理分隔符的代码消费。不要把这种需求写成 take_while 后再假设分隔符仍在。take_while

捕获方式与调用能力是两个维度

move 决定闭包捕获环境时取得所有权,不代表每次调用都把环境移出去。Fn/FnMut/FnOnce 描述的是调用时需要怎样访问闭包自身:

能力调用接收者典型限制
FnOnceself允许调用消耗闭包,不能保证之后还能调用
FnMut&mut self可重复调用,可能需要修改捕获状态
Fn&self可通过共享借用调用,不需独占访问闭包环境

Fn 类型也满足 FnMut 与 FnOnce,FnMut 类型也满足 FnOnce。接受 FnOnce 的函数允许最广的一次调用行为;要求 Fn 则对实现者更严格,不能笼统称 Fn 是“最通用”。普通闭包都至少实现 FnOnce,async 闭包还有其自身的调用 trait 规则。闭包类型

fn call_twice<F: Fn()>(f: F) { f(); f(); }

fn main() {
    let values = vec![1, 2, 3];
    call_twice(move || println!("{values:?}"));
    // move 让闭包拥有 Vec;println! 只借用它,仍能实现 Fn。

    let mut count = 0;
    {
        let mut add = || count += 1;
        add();
        add();
    }
    assert_eq!(count, 2);

    let text = String::from("owned");
    let take = move || text;
    assert_eq!(take(), "owned");
    // 再次 take() 不可行:第一次已把 text 移出闭包。
}

通过 Cell、RefCell 或锁等内部可变性,Fn 闭包也可以改变外部可观察状态,所以 Fn 不等于纯函数或无副作用。是否 Send、Sync、是否满足 'static 另由捕获内容和使用方式决定;move 捕获一个短生命周期引用不会把它变成 'static。

返回闭包时,把环境寿命写进接口

fn starts_with<'a>(prefix: &'a str) -> impl Fn(&str) -> bool + 'a {
    move |value| value.starts_with(prefix)
}

fn main() {
    let prefix = String::from("api/");
    let matches = starts_with(&prefix);
    assert!(matches("api/items"));
    assert!(!matches("static/items"));
}

闭包拥有的是那个引用,字符串仍属于调用者。返回类型中的 'a 表达环境依赖,不会延长 prefix 的实际寿命。若需要把闭包保存到要求 'static 的任务中,可以改为接收并拥有 String;是否值得分配或复制,应由接口需要的拥有关系决定。

collect 的目标类型决定如何结束

collect 通过 FromIterator 构造目标,不只限于 Vec。收集 Iterator<Item=Result<T,E>> 到 Result<Vec,E> 时,首个错误会让收集短路:

fn main() {
    let parsed: Result<Vec<u32>, _> = ["10", "bad", "30"]
        .into_iter()
        .map(str::parse)
        .collect();
    assert!(parsed.is_err());
}

这与 filter_map(|s| s.parse().ok()) 不等价:后者静默丢弃错误项。选择哪一种取决于业务是否允许忽略坏数据,而不是链式表达式长短。

"10"已解析,累计 10"bad"已取出,解析失败"30"未读取,可继续处理返回 Err;迭代器下一项是 "30"短路保存剩余迭代位置,但不会回滚已经执行的操作try_fold 的累计值不是外部事务;此前日志、写入或发送等副作用不会自动撤销。

try_fold 的错误与剩余位置

fn main() {
    let mut input = ["10", "bad", "30"].into_iter();
    let total: Result<u32, &'static str> = input.try_fold(0u32, |sum, text| {
        let value = text.parse::<u32>().map_err(|_| "invalid number")?;
        sum.checked_add(value).ok_or("sum overflow")
    });
    assert_eq!(total, Err("invalid number"));
    assert_eq!(input.next(), Some("30"));
}

失败项已经消费,后续项仍可处理,这对于带恢复策略的逐条输入很有用。若要重试失败项,必须在错误中保留它或另行缓存,不能期待迭代器自动倒退。示例的 checked_add 也使求和的溢出策略不依赖 debug/release。

另一个容易隐藏数据丢失的表达式是对 Result 迭代器直接 flatten:Ok 会提供一个值,Err 不提供值,错误就此被丢弃。需要失败信息时,应选择 Result 收集、显式 match 或保留逐项结果。try_fold

size_hint 能帮助分配,不能授权越界写入

fn main() {
    let values = [1u32, 2, 3, 4];
    let mut exact = values.iter();
    assert_eq!(exact.len(), 4);
    exact.next();
    assert_eq!(exact.len(), 3);

    let filtered = values.iter().filter(|x| **x % 2 == 0);
    assert_eq!(filtered.size_hint(), (0, Some(4)));
    assert_eq!(filtered.count(), 2);
}

过滤前可以知道剩余恰好四项;过滤后,在执行谓词前一般只能给出零到四项的范围。size_hint 的下界不代表已经初始化的元素数,上界也不是容量;普通安全 trait 的实现可能错误,unsafe 代码不能只凭这些提示就写出已分配区域或把未初始化内存视为有效值。即使 ExactSizeIterator 提供精确长度接口,它也不是允许调用者放弃内存安全检查的 unsafe 契约。size_hint

零成本抽象需要正确理解

静态类型的适配器链通常给编译器足够信息进行单态化、内联和循环优化,不需要为每个 map 创建一个中间 Vec。但 collect 到非空 Vec 仍可能分配,闭包内部也可能分配;动态分派、复杂分支与优化边界又会影响最终代码。

“零成本”不是保证任意链都和某个手写循环生成完全相同汇编,更不是不消耗 CPU。对热点比较相同语义、相同优化设置与相同数据,检查分配、短路行为和最终产物。先保留清晰的迭代模型,再根据证据优化。