子类型和协变性

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);
    }
}

在一个保守的生命周期实现中,由于 helloworld 有不同的生命周期,我们可能会看到以下错误

error[E0308]: mismatched types
 --> src/main.rs:10:16
   |
10 |         debug(hello, world);
   |                      ^
   |                      |
   |                      expected `&'static str`, found struct `&'world str`

这将是相当不幸的。在这种情况下,我们想要接受任何其存活时间*至少与* 'world 一样长的类型。让我们尝试将子类型用于我们的生命周期。

子类型

子类型是一个类型可以用于代替另一个类型的概念。

我们定义 SubSuper 的一个子类型(在本章中我们将一直使用符号 Sub <: Super)。

这表明 Super 定义的**要求**完全由 Sub 满足。Sub 可能还有更多要求。

现在,为了将子类型用于生命周期,我们需要定义生命周期的要求

'a 定义了一段代码区域。

现在我们已经定义了生命周期的一组要求,我们可以定义它们之间的关系

'long <: 'short 当且仅当 'long 定义的代码区域**完全包含** 'short

'long 定义的区域可能比 'short 大,但这仍然符合我们的定义。

正如我们在本章的其余部分将看到的那样,子类型比这要复杂和微妙得多,但这个简单的规则是一个非常好的 99% 的直觉。除非你编写 unsafe 代码,否则编译器将自动为你处理所有边界情况。

但这是 Rustonomicon。我们正在编写 unsafe 代码,所以我们需要了解这些东西是如何真正工作的,以及我们如何可能把它弄糟。

回到上面的例子,我们可以说 '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。但随后 world 超出了作用域,而 hello 稍后在 println! 中被使用。

这是一个经典的 use-after-free 错误!

我们的第一反应可能是责怪 assign 实现,但这里真的没有任何问题。我们可能想将一个 T 赋值给一个 T,这并不奇怪。

问题在于我们不能假设 &mut &'static str&mut &'b str 是兼容的。这意味着 &mut &'static str **不能**是 &mut &'b str 的**子类型**,即使 'static'b 的子类型。

协变性是 Rust 用来定义通过其泛型参数来确定子类型之间关系的概念。

注意:为了方便起见,我们将定义一个泛型类型 F<T>,以便我们可以轻松地谈论 T。希望这在上下文中是清晰的。

类型 F 的**协变性**是指其输入的子类型关系如何影响其输出的子类型关系。Rust 中有三种协变性。给定两个类型 SubSuper,其中 SubSuper 的子类型

  • F 是**协变**的,如果 F<Sub>F<Super> 的子类型(子类型属性传递通过)
  • F 是**逆变**的,如果 F<Super>F<Sub> 的子类型(子类型属性“反转”)
  • 否则,F 是**不变**的(不存在子类型关系)

如果我们回想上面的例子,将 &'a T 视为 &'b T 的子类型是可以的,如果 'a <: 'b,因此我们可以说 &'a T'a 是**协变**的。

此外,我们看到将 &mut &'a U 视为 &mut &'b U 的子类型是不可行的,因此我们可以说 &mut TT 是**不变**的。

这里有一些其他泛型类型及其协变性的表格

'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 使我们更容易关注引用的值传递方面,我们在上面部分忽略了这一点。

与许多允许随时自由创建别名(aliasing)的语言不同,Rust 有一个非常严格的规则:如果你被允许修改或移动一个值,你保证是唯一有权访问它的人。

考虑以下代码

let hello: Box<&'static str> = Box::new("hello");

let mut world: Box<&'b str>;
world = hello;

我们忘记了 hello 存活了 'static 并没有问题,因为一旦我们将 hello 移动到一个只知道它存活了 'b 的变量中,**我们就销毁了宇宙中唯一记住它活得更久的东西**!

只剩下最后一件事要解释:函数指针。

要了解为什么 fn(T) -> UU 应该是协变的,考虑以下签名

fn get_str() -> &'a str;

这个函数声称产生一个绑定到某个生命周期 'astr。因此,提供一个具有以下签名函数是完全有效的

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 中使用,那么 MyTypeA 的协变性与字段 aA 的协变性完全相同。

但是如果 A 在多个字段中使用怎么办?

  • 如果 A 的所有用途都是协变的,那么 MyTypeA 是协变的
  • 如果 A 的所有用途都是逆变的,那么 MyTypeA 是逆变的
  • 否则,MyTypeA 是不变的
#![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
}
}