在 Rust 中实现一个简单的生命周期保护者
在思考一个生命周期问题的时候搞了个微型玩具出来。
假设我们想在 Rust 里实现一个类似下面这样的类型(省略掉所有生命周期参数):
1 | struct Guardian { /* ... */ } |
我们希望这东西能起到如下作用:传给 guard
的引用在 Guardian
实例死亡之前不能失效。例如我们可以有如下 demo 代码:
1 | fn main() { |
上面这段代码要能正确编译,并且如果我们提前把一个曾经传给过 guard
的对象给移走,那么就要报错。
Step 1
首先的想法当然是要求传入 guard
的引用至少具有 &self
的声明周期,于是我们可以写出如下代码:
1 | struct Guardian {} |
这显然是不对的。生命周期参数 'a
会被自动推断为 &self
和 &T
的生命周期中更小的那个,导致它根本不能防止 &T
引用的对象被提前移走。
Step 2
如果 guard
方法上 &self
的生命周期参数会变小,那我们固定住它就好了。为此我们得给 Guardian
加个生命周期。得到的代码如下:
1 | struct Guardian<'a>(PhantomData<&'a ()>); |
遗憾的是,这也是不对的。问题出在 subtyping 上面:&'a Guardian<'a>
对 'a
和 Guardian<'a>
都是协变的,然后 Guardian<'a>
对 'a
也是协变的;于是编译器仍然可以搞一个更小的生命周期 '0
出来,然后把 &'a Guardian<'a>
转成 &'0 Guardian<'0>
,这样就寄了。
Step 3
我们希望 &self
不能对 Self
协变,所以我们可以搞成不变。一种手段是用 &mut self
代替 &self
:
1 | struct Guardian<'a>(PhantomData<&'a ()>); |
这样做又是不对的,它会爆炸。参考上面的 demo,虽然编译器能正确推断 'a
,但是 'a
至少是从 c
出生到 guardian
死亡这么长,而这段里面叠了 3 个对 guardian
的可变引用,显然过不了编译。为此我们得换另一种手段,让 Self
变成不变的,这可以通过把引用包进 UnsafeCell
来实现(或者搞个 PhantomData<&mut ()>
也行):
1 | struct Guardian<'a>(UnsafeCell<&'a PhantomData<()>>); |
然而,这又又是不对的。如果这么实现的话,demo 里面虽然 drop(c)
会报错,但 drop(guardian)
那行同样会报。注释掉 drop(c)
并不解决这个问题,但是如果转而把 drop(guardian)
注释掉,那么反而没错了。问题出在哪呢?编译器会给出类似下面这样的报错:
1 | error[E0505]: cannot move out of `guardian` because it is borrowed |
Fantastic!显然,编译器推断出的 'a
是要超出 drop(guardian)
的,由于我们在 guard
的时候给 &self
上了 'a
的生命周期约束,所以编译器觉得 drop 之后仍然存在对 guardian
的引用。
Final
问题已经很清楚了,guard
方法不能给 &self
施加 'a
生命周期约束,所以我们要换一个更小的生命周期。
1 | struct Guardian<'a>(UnsafeCell<&'a PhantomData<()>>); |
就是它了。现在它完美地完成了我们的预设目标,成为了一个引用的守护者。