一个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
25
struct 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>,就会因为生命周期问题产生编译错误。
个人感觉错误本身对修改的指导意义不大,就不贴在这里了,有需要可以看原文。

评论中提出,去掉parserself的生命周期标记,变成
fn parse<'a>(&mut self, buf: &'a [u8]) -> Option<&'a u8>
这的确可以通过编译,但是这表明的是作为返回值的引用和Parser本身无关,并没有基于它的任何字段。显然这和设计的目标不一致。

分析

在不实现ParseIter<'a>next时,程序可以编译,所以从next的实现着手分析,看他破坏了什么东西。将实现做如下展开:

1
2
3
4
5
fn next<'iter>(&'iter mut self) -> Option<&'a u8> {
let parser: &'a mut Parser = &mut *self.parser;
let buf: &'a [u8] = self.buf;
return Parser::parse(parser, buf);
}

buf的引用可以直接通过Copy复制,所以可以得到&'a [u8]

但是parser属于可变引用,不能Copy,只能通过Deref再重新取引用,如原文作者所说,实际发生的是

1
fn deref_mut<'iter>(&'iter mut &'a mut Parser) -> &'iter mut Self::Target;

返回的引用需要受限于'iter生命周期,但是这里并没有'iter: 'a关系,所以不能保证parser的存活范围,换言之,万一'iter更小,从中借出一个生命周期更长的引用显然不可能。

解决

原文作者并没有给出解决方法。这里给出一个大佬 BurntSushi 在 fst 库中给出的方案:Streamer

在上节分析的最后,我们发现缺少'iter: 'a的保证,如果想加上,就会发想Iterator这个trait阻止你这么做了。Iterator的本意,是希望迭代出的元素的生命周期和自身无关,可以被外界同时持有多个,甚至迭代器被drop也没有关系。但是上述场景不是这样的,每次迭代返回的引用,不可能被同时持有,因为受限制于Parser本身的状态,自然也就和持有&mut Parser引用的ParserIter有关了。因此我们可以说,针对这个场景,Iterator这个trait选错了。

fst中的迭代也是类似的场景,所以制作了一个Streamer的trait。

1
2
3
4
pub trait Streamer<'a> {
type Item: 'a;
fn next(&'a mut self) -> Option<Self::Item>;
}

形式上和Iterator很像,区别在于对迭代器本身和返回值多了'a生命周期绑定,表明返回值的生命周期不能超过迭代器本身。因此迭代的元素不可以被同时持有,要么阅后即焚,要么及时复制,所以得名Stream-流

如果实现Streamer<'a>,整个的生命周期会按照最短的迭代器的声明周期进行对齐,比如buf的长生命周期在编译时缩短(协变),以配合Parser::iter方法。成功通过编译,cool

Bonus

下面是Streamer文档的后续翻译,作为补充

“””

但是本身这个结构非常难用,会涉及到高阶生命周期绑定。在一个函数中使用该trait,Streamer的生命周期和使用函数没有任何关联,必须用一种方法表示Streamer<'a>'a可以对任意生命周期有效,不必受制于函数的调用作用域。大概长这样

1
2
3
fn takes_stream<T, S>(s: S)
where S: for<'a> Streamer<'a, Item=T>
{}

但是这个声明有 三个 问题:

  1. S 没有绑定特定生命周期,实际上大多数 stream 都包含者一个底层状态机的引用,才能迭代访问Fst
  2. 区分 “stream” 和 “stream 构建器” 的概念,往往会给我们带来方便。在标准库中,这对应这 IteratorIntoIterator
  3. Item=T 是非法的,因为 Streamer 的关联类型要求绑定一个生命周期参数,但是我们没有办法给任意的类型构建器标记一个生命周期绑定。(当前的情况里,T就是一个类型构建器,因为它始终需要一个生命周期才能成为一个具体的类型)

因此,我们需要重新写成这样的庞然大物

1
2
3
4
fn takes_stream<'f, I, S>(s: I)
where I: for<'a> IntoStreamer<'a, Into=S, Item=(&'a [u8], Output)>,
S: 'f + for<'a> Streamer<'a, Item=(&'a [u8], Output)>
{}

我们是这样解决问题的:

  1. S现在绑定'f,表示底层stream的生命周期,可能是'static
  2. 类型参数I加入,表示一个知道如何构建 stream 的类型。注意IS并没有共享相同的生命周期,因为高阶生命周期绑定的’a表示任意的生命周期。
  3. T 被替换成了一个具体类型。注意到具体类型在 IS 中是重复的。在 IteratorIntoIterator 中,I可以通过S::Item简写,但是在高阶生命周期绑定中,我们不能访问关联类型。

如你所见,stream 的灵活零欠缺,一丢丢反人类,给了一大堆难以阅读的trait绑定。情况就是这么尴尬,但是没有它,我们又不能组合各种数据流。

唯一的慰藉可能是,完全相同的 trait bound 可以到处用。一旦你领会了精神,剩下的可能就是复制粘贴。

“””

结论

  • Iterator并不是万能的,需要了解它的使用场景和局限
  • 编译错误可能并不能指导修改,程序的语义需要自己把握
  • 单是函数的签名就能反应出整个工作流程,很神奇

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!