原文链接:https://doc.rust-lang.org/nomicon/dropck.html
Drop检查
我们已经知道生命周期给我们提供了一些很简单的规则,以保证我们永远不会读取悬垂引用。但是,到目前为止我们只以包容性(inclusive)的方式与 存活(outlives)关系进行交互。也就是说,当我们写'a: 'b
的时候,'a
其实也可以和'b
一样 长。乍一看,这一点没什么意义。本来也不会有两个东西被同时销毁的,不是吗?我们去掉下面的let
表达式的语法糖看看:
let x;
let y;
{
let x;
{
let y;
}
}
每一个都创建了自己的作用域,可以很清楚地看出来一个在另一个之前被销毁。但是,如果是下面这样的呢?
存在一些更复杂的情况,使得无法使用作用域进行去糖化,但是仍然定义了顺序──变量以其定义的相反顺序校销毁,结构和元组的字段以其定义的顺序反向销毁。RFC 1857 中有一些有关删除顺序的更多详细信息。
让我们看以下例子:
let tuple = (vec![], vec![]);
左边向量先被销毁。但这是否意味着右边的向量在借用检查器的眼中严格地存活?这个问题的答案是 否。借用检查器可以单独跟踪元组的字段,但是对向量元素(通过纯库代码手动删除,借用检查器无法理解这些代码),它仍然无法确定一个元素比另一个元素活得更久,
可是我们为什么要关心这个?因为如果系统不够小心,就可能搞出来悬垂指针。考虑下面这个简单的程序:
struct Inspector<'a>(&'a u8); struct World<'a> { inspector: Option<Inspector<'a>>, days: Box<u8>, } fn main() { let mut world = World { inspector: None, days: Box::new(1), }; world.inspector = Some(Inspector(&world.days)); }
这段程序是正确且可以正常编译的。days
并不严格地比inspector
存活得更长,但这没什么关系。只要inspector
还存活着,days
就一定也活着。
可如果我们添加一个析构函数,程序就不能编译了!
struct Inspector<'a>(&'a u8); impl<'a> Drop for Inspector<'a> { fn drop(&mut self) { println!("I was only {} days from retirement!", self.0); } } struct World<'a> { inspector: Option<Inspector<'a>>, days: Box<u8>, } fn main() { let mut world = World { inspector: None, days: Box::new(1), }; world.inspector = Some(Inspector(&world.days)); // `days` 刚好被先销毁了 // 当 Inspector 被销毁时,它会尝试读取被释放的内存! }
error[E0597]: `world.days` does not live long enough
--> src/main.rs:19:38
|
19 | world.inspector = Some(Inspector(&world.days));
| ^^^^^^^^^^^ borrowed value does not live long enough
...
22 | }
| -
| |
| `world.days` dropped here while still borrowed
| borrow might be used here, when `world` is dropped and runs the destructor for type `World<'_>`
你可以尝试更改字段的顺序或使用元组代替结构体,但仍然无法编译。
实现Drop
使得Inspector
可以在销毁前执行任意的代码。一些通常认为和它生命周期一样长的类型可能实际上比它先销毁,而这会有潜在的问题。
有意思的是,只有泛型需要考虑这个问题。如果不是泛型的话,那么唯一可用的生命周期就是'static
,而它确确实实会 永远 存在。这也就是这一问题被称之为“安全泛型销毁”的原因。安全泛型销毁是通过 销毁检查器 强制执行的。我们还未涉及到销毁检查器判断类型是否可用的细节,但其实我们之前已经讨论了这个问题的最主要规则:
一个安全地实现Drop的类型,它的泛型参数生命周期必须严格地长于它本身
遵守这一规则(大部分情况下)是满足借用检查器要求的必要条件,同时是满足安全要求的充分非必要条件。也就是说,如果类型遵守上述规则,它就一定可以安全地drop。
之所以并不总是满足借用检查器要求的必要条件,是因为有时类型借用了数据但是在Drop的实现里没有访问这些数据,或者是因为我们知道特定的销毁顺序,并且即使借用检查器不知道,借用的数据仍然很好。
例如,上面的Inspector
的这一变体就不会访问借用的数据:
struct Inspector<'a>(&'a u8, &'static str); impl<'a> Drop for Inspector<'a> { fn drop(&mut self) { println!("Inspector(_, {}) knows when *not* to inspect.", self.1); } } struct World<'a> { inspector: Option<Inspector<'a>>, days: Box<u8>, } fn main() { let mut world = World { inspector: None, days: Box::new(1), }; world.inspector = Some(Inspector(&world.days, "gadget")); // `days` 刚好被先销毁了 // 即使 Inspector 被销毁,它的析构器也不会读取被引用的 `days`。 }
同样,这个变体也不会访问借用的数据:
struct Inspector<T>(T, &'static str); impl<T> Drop for Inspector<T> { fn drop(&mut self) { println!("Inspector(_, {}) knows when *not* to inspect.", self.1); } } struct World<T> { inspector: Option<Inspector<T>>, days: Box<u8>, } fn main() { let mut world = World { inspector: None, days: Box::new(1), }; world.inspector = Some(Inspector(&world.days, "gadget")); // `days` 刚好被先销毁了 // 即使 Inspector 被销毁,它的析构器也不会读取被引用的 `days`。 }
但是,借用检查器在分析main
函数的时候,上面两段代码 都 会被拒绝,并指出days
存活得不够长。
这是因为,当借用检查分析main
函数的时候,它并不知道每个Inspector
的Drop
实现的内部细节。它只知道inspector的析构函数有访问借用数据的可能。
因此,drop检查器强制要求一个值借用的所有数据的生命周期必须严格长于值本身。
留一个后门
上面的类型检查的规则在未来有可能会松动。
当前的分析方法是很保守甚至苛刻的,它强制要求一个值借用的数据必须比值本身长寿,以保证绝对的安全。
未来的版本中,分析过程会更加精细,以减少安全的代码被拒绝的情况。比如上面的两个Inspector
,它们知道在销毁过程中不应该被检查。
同时,有一个还未稳定的属性可以用来(非安全地)声明类型的析构函数 保证 不会访问过期的数据,即使类型的签名显示有这种可能存在。
这个属性是may_dangle
,在RFC 1327中被引入。我们可以这样将其放在上面的Inspector
例子里:
#![feature(dropck_eyepatch)] struct Inspector<'a>(&'a u8, &'static str); unsafe impl<#[may_dangle] 'a> Drop for Inspector<'a> { fn drop(&mut self) { println!("Inspector(_, {}) knows when *not* to inspect.", self.1); } } struct World<'a> { days: Box<u8>, inspector: Option<Inspector<'a>>, } fn main() { let mut world = World { inspector: None, days: Box::new(1), }; world.inspector = Some(Inspector(&world.days, "gatget")); }
使用这个属性要求Drop
的实现被标为unsafe
,因为编译器将不会检查有没有过期的数据(比如self.0
)被访问。
这个属性可以赋给任意数量的生命周期和类型参数。下面这个例子里,我们声明我们不会访问有生命周期'b
的引用背后的数据,而类型T
也只会被用来转移或销毁。但是我们没有为'a
和U
添加属性,因为我们确实会用到这个生命周期和类型:
#![allow(unused)] fn main() { use std::fmt::Display; struct Inspector<'a, 'b, T, U: Display>(&'a u8, &'b u8, T, U); unsafe impl<'a, #[may_dangle] 'b, #[may_dangle] T, U: Display> Drop for Inspector<'a, 'b, T, U> { fn drop(&mut self) { println!("Inspector({}, _, _, {})", self.0, self.3); } } }
上面的例子中,哪些数据不会被用到是一目了然的。但是,有时候这些泛型参数会被间接地访问。间接访问的形式包括:
- 使用回调函数
- 通过调用trait方法
(在日后的版本里可能增加其他间接访问的途径。)
以下是使用回调的例子:
#![allow(unused)] fn main() { struct Inspector<T>(T, &'static str, Box<for <'r> fn(&'r T) -> String>); impl<T> Drop for Inspector<T> { fn drop(&mut self) { // 如果T的类型是&'a _,self.2的调用可能访问借用的数据 println!("Inspector({}, {}) unwittingly inspects expired data.", (self.2)(&self.0), self.1); } } }
这是trait方法调用的例子:
#![allow(unused)] fn main() { use std::fmt; struct Inspector<T: fmt::Display>(T, &'static str); impl<T: fmt::Display> Drop for Inspector<T> { fn drop(&mut drop) { // 下面有一个对<T as Display>::fmt的隐藏调用, // 当T的类型是&'a _时,可能访问借用数据 println!("Inspector({}, {}) unwittingly inspects expired data.", self.0, self.1); } } }
当然,这些访问可以进一步地被隐藏在其他的析构函数调用的方法里,而不仅是直接写在函数中。
上面的几个例子里,&'a u8
都在析构函数里被访问了。如果给它添加#[may_dangle]
属性,这些类型很可能会产生借用检查器无法捕捉的错误,引发不可预料的灾难。所以最好能避免使用这个属性。
有关顺序的相关说明
虽然定义了结构内部字段的销毁顺序,但是依赖它是脆弱而微妙的。当顺序很重要时,最好使用ManuallyDrop
包装器。
drop检查的故事讲完了吗?
我们发现,在写非安全代码时,其实并不用关心是否满足drop检查器的要求。不过有一个特殊的场景是例外的,我们将在下一章讲到它。