本文依据原文 Recursive iterators in Rust 内容完整编译,补充了背景解释
这篇文章原本不存在。作者四处寻找,没找到,于是自己写了出来。
这个问题乍看很简单:给一棵树的所有节点写一个深度优先的迭代器。但它背后牵扯出了几个 Rust 初学者乃至中级开发者都很容易踩坑的核心概念:impl Trait 返回值的本质、递归类型的大小问题、以及生命周期标注究竟在标注什么。
一、问题场景:递归数据结构的遍历
假设我们有如下的递归树结构:
rust
struct Node {
values: Vec<i32>,
children: Vec<Node>,
}
这个结构可以表示下面这棵树:
css
[1, 2, 3]
/\
/ \
/ \
/ \
/ \
[4, 5] [6, 7]
现在我们希望对这棵树做深度优先遍历,依次产出所有节点的值,得到序列 [1, 2, 3, 4, 5, 6, 7]。
目标是给 Node 实现一个 values() 方法,返回一个迭代器,能递归地产出根节点及所有子节点的值。
二、直觉写法:为什么编不过?
有 Python 或 Java 经验的开发者,第一反应往往是这样写:
rust
impl Node {
fn values(&self) -> impl Iterator<Item = &i32> {
self.values
.iter()
.chain(self.children.iter().map(|n| n.values()).flatten())
}
}
逻辑很清晰:
self.values.iter()先产出本节点自己的值[1, 2, 3]self.children.iter().map(|n| n.values())对每个子节点递归调用values(),产出一个"迭代器的迭代器",即[[4, 5], [6, 7]].flatten()将其展平为[4, 5, 6, 7].chain(...)将两段拼接起来,得到[1, 2, 3, 4, 5, 6, 7]
逻辑完全正确。但编译器拒绝了它:
rust
error[E0720]: opaque type expands to a recursive type
--> src/main.rs:8:25
|
8 | fn values(&self) -> impl Iterator<Item = &i32> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ expands to self-referential type
|
= note: expanded type is `std::iter::Chain<
std::slice::Iter<'_, i32>,
std::iter::Flatten<
std::iter::Map<
std::slice::Iter<'_, Node>,
[closure@src/main.rs:11:45: 11:59]
>
>
>`
错误的本质:类型展开成了自引用结构
把编译器展开的类型整理一下,它大概长这样:
ini
T = Chain(
Slice,
Flatten(
Map(Slice, T)
)
)
T 的定义里包含了 T 自身。这是一个自引用类型(self-referential type),其大小在编译期无法确定。
编译器并没有说代码逻辑错了,它说的是:我没法在编译期知道这个类型到底有多大。
三、深挖原因:impl Trait 是怎么工作的?
要理解为什么编译器需要知道类型的大小,先得搞清楚 impl Trait 返回值的本质。
3.1 你不能直接返回一个 trait
rust
// 无法编译
fn foo() -> fmt::Display {
"hi"
}
在 Rust 中,trait 本身不是一个具体类型,它只是对某些能力的描述。函数返回的必须是一个具体类型(concrete type),哪怕这个类型实现了某个 trait。
3.2 impl Trait 是"让编译器推断具体类型"的语法糖
rust
// 可以编译
fn foo() -> impl fmt::Display {
"hi"
}
impl fmt::Display 的意思是:我返回"某个实现了 fmt::Display 的具体类型",具体是什么类型由编译器从函数体里推断出来。在上面的例子里,这个类型是 &'static str。
3.3 具体类型必须在编译期确定大小
写出这段代码时:
rust
fn main() {
let s = foo();
println!("{}", s);
}
变量 s 是分配在栈 上的。栈上的每一块空间都必须在编译期就知道大小。因此,编译器必须在编译期知道 foo() 返回的具体类型是什么,以及它占多少字节。
这就是问题所在:我们的 values() 方法,其返回类型展开后是一个自引用类型,其大小是无限的,编译器无法确定,也就无法在栈上为它分配空间。
3.4 impl Trait 类似于"返回类型参数"
可以把 impl Trait 理解成一种特殊的泛型,只不过类型参数不是由调用方指定,而是由函数体推断:
rust
// 类似于(但并不完全等价于)
fn foo<T: fmt::Display>() -> T {
"hi" // 这实际上无法编译,因为 "hi" 不一定是 T
}
区别在于:impl Trait 版本隐藏了真实类型,调用方只看到 trait,但编译器内部仍然知道并处理的是具体类型。
四、解法一(初版,不完整):自定义迭代器结构体
既然不能返回一个自引用的 opaque type,那就换一个思路:自己手写一个迭代器结构体。
4.1 第一个想法:用 trait 作为字段类型
rust
// 无法编译
struct NodeIter<'a> {
viter: Iterator<Item = &'a i32>,
citer: Iterator<Item = &'a Node>,
}
这同样不行------结构体的字段必须是具体类型,不能直接是 trait。
4.2 用 Box 包装:让类型变成固定大小
rust
struct NodeIter<'a> {
viter: Box<Iterator<Item = &'a i32>>,
citer: Box<Iterator<Item = &'a Node>>,
}
Box<T> 是一个固定大小的胖指针(在 64 位系统上是 16 字节),它把真实数据放在堆上,结构体自身只存储指针。这样 NodeIter 的大小就是固定的,可以在栈上分配。
同时,Box<dyn Trait> 使用了动态分发(虚函数表),允许在运行时持有不同的具体类型。
4.3 实现 Iterator trait
rust
impl<'a> Iterator for NodeIter<'a> {
type Item = &'a i32;
fn next(&mut self) -> Option<Self::Item> {
unimplemented!()
}
}
然后在 Node 上提供 values() 方法:
rust
impl Node {
fn values<'a>(&'a self) -> NodeIter<'a> {
NodeIter {
// 还未能编译
viter: Box::new(self.values.iter()),
citer: Box::new(self.children.iter()),
}
}
}
但这里还会遇到一个新的编译错误。
五、生命周期标注的精髓:不只是引用,也是泛型约束
5.1 Box 里的 trait object 默认假设 'static
编译错误如下:
rust
error[E0495]: cannot infer an appropriate lifetime for lifetime parameter
...
= note: but, the lifetime must be valid for the static lifetime...
= note: ...so that the expression is assignable:
expected std::boxed::Box<(dyn std::iter::Iterator<Item=&i32> + 'static)>
found std::boxed::Box<dyn std::iter::Iterator<Item=&i32>>
关键在最后两行。编译器期望的是 Box<dyn Iterator + 'static>,而我们实际提供的是 Box<dyn Iterator>。
当你写 Box<dyn Trait> 时,Rust 默认它的生命周期是 'static,意思是:里面的 trait object 可以活任意长时间(不依赖任何外部借用)。但我们的迭代器借用了 self,它的生命周期受限于 'a,并非 'static。
5.2 解决方案:给 Box 里的 trait 加上生命周期约束
rust
// 修改前
struct NodeIter<'a> {
viter: Box<Iterator<Item = &'a i32>>,
citer: Box<Iterator<Item = &'a Node>>,
}
// 修改后
struct NodeIter<'a> {
viter: Box<Iterator<Item = &'a i32> + 'a>,
citer: Box<Iterator<Item = &'a Node> + 'a>,
}
+ 'a 加在 trait 后面,意思是:这个 trait object(迭代器本身)的生命周期至少要和 'a 一样长。
这里涉及一个细微但重要的区别:
Iterator<Item = &'a i32>说的是:迭代器产出的引用 活至少'a这么长+ 'a说的是:迭代器自身 至少活'a这么长
两个约束针对的对象不同,缺一不可。前者保证你拿到的值不会悬空,后者保证迭代器自身不会在使用期间被释放。
5.3 完整的自定义迭代器实现
有了正确的结构体定义,再来实现 next() 方法:
rust
impl<'a> Iterator for NodeIter<'a> {
type Item = &'a i32;
fn next(&mut self) -> Option<Self::Item> {
// 如果当前节点自己的值还没取完
if let Some(val) = self.viter.next() {
// 直接返回
Some(val)
} else {
// 自己的值取完了,看看还有没有子节点
if let Some(child) = self.citer.next() {
// 有子节点,就把 viter 替换成该子节点的值迭代器
self.viter = Box::new(child.values());
// 然后递归调用 next():
// 如果子节点有值,立即返回;
// 如果子节点为空,继续找下一个子节点
self.next()
} else {
// 没有更多子节点了,迭代结束
None
}
}
}
}
逻辑分三层:
- 本节点的值还没遍历完?直接返回。
- 本节点的值遍历完了,但还有子节点?把
viter替换为该子节点的迭代器,然后递归调用self.next()。 - 子节点也没了?返回
None,迭代结束。
values() 方法的完整实现:
rust
impl Node {
fn values<'a>(&'a self) -> NodeIter<'a> {
NodeIter {
viter: Box::new(self.values.iter()),
citer: Box::new(self.children.iter()),
}
}
}
5.4 验证:用一个完整示例测试
rust
fn main() {
let n = Node {
values: vec![1, 2, 3],
children: vec![
Node {
values: vec![4, 5],
children: vec![],
},
Node {
values: vec![6, 7],
children: vec![],
},
],
};
let v: Vec<_> = n.values().collect();
println!("{:?}", v);
}
输出:
csharp
[1, 2, 3, 4, 5, 6, 7]
符合预期。
六、解法二(更简洁):直接返回 Box<Iterator + 'a>
既然理解了生命周期标注的作用,我们可以回到最初的直觉写法,只改一处:不用 impl Iterator,改用 Box<dyn Iterator + 'a>。
6.1 第一次尝试:直接 Box 化
rust
impl Node {
pub fn values<'a>(&'a self) -> Box<Iterator<Item = &i32>> {
Box::new(
self.values
.iter()
.chain(self.children.iter().map(|n| n.values()).flatten()),
)
}
}
又遇到了熟悉的生命周期错误:
rust
error[E0495]: cannot infer an appropriate lifetime...
= note: ...so that the expression is assignable:
expected std::boxed::Box<(dyn std::iter::Iterator<Item=&'a i32> + 'static)>
found std::boxed::Box<dyn std::iter::Iterator<Item=&i32>>
是的,和之前一模一样的问题:Box 里的 trait object 默认要求 'static。
6.2 加上生命周期标注,搞定
rust
// 修改前
pub fn values<'a>(&'a self) -> Box<Iterator<Item = &i32>> {
// 修改后
pub fn values<'a>(&'a self) -> Box<Iterator<Item = &i32> + 'a> {
完整代码:
rust
impl Node {
pub fn values<'a>(&'a self) -> Box<Iterator<Item = &i32> + 'a> {
Box::new(
self.values
.iter()
.chain(self.children.iter().map(|n| n.values()).flatten()),
)
}
}
这个版本可以编译、可以运行,输出结果同样是 [1, 2, 3, 4, 5, 6, 7]。
6.3 为什么 Box 化能解决递归类型的问题?
回想一下最初的错误:类型 T = Chain(Slice, Flatten(Map(Slice, T))) 是一个自引用类型,大小无限。
用 Box<dyn Iterator> 之后,递归处被截断了。调用 n.values() 返回的是一个 Box<dyn Iterator>,这是一个固定大小的胖指针(指针 + vtable),不再是具体的迭代器链类型。编译器不需要展开递归,只需要知道 Box 的大小即可。
这就是为什么 Box 是 Rust 中处理递归类型的标准手段------它把"无限大的类型"变成了"指向堆上数据的固定大小指针"。
七、两种解法的对比
| 维度 | 解法一(自定义迭代器结构体) | 解法二(直接返回 Box) |
|---|---|---|
| 代码量 | 多(需要定义结构体、实现 Iterator trait) | 少(几行搞定) |
| 可读性 | 逻辑显式,每一步一目了然 | 简洁,依赖组合子 |
| 性能 | 略好(减少了部分动态分发) | 略低(额外的堆分配 + vtable 查找) |
| 适用场景 | 需要精细控制、或返回类型需要被命名 | 快速实现、原型开发 |
| 关键学习点 | 自定义状态机、生命周期标注的精确语义 | Box<dyn Trait + 'a> 的用法 |
对于大多数实际项目,如果遍历性能不是瓶颈,解法二的简洁性更有吸引力。如果需要把返回类型作为公开 API 的一部分(让调用方能命名这个类型),解法一的命名结构体更合适。
八、总结:这次"踩坑"教给我们什么
关于 impl Trait
impl Trait 返回值不是魔法。它仍然对应一个具体类型,编译器会在编译期推断它是什么,并为它在栈上分配空间。这意味着:如果这个具体类型是递归的(自引用的),编译器就无法确定它的大小,编译失败。
关于 Rust 泛型与 Java 泛型的区别
Java 的泛型是类型擦除的(erasure):运行时所有泛型类型都变成 Object,没有额外的类型信息。Rust 的泛型是具象化的(reified):每一个具体类型组合都会生成一份单独的代码,编译器在编译期必须知道所有的类型信息。这对性能有好处(零开销抽象),但也意味着类型系统更严格,在这种递归情形下更容易遇到限制。
关于生命周期标注的两个层次
写 Box<dyn Iterator<Item = &'a i32> + 'a> 时,有两个生命周期约束同时在起作用:
Item = &'a i32:迭代器产出的引用,其有效期为'a。保证你拿到的值不会变成悬垂引用。+ 'a:迭代器对象本身,其有效期为'a。保证迭代器不会在使用期间被释放(因为它借用了外部的self)。
这两个约束针对不同的对象,都是必要的。初学者容易只想到第一个,忽略第二个。
关于 Box 在递归类型中的作用
当你有一个自引用类型时,在递归点插入 Box 可以打破无限递归,因为 Box<T> 的大小是固定的(一个指针的大小),不管 T 有多复杂。这个技巧不仅适用于迭代器,在定义递归数据结构(如链表、树)时同样常用。
附录:关键代码对照
最终正确的解法一(自定义迭代器)
rust
struct Node {
values: Vec<i32>,
children: Vec<Node>,
}
struct NodeIter<'a> {
viter: Box<dyn Iterator<Item = &'a i32> + 'a>,
citer: Box<dyn Iterator<Item = &'a Node> + 'a>,
}
impl<'a> Iterator for NodeIter<'a> {
type Item = &'a i32;
fn next(&mut self) -> Option<Self::Item> {
if let Some(val) = self.viter.next() {
Some(val)
} else {
if let Some(child) = self.citer.next() {
self.viter = Box::new(child.values());
self.next()
} else {
None
}
}
}
}
impl Node {
fn values<'a>(&'a self) -> NodeIter<'a> {
NodeIter {
viter: Box::new(self.values.iter()),
citer: Box::new(self.children.iter()),
}
}
}
最终正确的解法二(直接返回 Box)
rust
impl Node {
pub fn values<'a>(&'a self) -> Box<dyn Iterator<Item = &i32> + 'a> {
Box::new(
self.values
.iter()
.chain(self.children.iter().map(|n| n.values()).flatten()),
)
}
}
两种解法都能通过编译,都能产出正确结果 [1, 2, 3, 4, 5, 6, 7],选哪个取决于你更看重代码的简洁性还是显式控制。