子类型和变型

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` 是*不变的*

下表列出了一些其他泛型类型及其变型

'aTU
&'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 TT 上是不变的,编译器得出结论,它不能对第一个参数应用任何子类型化,因此 T 必须完全是 &'static str

这与 &T 的情况相反

#![allow(unused)]
fn main() {
fn debug<T: std::fmt::Debug>(a: T, b: T) {
    println!("a = {a:?} b = {b:?}");
}
}

其中类似地,ab 必须具有相同的类型 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 上的变异性就是 aA 上的变异性。

但是,如果 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
}
}