跳转到主内容iterator | 物联网民工

iterator

C 程序员写遍历,第一反应通常是索引或指针:

for (size_t i = 0; i < len; i++) {
    sum += data[i];
}

或者:

for (uint8_t *p = buf; p < buf + len; p++) {
    *p ^= key;
}

Rust 当然也能写循环,但更常用的模型是 iterator。它不是“高级语法糖”,而是把遍历过程变成一个可以传递、组合、消费的值。

let sum: u32 = data.iter().map(|b| *b as u32).sum();

这一篇只讲 C 程序员最需要的工程直觉:怎样从索引循环迁移到 iterator,什么时候借用,什么时候可变借用,什么时候消耗容器。

for 遍历的是 iterator

Rust 的 for 不是 C 的三段式 for。它遍历一个实现了 IntoIterator 的值:

let data = vec![1_u8, 2, 3];

for byte in &data {
    println!("{byte}");
}

这里的 &data 会产生只读 iterator。每次循环拿到一个元素引用。

如果只想按索引访问,Rust 也可以写:

for i in 0..data.len() {
    println!("{}", data[i]);
}

但普通遍历优先写成:

for byte in &data {
    println!("{byte}");
}

这样少写索引,也少一次越界风险。Rust 的索引越界会 panic,iterator 会自然按元素范围遍历。

iter、iter_mut、into_iter 的区别

对 C 程序员来说,最先要分清三个入口:

iter()      -> 只读借用每个元素
iter_mut()  -> 可变借用每个元素
into_iter() -> 消耗容器或值,拿走元素

只读遍历:

let data = vec![1_u8, 2, 3];

for byte in data.iter() {
    println!("{byte}");
}

println!("{}", data.len());

byte 是 &u8,data 仍然可用。

原地修改:

let mut data = vec![1_u8, 2, 3];

for byte in data.iter_mut() {
    *byte ^= 0xAA;
}

byte 是 &mut u8。循环期间,iterator 逐个给你元素的可变借用。

消耗容器:

let data = vec![String::from("a"), String::from("b")];

for item in data.into_iter() {
    println!("{item}");
}

// data 不能再用

item 是 String,所有权被拿出来。容器被消费。

这个区别和前面讲所有权、借用、可变借用是一条线:遍历不是例外,它也要回答“谁拥有元素,谁只是看,谁在改”。

map 和 filter 适合表达数据转换

C 里常见写法是手动创建输出数组,再循环填充:

size_t out_len = 0;
for (size_t i = 0; i < len; i++) {
    if (items[i] > 0) {
        out[out_len++] = items[i] * 2;
    }
}

Rust 可以写成 iterator 组合:

let out: Vec<i32> = items
    .iter()
    .filter(|value| **value > 0)
    .map(|value| *value * 2)
    .collect();

filter 负责筛选,map 负责转换,collect 把结果收集成 Vec。

这里 items.iter() 产生的是 &i32,所以闭包里会看到引用。也可以先用 copied() 把简单值复制出来:

let out: Vec<i32> = items
    .iter()
    .copied()
    .filter(|value| *value > 0)
    .map(|value| value * 2)
    .collect();

对 Copy 的小整数来说,copied() 很常见,能减少闭包里到处写 *value。

collect 决定最终容器

iterator 是惰性的。单独写 map 不会立刻生成新容器:

let iter = data.iter().map(|value| value * 2);

只有被消费时才真正运行。常见消费方式有:

let values: Vec<i32> = iter.collect();

或者直接求和:

let sum: i32 = data.iter().sum();

collect 的目标类型很重要。同一个 iterator 可以收集成不同容器:

let values: Vec<i32> = data.iter().copied().collect();
let set: std::collections::HashSet<i32> = data.iter().copied().collect();

这和 C 里“循环逻辑”和“输出容器”通常绑在一起不一样。Rust 会把“怎么遍历”和“收集到哪里”拆开。

enumerate 替代手写索引

如果既要元素又要下标,用 enumerate:

for (index, byte) in data.iter().enumerate() {
    println!("{index}: {byte}");
}

这比手写:

for index in 0..data.len() {
    let byte = data[index];
}

更少出错。索引和元素来自同一个 iterator,不需要自己维护同步关系。

处理协议字段、错误位置、行号时,enumerate 很常用:

fn find_zero(data: &[u8]) -> Option<usize> {
    for (index, byte) in data.iter().enumerate() {
        if *byte == 0 {
            return Some(index);
        }
    }

    None
}

也可以写成:

fn find_zero(data: &[u8]) -> Option<usize> {
    data.iter().position(|byte| *byte == 0)
}

两种都可以。初学时先写 for 循环,读起来更直接;熟悉以后再用更短的组合器。

不要为了炫技写难懂链式调用

iterator 很强,但不是每段代码都要写成一长串链式调用。

下面这种写法可能太密:

let result: Vec<_> = packets
    .iter()
    .filter(|packet| packet.enabled)
    .flat_map(|packet| packet.fields.iter())
    .filter_map(|field| parse_field(field).ok())
    .filter(|field| field.len() > 0)
    .map(|field| field.normalize())
    .collect();

如果每一步都有业务含义,可以拆开:

let enabled_packets = packets.iter().filter(|packet| packet.enabled);
let fields = enabled_packets.flat_map(|packet| packet.fields.iter());

let result: Vec<_> = fields
    .filter_map(|field| parse_field(field).ok())
    .filter(|field| field.len() > 0)
    .map(|field| field.normalize())
    .collect();

或者直接写 for。Rust 里清楚比“函数式味道足”更重要。

iterator 和错误处理

处理 Result 时,iterator 也能很好配合。

假设每行都要解析:

fn parse_line(line: &str) -> Result<u32, ParseError> {
    line.parse().map_err(|_| ParseError)
}

可以把 Iterator<Item = Result<T, E>> 收集成 Result<Vec<T>, E>:

let values: Result<Vec<u32>, ParseError> =
    lines.iter().map(|line| parse_line(line)).collect();

只要中间有一个 Err,结果就是 Err;全部成功才得到 Vec<u32>。

如果逻辑更复杂,for 加 ? 也很清楚:

let mut values = Vec::new();

for line in lines {
    values.push(parse_line(line)?);
}

不要强迫自己把所有错误路径都塞进链式调用。库代码里,可读性通常更重要。

自己的类型也可以提供 iterator

如果你写的是库,类型内部可能有 Vec、数组或其他集合。不要把内部字段直接暴露出去,可以提供 iterator 方法:

struct Packet {
    fields: Vec<Field>,
}

impl Packet {
    fn fields(&self) -> impl Iterator<Item = &Field> {
        self.fields.iter()
    }
}

调用方可以:

for field in packet.fields() {
    println!("{}", field.name());
}

如果允许修改:

impl Packet {
    fn fields_mut(&mut self) -> impl Iterator<Item = &mut Field> {
        self.fields.iter_mut()
    }
}

这比返回内部 Vec 更稳妥。API 暴露的是“可以遍历字段”,不是“内部一定用 Vec 存字段”。

这一篇先记住什么

iterator 是 Rust 里表达遍历的核心工具。它不是为了替代所有 for,而是让遍历、过滤、转换和收集变得可组合。

从 C 迁移时先抓住这些对应关系:

for i in 0..len                 -> 优先考虑 for item in slice
只读遍历                         -> iter() / &collection
原地修改                         -> iter_mut()
拿走元素                         -> into_iter()
手写索引计数                     -> enumerate()
筛选和转换                       -> filter() / map()
生成 Vec 或其他容器              -> collect()
遍历中可能失败                   -> for + ? 或 collect::<Result<Vec<_>, _>>()

iterator 不要求你放弃普通循环。更实际的标准是:简单遍历用 for,清晰的数据管道用组合器,复杂业务逻辑拆开写。

下一篇讲 closure 和回调。C 里常用函数指针加 void *ctx 表达回调,Rust 会用 closure 把函数和捕获环境放在一起。