Things do not always work out. I'm working for a way out.
Published Dec 16, 2024
Rust 使用生命周期来跟踪引用和所有权,而生命周期检查实现的背后是 subtyping(子类型) 和 variance(变型)。
Subtyping 和 variance 本是继承中使用的概念,但是 Rust 中并没有继承机制,Rust 中这两概念用在了生命周期检查上。
先看一个例子:
// Note: debug expects two parameters with the *same* lifetime
fn debug<'a>(a: &'a str, b: &'a str) {
println!("a = {a:?} b = {b:?}");
}
fn main() { // 'static
let hello: &'static str = "hello";
{ // 'world
let world = String::from("world");
let world = &world; // 'world has a shorter lifetime than 'static
debug(hello, world);
}
}
一个保守的生命周期实现不会允许这样的行为,因为 'static 并不等于 'world。但是直觉告诉我们,这段代码这样做并没有安全问题。尝试编译这段代码,发现确实是可以正常通过检查的。这样灵活的约束得益于 subtyping。
首先我们要知道:
'long <: 'short 表示 'long 是 'short 的 subtype;当且仅当 'long 定义的代码区域完全包括了 'short 定义的区域回到上面的例子,我们可以知道 'static <: 'world。
同时,debug(&'static str, &'world world) 合法,因此我们说 &'static str 是 &'a str 的一个 subtype
可以认为在这里 &'static str 被“降级”为 &'a str,从而匹配了 debug 的函数签名。
也就是说,尽管生命周期不完全匹配,只要参数是函数签名要求的subtype,这个行为就是允许的。
Variance 描述了 subtype 属性在不同情况下是否可以保持下去的性质。
给定类型 F<T>,其中 T 为 Sub or Super, variance 定义了三种关系:
F<Sub> 是 F<Super> 的 subtype,那么我们说 F 是 co-variant (协变)的;就好像 subtype 属性传递下去了F<Super> 是 F<Sub> 的 subtype,那么我们说 F 是 contra-variant (逆变)的;就好像 subtype 属性扭转了;这种情况在实际中非常少出现F 是 in-variant (不变)的;不能应用 subtype 属性各类型和它们的 variances 如下表:
| ‘a | T | U | |
|---|---|---|---|
&'a T |
covariant | covariant | |
&'a mut T |
covariant | invariant | |
Box<T> |
covariant | ||
Vec<T> |
covariant | ||
UnsafeCell<T> |
invariant | ||
Cell<T> |
invariant | ||
fn(T) -> U |
contravariant | covariant | |
*const T |
covariant | ||
*mut T |
invariant |
简单来说:
Vec<T> 和其他指针和集合同 Box<T>Cell<T> 和其他具有内部可变性的同 UnsafeCell<T>UnsafeCell<T> 因为它的内部可变性因此同 &mut T*const T 同 &T*mut T 同 &mut T下面结合例子来理解这三种关系。
&'a T over 'a is covariantIf 'a <: 'b, then &'a T <: &'b T.
fn debug<T: std::fmt::Debug>(a: T, b: T) {
println!("a = {a:?} b = {b:?}");
}
fn main() {
let a: &'static = "hello";
{ // 'b
let string = String::from("world");
let b: &str = &string;
debug(a, b);
}
}
debug 的参数必须是相同类型 T,在这里类型推断为 &'a str,完整函数签名:
fn debug<'a>(a: &'a str, b: &'a str)
main 中有 a: &'static str 和 b: &'b str,且 'static <: 'b。
因为 &'a T 对 'a 是 covariant 的,所以 &'static str 在这里可以“降级”为 &'b str,被 debug 接受。
&'a T over T is covariantIf t1 <: t2, then &'a t1 <: &'b t2.
fn debug<'a, T: std::fmt::Debug>(a: &'a T, b: T) {
println!("a = {a:?} b = {b:?}");
}
fn main() {
let a: &'static str = "hello";
{ // 'b
let string = String::from("world");
let b: &str = &string;
debug(&a, b);
}
}
这次参数变成了 (a: &'a T, b: T)。
我们传入的参数为 (&'b &'static str, &'b str)
类型推导过程中,
当 T = &'static str',编译器判定不能满足生命周期约束,因为 &'b str <: &'static 不成立;
当 T = &'b str,根据规则, 'static <: 'b => &'static str <: &'b str。
从而可以将 a: &'a &'static str “降级”为 &'a &'b str,成功匹配了函数签名。
&'a mut T over 'a is covariantIf 'a <: 'b, then &'a mut T <: &'b mut T.
fn debug<'a, T: std::fmt::Debug>(a: &'a mut T) {
println!("a={:?}", a);
}
fn main() {
let mut a: &'static str = "hello";
debug(&mut a);
}
参数:(&'static mut &'static str)
根据规则: 'static <: 'a => &'static mut &'static str <: &'a mut &'static str
类型推断 T = &'static str,匹配成功。
&'a mut T' over T is invariantEven if t1 <: t2, there’s no &'a mut t1 <: &'a mut t2.
fn debug<'a, T: std::fmt::Debug>(a: &'a mut T, b: &'a T) {
println!("a={:?}, b={:?}", a, b);
}
fn main() {
let mut a: &'static str = "hello";
{ // 'b
let string = String::from("world");
let b = string.as_str();
debug(&mut a, &b); // won't compile
}
}
参数:(&'b mut &'static str, &'b '&'b str)
尽管有 &'static str <: &'b str(根据 &'a T over 'a is covariant)
我们不能得出 &'b mut &'static str <: &'b mut &'b str
因此 T 必须是 &'static str (即不允许“降级”)
而 &'b str <: &'static str 不成立,所以两个参数的 T 无法统一,编译无法通过。
TIP:
下面的代码是可以正常运行的
fn debug<T: std::fmt::Debug>(a: &mut T, b: T) {
println!("a={:?}, b={:?}", a, b);
}
fn debug_str<T: std::fmt::Debug>(a: &T) {
println!("{:?}", a);
}
fn main() {
let mut a = "hello"; // &str, with an implicit lifetime.
debug_str(&a);
{ // 'b
let string = String::from("world");
let b = string.as_str();
debug(&mut a, b);
}
}
参数: (&'b mut &'b str, &'b str)
这是因为在没有手动指定生命周期的情况下,编译器会根据代码逻辑自动推断生命周期。
a 的生命周期被“压缩”到了 'b,使得它能匹配 debug 签名同时不违反 invariance。
而下面的代码:
fn debug<T: std::fmt::Debug>(a: &mut T, b: T) {
println!("a={:?}, b={:?}", a, b);
}
fn debug_static<T: std::fmt::Debug>(a: &'static T) {
println!("{:?}", a);
}
fn main() {
let mut a = "hello"; // &str, with no explicit lifetime.
debug_static(&a); // 1.
{ // 'b
let string = String::from("world");
let b = string.as_str();
debug(&mut a, b);
}
println!("a={:?}", a); // 2.
// 1. sets lifetime of `a` to 'static
// 2. needs lifetime of `a` at least longer than 'b
}
上面的代码是不能编译通过的,因为编译器推断出的生命周期
'static 以及at least longer than 'b1. 和 2. 任意存在都会导致违反 &'a mut T 对 T 的 invariance.
最后,也是是唯一一个的 contra-variant。
fn(T) -> U over T is contravariant.
If t1 <: t2, then fn(t2) <: fn(t1).
直接看例子:
thread_local! {
pub static StaticVecs: RefCell<Vec<&'static str>> = RefCell::new(Vec::new());
}
/// saves the input given into a thread local `Vec<&'static str>`
fn store(input: &'static str) {
StaticVecs.with_borrow_mut(|v| v.push(input));
}
/// Calls the function with it's input (must have the same lifetime!)
fn demo<'a>(input: &'a str, f: fn(&'a str)) {
f(input);
}
fn main() {
demo("hello", store); // "hello" is 'static. Can call `store` fine
{
let smuggle = String::from("smuggle");
// `&smuggle` is not static. If we were to call `store` with `&smuggle`,
// we would have pushed an invalid lifetime into the `StaticVecs`.
// Therefore, `fn(&'static str)` cannot be a subtype of `fn(&'a str)`
demo(&smuggle, store);
}
// use after free...
StaticVecs.with_borrow(|v| println!("{v:?}"));
}
References
https://doc.rust-lang.org/nomicon/subtyping.html https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)