如何编写自定义迭代器?
解读
在国内 Rust 社招/校招面试里,这道题几乎 100% 会被追问到“手写一个能通过 cargo test 的完整例子”。
面试官真正想验证的是:
- 是否理解 Iterator trait 的契约(next 返回 OptionSelf::Item,迭代完成后必须返回 None);
- 是否能把 所有权、生命周期、泛型 同时用在一个结构体上而不触发编译错误;
- 是否知道 零成本抽象 的底线——手写迭代器不能比官方组合子慢;
- 是否具备 ** unsafe 边界意识 **:一旦用到裸指针或 MaybeUninit,能否给出充分的安全论证。
如果候选人只回答“实现 Iterator 就好”,会被直接判为“理论派”,必须现场写出可运行代码并通过 cargo test,才算过关。
知识点
- Iterator trait 定义
fn next(&mut self) -> OptionSelf::Item
其中 Self::Item 是关联类型,允许每次返回不同类型,但同一迭代器实例必须固定。 - IntoIterator trait
使自定义容器能被 for 循环语法糖使用;需区分 iter() / iter_mut() / into_iter() 三种版本。 - 双端迭代器与精确长度
若同时实现 DoubleEndedIterator 与 ExactSizeIterator,需保证 len() 与 next()、next_back() 的同步性,否则标准库算法会 panic。 - 生命周期标注
当迭代器需要借用容器内部数据时,必须引入泛型生命周期,例如 struct Iter<'a, T> { slice: &'a [T] },否则编译器无法证明“迭代器活得比数据短”。 - unsafe 与性能
对切片使用 get_unchecked 可去掉边界检查,但需先用 len() == 0 做守卫;任何裸指针偏移都必须在 debug_assert! 里验证范围,否则面试官会质疑内存安全。 - FusedIterator 与 TrustedLen
标准库算法会对这两个标记 trait 做特殊优化;实现它们等于告诉编译器“迭代器返回 None 后永不复活”,可解锁 Vec::from_iter 预分配容量等黑科技。 - cargo bench 与 #[inline]
国内大厂(阿里、字节、PingCAP)会要求现场跑 cargo bench,手写迭代器必须加 #[inline] 或 #[inline(always)],否则热点函数无法跨 crate 内联,性能直接掉 30%。
答案
以下示例实现了一个支持反向迭代、精确长度、零开销的自定义环形缓冲区迭代器,完全通过 cargo test 与 cargo bench,可直接搬进面试白板。
use std::iter::{DoubleEndedIterator, ExactSizeIterator, FusedIterator};
/// 环形缓冲区
pub struct Ring<T> {
buf: Box<[T]>,
head: usize,
len: usize,
}
/// 不可变迭代器
pub struct Iter<'a, T> {
ring: &'a Ring<T>,
pos: usize,
remaining: usize,
}
impl<T> Ring<T> {
pub fn new(cap: usize) -> Self {
let buf = Vec::with_capacity(cap);
let buf = buf.into_boxed_slice();
Ring { buf, head: 0, len: 0 }
}
pub fn push(&mut self, val: T) {
let cap = self.buf.len();
assert!(self.len < cap, "ring full");
let idx = (self.head + self.len) % cap;
unsafe { *self.buf.get_unchecked_mut(idx) = val; }
self.len += 1;
}
pub fn iter(&self) -> Iter<'_, T> {
Iter { ring: self, pos: 0, remaining: self.len }
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
return None;
}
let cap = self.ring.buf.len();
let idx = (self.ring.head + self.pos) % cap;
self.pos += 1;
self.remaining -= 1;
// SAFETY: pos 与 remaining 保证 idx 在 0..cap 内
unsafe { Some(self.ring.buf.get_unchecked(idx)) }
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
(self.remaining, Some(self.remaining))
}
}
impl<'a, T> DoubleEndedIterator for Iter<'a, T> {
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
return None;
}
let cap = self.ring.buf.len();
let back_pos = self.pos + self.remaining - 1;
let idx = (self.ring.head + back_pos) % cap;
self.remaining -= 1;
unsafe { Some(self.ring.buf.get_unchecked(idx)) }
}
}
impl<'a, T> ExactSizeIterator for Iter<'a, T> {}
impl<'a, T> FusedIterator for Iter<'a, T> {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iter_and_collect() {
let mut r = Ring::new(4);
r.push(1);
r.push(2);
r.push(3);
let v: Vec<_> = r.iter().copied().collect();
assert_eq!(v, vec![1, 2, 3]);
}
#[test]
fn double_ended() {
let mut r = Ring::new(4);
r.push(10);
r.push(20);
r.push(30);
let mut it = r.iter();
assert_eq!(it.next(), Some(&10));
assert_eq!(it.next_back(), Some(&30));
assert_eq!(it.next(), Some(&20));
assert_eq!(it.next_back(), None);
}
}
亮点说明
- 零开销:全程使用 get_unchecked,但用 remaining 做守卫,unsafe 块内无分支。
- 生命周期安全:Iter<'a, T> 借用 Ring<T>,编译器自动禁止“迭代器比容器活得长”。
- 标准库契约:实现 ExactSizeIterator + FusedIterator,使 collect::<Vec<_>>() 内部能一次性预分配容量,避免重复 realloc。
- cargo test 即正确:国内面试官习惯当场
cargo test --release,上述代码在 release 模式下汇编与官方 slice::iter 一致,可直接通过。
拓展思考
- 如何支持 mut 迭代?
实现 IterMut<'a, T> 需要 *mut T 裸指针,并保证 同一元素不会同时通过 next 与 next_back 返回,需引入 &mut self 的独占借用语义。 - 并行迭代器( rayon::ParallelIterator )
国内大数据场景(如 TiKV)会要求手写 chunks 并行扫描;需要实现 IndexedParallelIterator,并证明 split_at 不产生数据竞争。 - async 迭代器( Stream )
在嵌入式网络协议栈中,需把上述 Ring 改造成 异步字节流;此时应实现 futures::Stream,并用 Waker 机制在缓冲区非空时通知任务调度器。 - const 泛型与 SIMD
若缓冲区长度在编译期已知(如音频处理 128 样本帧),可用 const N: usize 参数化,并在 next 里返回 packed_simd2::Simd<[f32; N]>,实现 无分支向量化迭代。 - miri 与 unsafe 审计
国内安全赛道(如区块链底层)会要求 miri --strict-provenance 零报错;手写迭代器若用到裸指针,需配套 #[cfg(test)] mod miri_test 用 miri 跑边界 case,否则无法过 CI。