一个Rust生命周期问题
给知乎上一个生命周期问题提供的解决办法
问题来源
一个关于rust生命周期的问题分析 - 知乎 (zhihu.com)
相关的 Rust Playground (rust-lang.org)
场景目标是,有一个已经存在的 &[u8]
buffer,一个Parser
结构的parse
方法希望查阅这个buffer,同时修改自身的状态,以自身状态为基础产生一个新的&[u8]
buffer,返回这个新引用。 parse
方法可以多次调用,所以希望在Parser
结构的基础上制作一个迭代器,每次迭代执行parse
,返回构建的 u8
buffer
由此有以下代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25struct Parser {
// ...
}
impl Parser {
pub fn parse<'a>(&'a mut self, buf: &'a [u8]) -> Option<&'a u8> {
todo!()
}
pub fn iter<'a>(&'a mut self, buf: &'a [u8]) -> ParseIter<'a> {
ParseIter { parser: self, buf }
}
}
pub struct ParseIter<'a> {
parser: &'a mut Parser,
buf: &'a [u8],
}
impl<'a> Iterator for ParseIter<'a> {
type Item = &'a u8;
fn next(&mut self) -> Option<Self::Item> {
self.parser.parse(self.buf)
}
}
但是甚至不待使用ParseIter<'a>
,就会因为生命周期问题产生编译错误。
个人感觉错误本身对修改的指导意义不大,就不贴在这里了,有需要可以看原文。
评论中提出,去掉
parser
中self
的生命周期标记,变成fn parse<'a>(&mut self, buf: &'a [u8]) -> Option<&'a u8>
这的确可以通过编译,但是这表明的是作为返回值的引用和Parser
本身无关,并没有基于它的任何字段。显然这和设计的目标不一致。
分析
在不实现ParseIter<'a>
的next
时,程序可以编译,所以从next
的实现着手分析,看他破坏了什么东西。将实现做如下展开:
1 |
|
buf的引用可以直接通过Copy复制,所以可以得到&'a [u8]
。
但是parser
属于可变引用,不能Copy,只能通过Deref
再重新取引用,如原文作者所说,实际发生的是
1 |
|
返回的引用需要受限于'iter
生命周期,但是这里并没有'iter: 'a
关系,所以不能保证parser的存活范围,换言之,万一'iter
更小,从中借出一个生命周期更长的引用显然不可能。
解决
原文作者并没有给出解决方法。这里给出一个大佬 BurntSushi 在 fst 库中给出的方案:Streamer。
在上节分析的最后,我们发现缺少'iter: 'a
的保证,如果想加上,就会发想Iterator
这个trait阻止你这么做了。Iterator的本意,是希望迭代出的元素的生命周期和自身无关,可以被外界同时持有多个,甚至迭代器被drop也没有关系。但是上述场景不是这样的,每次迭代返回的引用,不可能被同时持有,因为受限制于Parser
本身的状态,自然也就和持有&mut Parser
引用的ParserIter
有关了。因此我们可以说,针对这个场景,Iterator
这个trait选错了。
fst中的迭代也是类似的场景,所以制作了一个Streamer
的trait。
1 |
|
形式上和Iterator
很像,区别在于对迭代器本身和返回值多了'a
生命周期绑定,表明返回值的生命周期不能超过迭代器本身。因此迭代的元素不可以被同时持有,要么阅后即焚,要么及时复制,所以得名Stream-流
如果实现Streamer<'a>
,整个的生命周期会按照最短的迭代器的声明周期进行对齐,比如buf
的长生命周期在编译时缩短(协变),以配合Parser::iter
方法。成功通过编译,cool
Bonus
下面是Streamer文档的后续翻译,作为补充
“””
但是本身这个结构非常难用,会涉及到高阶生命周期绑定。在一个函数中使用该trait,Streamer
的生命周期和使用函数没有任何关联,必须用一种方法表示Streamer<'a>
的'a
可以对任意生命周期有效,不必受制于函数的调用作用域。大概长这样
1 |
|
但是这个声明有 三个 问题:
S
没有绑定特定生命周期,实际上大多数 stream 都包含者一个底层状态机的引用,才能迭代访问Fst- 区分 “stream” 和 “stream 构建器” 的概念,往往会给我们带来方便。在标准库中,这对应这
Iterator
和IntoIterator
Item=T
是非法的,因为Streamer
的关联类型要求绑定一个生命周期参数,但是我们没有办法给任意的类型构建器标记一个生命周期绑定。(当前的情况里,T
就是一个类型构建器,因为它始终需要一个生命周期才能成为一个具体的类型)
因此,我们需要重新写成这样的庞然大物
1 |
|
我们是这样解决问题的:
S
现在绑定'f
,表示底层stream的生命周期,可能是'static
- 类型参数
I
加入,表示一个知道如何构建 stream 的类型。注意I
和S
并没有共享相同的生命周期,因为高阶生命周期绑定的’a
表示任意的生命周期。 T
被替换成了一个具体类型。注意到具体类型在I
和S
中是重复的。在Iterator
和IntoIterator
中,I
可以通过S::Item
简写,但是在高阶生命周期绑定中,我们不能访问关联类型。
如你所见,stream 的灵活零欠缺,一丢丢反人类,给了一大堆难以阅读的trait绑定。情况就是这么尴尬,但是没有它,我们又不能组合各种数据流。
唯一的慰藉可能是,完全相同的 trait bound 可以到处用。一旦你领会了精神,剩下的可能就是复制粘贴。
“””
结论
Iterator
并不是万能的,需要了解它的使用场景和局限- 编译错误可能并不能指导修改,程序的语义需要自己把握
- 单是函数的签名就能反应出整个工作流程,很神奇
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!