在 Rust 中实现一个简单的生命周期保护者

在思考一个生命周期问题的时候搞了个微型玩具出来。

假设我们想在 Rust 里实现一个类似下面这样的类型(省略掉所有生命周期参数):

1
2
3
4
struct Guardian { /* ... */ }
impl Guardian {
fn guard<T: ?Sized>(&self, _: &T) {}
}

我们希望这东西能起到如下作用:传给 guard 的引用在 Guardian 实例死亡之前不能失效。例如我们可以有如下 demo 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let a = Box::new(1);

let guardian = Guardian::new();

let b = Box::new(2);
let c = Box::new(3);

guardian.guard(&a);
guardian.guard(&b);
guardian.guard(&c);

// Uncomment this line to get a compile error!
// drop(c);

drop(guardian);
}

上面这段代码要能正确编译,并且如果我们提前把一个曾经传给过 guard 的对象给移走,那么就要报错。

Step 1

首先的想法当然是要求传入 guard 的引用至少具有 &self 的声明周期,于是我们可以写出如下代码:

1
2
3
4
struct Guardian {}
impl Guardian {
fn guard<'a, T: ?Sized>(&'a self, _: &'a T) {}
}

这显然是不对的。生命周期参数 'a 会被自动推断为 &self&T 的生命周期中更小的那个,导致它根本不能防止 &T 引用的对象被提前移走。

Step 2

如果 guard 方法上 &self 的生命周期参数会变小,那我们固定住它就好了。为此我们得给 Guardian 加个生命周期。得到的代码如下:

1
2
3
4
struct Guardian<'a>(PhantomData<&'a ()>);
impl<'a> Guardian<'a> {
fn guard<'b, T: ?Sized>(&'a self, _: &'b T) where 'b: 'a {}
}

遗憾的是,这也是不对的。问题出在 subtyping 上面:&'a Guardian<'a>'aGuardian<'a> 都是协变的,然后 Guardian<'a>'a 也是协变的;于是编译器仍然可以搞一个更小的生命周期 '0 出来,然后把 &'a Guardian<'a> 转成 &'0 Guardian<'0>,这样就寄了。

Step 3

我们希望 &self 不能对 Self 协变,所以我们可以搞成不变。一种手段是用 &mut self 代替 &self

1
2
3
4
struct Guardian<'a>(PhantomData<&'a ()>);
impl<'a> Guardian<'a> {
fn guard<'b, T: ?Sized>(&'a mut self, _: &'b T) where 'b: 'a {}
}

这样做又是不对的,它会爆炸。参考上面的 demo,虽然编译器能正确推断 'a,但是 'a 至少是从 c 出生到 guardian 死亡这么长,而这段里面叠了 3 个对 guardian 的可变引用,显然过不了编译。为此我们得换另一种手段,让 Self 变成不变的,这可以通过把引用包进 UnsafeCell 来实现(或者搞个 PhantomData<&mut ()> 也行):

1
2
3
4
struct Guardian<'a>(UnsafeCell<&'a PhantomData<()>>);
impl<'a> Guardian<'a> {
fn guard<'b, T: ?Sized>(&'a mut self, _: &'b T) where 'b: 'a {}
}

然而,这又又是不对的。如果这么实现的话,demo 里面虽然 drop(c) 会报错,但 drop(guardian) 那行同样会报。注释掉 drop(c) 并不解决这个问题,但是如果转而把 drop(guardian) 注释掉,那么反而没错了。问题出在哪呢?编译器会给出类似下面这样的报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
error[E0505]: cannot move out of `guardian` because it is borrowed
--> src/main.rs:32:10
|
21 | let guardian = Guardian::new();
| -------- binding `guardian` declared here
...
26 | guardian.guard(&a);
| ------------------ borrow of `guardian` occurs here
...
32 | drop(guardian);
| ^^^^^^^^
| |
| move out of `guardian` occurs here
| borrow later used here

Fantastic!显然,编译器推断出的 'a 是要超出 drop(guardian) 的,由于我们在 guard 的时候给 &self 上了 'a 的生命周期约束,所以编译器觉得 drop 之后仍然存在对 guardian 的引用。

Final

问题已经很清楚了,guard 方法不能给 &self 施加 'a 生命周期约束,所以我们要换一个更小的生命周期。

1
2
3
4
5
6
7
8
9
struct Guardian<'a>(UnsafeCell<&'a PhantomData<()>>);
impl<'a> Guardian<'a> {
fn guard<'b, 'c, T: ?Sized>(&'b self, _: &'c T)
where
'c: 'a,
'a: 'b,
{
}
}

就是它了。现在它完美地完成了我们的预设目标,成为了一个引用的守护者。