1. 摘要
当我们在写工厂方法时,有时会希望同时返回两个值:一个目标对象 + 对该对象某个成员的引用。这种设计初听起来可能非常不合理,但是当使用某些封装不太完美的第三方库时是可能出现的。举个例子:我们需要一个对象 A(A 由第三方库提供,因而无法改造),而又要构造一个工具对象 B,其中 B 又持有 A 中某个成员的引用。
显然,A 和 B 是强绑定的共生关系,因此我们希望将 A 和 B 保存在同一个类里,让他们的生命周期同步。只要我们确保它俩一同构造、一同销毁,也绝不修改 B 引用的指向,那么内存安全是可以得到保证的。然而 rust 编译器会阻止我们这么做,这是因为其预防悬垂引用规则禁止我们构建一个可能指向无效对象的引用,即禁止持有一个临时引用的 B 作为工厂方法返回值的一部分。这时,我们需要想一些办法来绕开这条规则。
本文给出了一种通过 unsafe
方法 std::mem::transmute
,强制修改引用的生命周期,从而绕开编译器的检查的方法 。
2. 举个例子
对于引用对象,我们用一个简单的 struct
作为例子:
struct Referee(i64);
impl Referee {
fn get_mut(&mut self) -> &mut i64 {
&mut self.0
}
}
之前提到,只要能保证对象本身和其部分引用同步销毁(再加上避免修改引用指向对象),则理论上是能确保内存安全的。因此我们构建一个 Bound
类,存放这两个成员。为了让问题简单化,我们直接将另一个成员设计成对 i64
的引用,即:
struct Bound<'a> {
reference: &'a i64,
inner: Referee,
}
接下来就是工厂方法了。在不考虑 rust 悬垂引用规则时,我们可能会想要这么写:
fn create_bound<'a>() -> Bound<'a> {
let mut referee = Referee(666);
Bound {
reference: referee.get_mut(),
inner: referee,
}
}
果不其然,编译器报错:
error[E0515]: cannot return value referencing local variable `referee`
--> src/main.rs:113:5
|
113 | / Bound {
114 | | reference: referee.get_mut(),
| | ------- `referee` is borrowed here
115 | | inner: referee,
116 | | }
| |_____^ returns a value referencing data owned by the current function
对于绝大多数情况,上述操作确实会产生悬垂引用,编译器的报错是合理的。但对于这个特殊的场景,我们也无法对编译器进行解释来说服它。因此,为了解决眼前的问题,只能掏出 unsafe 来催眠了。一个可行的写法是:
fn create_bound<'a>() -> Bound<'a> {
let mut referee = Referee(666);
Bound {
reference: unsafe { std::mem::transmute(referee.get_mut()) },
inner: referee,
}
}
std::mem::transmute
可以将一块内存强制解释成另一种对象,即实现无条件强制类型转换,类似于 C++ 中的 reinterpret_cast
。而在引入了生命周期机制的 rust 中,transmute
不仅能转换对象的类型,还能延长或缩短对象的生命周期。
在上述代码中, std::mem::transmute
的作用正是把其内含的 referee.get_mut())
的返回值的生命周期从局部变量强制转换为与返回值一致的 'a
。
3. 补充之一:'a
不如改成 'static
?
既然已经在用黑魔法了,那么 Bound
类的 'a
标注显然已经失去了意义。倒不如直接改成 'static
,这样也能引导使用者产生疑问,进而发现这样封装的高危性,从而用的更加谨慎。
4. 补充之二:从几行代码看 rust 成员构建顺序
在刚刚的代码段里,工厂方法 create_bound()
在构造返回值时,这么写是没问题的:
Bound {
reference: referee.get_mut(),
inner: referee,
}
但是如果这么写就不行了:
Bound {
inner: referee,
reference: referee.get_mut(),
}
编译器报错内容是:
error[E0382]: borrow of moved value: `referee`
--> src/main.rs:106:49
|
100 | let mut referee = Referee(666);
| ----------- move occurs because `referee` has type `Referee`, which does not implement the `Copy` trait
...
105 | inner: referee,
| ------- value moved here
106 | reference: unsafe { std::mem::transmute(referee.get_mut()) },
| ^^^^^^^ value borrowed here after move
可见,rust 在构造对象时,是按照代码写的顺序来初始化各个成员的,因此这里先 move 了 referee 以后,就不能再使用之了。这个性质与 C++ 的构造函数一定按照类型定义的顺序来初始化成员不一致。即在 C++ 中:
class MyClass {
public:
int a;
int b;
MyClass(int x, int y) : b(y), a(x) {}
};
即使我构造函数的初始化列表里先写了 b
,实际上最先构造的依然是声明顺序中的 a