子类型和变型
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() { let hello: &'static str = "hello"; { let world = String::from("world"); let world = &world; // 'world has a shorter lifetime than 'static debug(hello, world); } }
在生命周期的保守实现中,由于 `hello` 和 `world` 具有不同的生命周期,我们可能会看到以下错误
error[E0308]: mismatched types
--> src/main.rs:10:16
|
10 | debug(hello, world);
| ^
| |
| expected `&'static str`, found struct `&'world str`
这将是非常不幸的。在这种情况下,我们想要的是接受任何生命周期*至少与 `'world` 一样长*的类型。让我们尝试对我们的生命周期使用子类型。
子类型
子类型是指一种类型可以用来代替另一种类型的概念。
让我们定义 `Sub` 是 `Super` 的子类型(我们将在本章中使用符号 `Sub <: Super`)。
这对我们来说意味着 `Super` 定义的*需求*集完全由 `Sub` 满足。`Sub` 可能还有更多需求。
现在,为了对生命周期使用子类型,我们需要定义生命周期的需求
`'a` 定义了一个代码区域。
现在我们已经为生命周期定义了一组需求,我们可以定义它们之间的关系
当且仅当 `'long` 定义的代码区域**完全包含** `'short` 时,`'long <: 'short`。
`'long` 定义的区域可能大于 `'short`,但这仍然符合我们的定义。
正如我们将在本章的其余部分中看到的那样,子类型比这要复杂和微妙得多,但这个简单的规则在 99% 的情况下都是一个很好的直觉。除非你编写不安全的代码,编译器会自动为你处理所有特殊情况。
但这是 Rust 秘典。我们正在编写不安全的代码,所以我们需要了解这些东西是如何工作的,以及我们如何搞砸它。
回到上面的例子,我们可以说 `'static <: 'world`。现在,让我们也接受这样一个观点:生命周期的子类型可以通过引用传递(更多信息请参见变型),*例如* `&'static str` 是 `&'world str` 的子类型,那么我们可以将 `&'static str`“降级”为 `&'world str`。这样,上面的例子就可以编译了
fn debug<'a>(a: &'a str, b: &'a str) { println!("a = {a:?} b = {b:?}"); } fn main() { let hello: &'static str = "hello"; { let world = String::from("world"); let world = &world; // 'world has a shorter lifetime than 'static debug(hello, world); // hello silently downgrades from `&'static str` into `&'world str` } }
变型
上面,我们忽略了 `'static <: 'b` 意味着 `&'static T <: &'b T` 的事实。这使用了一个称为*变型*的属性。不过,它并不总是像这个例子那样简单。为了理解这一点,让我们尝试扩展一下这个例子
fn assign<T>(input: &mut T, val: T) { *input = val; } fn main() { let mut hello: &'static str = "hello"; { let world = String::from("world"); assign(&mut hello, &world); } println!("{hello}"); // use after free 😿 }
在 `assign` 中,我们将 `hello` 引用设置为指向 `world`。但是在 `println!` 中稍后使用 `hello` 之前,`world` 就超出了作用域。
这是一个典型的释放后使用错误!
我们的第一反应可能是责怪 `assign` 实现,但这里真的没有错。我们可能想将一个 `T` 赋值给另一个 `T`,这并不奇怪。
问题是我们不能假设 `&mut &'static str` 和 `&mut &'b str` 是兼容的。这意味着即使 `'static` 是 `'b` 的子类型,`&mut &'static str` 也**不能**是 `&mut &'b str` 的*子类型*。
变型是 Rust 借用来定义泛型参数的子类型关系的概念。
注意:为了方便起见,我们将定义一个泛型类型 `F<T>`,以便我们可以轻松地讨论 `T`。希望这在上下文中是清楚的。
类型 `F` 的*变型*是指其输入的子类型如何影响其输出的子类型。Rust 中有三种变型。给定两种类型 `Sub` 和 `Super`,其中 `Sub` 是 `Super` 的子类型
- 如果 `F<Sub>` 是 `F<Super>` 的子类型(子类型属性被传递),则 `F` 是**协变的**
- 如果 `F<Super>` 是 `F<Sub>` 的子类型(子类型属性被“反转”),则 `F` 是**逆变的**
- 否则,`F` 是**不变的**(不存在子类型关系)
如果我们还记得上面的例子,如果 `'a <: 'b`,那么我们可以将 `&'a T` 视为 `&'b T` 的子类型,因此我们可以说 `&'a T` 对 `'a` 是*协变的*。
此外,我们还看到将 `&mut &'a U` 视为 `&mut &'b U` 的子类型是不行的,因此我们可以说 `&mut T` 对 `T` 是*不变的*
下表列出了一些其他泛型类型及其变型
'a | T | U | |
---|---|---|---|
&'a T | 协变 | 协变 | |
&'a mut T | 协变 | 不变 | |
Box<T> | 协变 | ||
Vec<T> | 协变 | ||
UnsafeCell<T> | 不变 | ||
Cell<T> | 不变 | ||
fn(T) -> U | **逆变** | 协变 | |
*const T | 协变 | ||
*mut T | 不变 |
其中一些可以简单地解释为与其他类型相关
- `Vec<T>` 和所有其他拥有所有权的指针和集合都遵循与 `Box<T>` 相同的逻辑
- `Cell<T>` 和所有其他内部可变类型都遵循与 `UnsafeCell<T>` 相同的逻辑
- `UnsafeCell<T>` 具有内部可变性,这使其具有与 `&mut T` 相同的变型属性
- `*const T` 遵循 `&T` 的逻辑
- `*mut T` 遵循 `&mut T`(或 `UnsafeCell<T>`)的逻辑
有关更多类型,请参阅参考文档中的“变型”部分。
注意:语言中*唯一*的逆变来源是函数的参数,这就是为什么它在实践中很少出现的原因。调用逆变涉及使用函数指针的高阶编程,这些函数指针接受具有特定生命周期的引用(而不是通常的“任何生命周期”,这会涉及到高阶生命周期,它独立于子类型工作)。
现在我们对变异性有了一些更正式的理解,让我们更详细地看一下更多示例。
fn assign<T>(input: &mut T, val: T) { *input = val; } fn main() { let mut hello: &'static str = "hello"; { let world = String::from("world"); assign(&mut hello, &world); } println!("{hello}"); }
当我们运行它时会得到什么?
error[E0597]: `world` does not live long enough
--> src/main.rs:9:28
|
6 | let mut hello: &'static str = "hello";
| ------------ type annotation requires that `world` is borrowed for `'static`
...
9 | assign(&mut hello, &world);
| ^^^^^^ borrowed value does not live long enough
10 | }
| - `world` dropped here while still borrowed
很好,它无法编译!让我们详细分析一下这里发生的事情。
首先让我们看一下 assign
函数
#![allow(unused)] fn main() { fn assign<T>(input: &mut T, val: T) { *input = val; } }
它所做的只是获取一个可变引用和一个值,并用该值覆盖引用。此函数的重要之处在于它创建了一个类型相等约束。它的签名清楚地表明,引用和值必须是*完全相同的*类型。
同时,在调用方中,我们传入 &mut &'static str
和 &'world str
。
因为 &mut T
在 T
上是不变的,编译器得出结论,它不能对第一个参数应用任何子类型化,因此 T
必须完全是 &'static str
。
这与 &T
的情况相反
#![allow(unused)] fn main() { fn debug<T: std::fmt::Debug>(a: T, b: T) { println!("a = {a:?} b = {b:?}"); } }
其中类似地,a
和 b
必须具有相同的类型 T
。但由于 &'a T
*是* 在 'a
上协变的,因此我们允许执行子类型化。所以编译器决定 &'static str
可以变为 &'b str
,当且仅当 &'static str
是 &'b str
的子类型,这在 'static <: 'b
时成立。这是真的,所以编译器很乐意继续编译此代码。
事实证明,Box(以及 Vec、HashMap 等)可以协变的原因与生命周期可以协变的原因非常相似:一旦你尝试将它们塞入可变引用之类的东西中,它们就会继承不变性,并且你无法做任何坏事。
然而,Box 使得我们更容易关注我们之前部分忽略的引用的按值方面。
与许多允许值始终自由别名的语言不同,Rust 有一个非常严格的规则:如果你被允许改变或移动一个值,你就能保证你是唯一一个可以访问它的人。
考虑以下代码
let hello: Box<&'static str> = Box::new("hello");
let mut world: Box<&'b str>;
world = hello;
我们已经忘记 hello
的生命周期是 'static
,但这完全没有问题,因为一旦我们将 hello
移动到一个只知道它在 'b
内有效的变量中,**我们就销毁了宇宙中唯一记得它活得更久的东西**!
只剩下最后一件事需要解释:函数指针。
要了解为什么 fn(T) -> U
应该在 U
上协变,请考虑以下签名
fn get_str() -> &'a str;
此函数声称生成一个受某个生命周期 'a
约束的 str
。因此,提供具有以下签名的函数是完全有效的
fn get_static() -> &'static str;
所以当函数被调用时,它所期望的只是一个至少在 'a
的生命周期内有效的 &str
,该值实际存活的时间更长并不重要。
但是,相同的逻辑并不适用于*参数*。考虑尝试用
fn store_ref(&'a str);
来满足
fn store_static(&'static str);
第一个函数可以接受任何字符串引用,只要它至少在 'a
的生命周期内有效,但第二个函数不能接受生命周期小于 'static
的字符串引用,这会导致冲突。协变在这里不起作用。但如果我们反过来,它实际上*确实*有效!如果我们需要一个可以处理 &'static str
的函数,那么一个可以处理*任何*引用生命周期的函数肯定可以正常工作。
让我们在实践中看看这一点
use std::cell::RefCell; 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:?}")); }
这就是为什么函数类型与语言中的任何其他类型不同,它们在其参数上是*逆*变的。
现在,对于标准库提供的类型来说,这一切都很好,但是如何确定*你*定义的类型的变异性呢?非正式地说,结构体继承其字段的变异性。如果结构体 MyType
有一个泛型参数 A
,该参数用在字段 a
中,那么 MyType 在 A
上的变异性就是 a
在 A
上的变异性。
但是,如果 A
在多个字段中使用
- 如果
A
的所有使用都是协变的,那么 MyType 在A
上是协变的 - 如果
A
的所有使用都是逆变的,那么 MyType 在A
上是逆变的 - 否则,MyType 在
A
上是不变的
#![allow(unused)] fn main() { use std::cell::Cell; struct MyType<'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H, In, Out, Mixed> { a: &'a A, // covariant over 'a and A b: &'b mut B, // covariant over 'b and invariant over B c: *const C, // covariant over C d: *mut D, // invariant over D e: E, // covariant over E f: Vec<F>, // covariant over F g: Cell<G>, // invariant over G h1: H, // would also be covariant over H except... h2: Cell<H>, // invariant over H, because invariance wins all conflicts i: fn(In) -> Out, // contravariant over In, covariant over Out k1: fn(Mixed) -> usize, // would be contravariant over Mixed except.. k2: Mixed, // invariant over Mixed, because invariance wins all conflicts } }