高级 The Rust Project Developers 著,Pure White 中译 2026-09-13 14:59:14 · 0 阅读

第4章 所有权

所有权是 Rust 的突破性功能。它使 Rust 能够做到完全的内存安全和高效,同时避免了垃圾回收。在详细介绍所有权系统之前,我们将考虑这一设计的动机。

我们将假设你同意垃圾收集(GC)并不总是一个最佳解决方案,而且在某些情况下手动管理内存更为适合。如果你不接受这一点,我是否可以让你对另一种语言感兴趣?

不管你对 GC 的看法如何,它显然是一个使代码更安全的好办法,你永远不必担心你的对象会在引用失效前就被释放。这是一个 C 和 C++ 程序需要处理的普遍存在的问题。比如下面这个简单的错误,我们所有使用过非 GC 语言的人都曾经犯过:

#![allow(unused)]
fn main() {
fn as_str(data: &u32) -> &str {
    // 计算出字符串
    let s = format!("{}", data);

    // 不好!我们返回了一个仅仅在函数中存在的变量的引用!
    // 悬挂指针!释放后使用!哎呀!
    //(这在 Rust 中无法编译通过)
    &s
}
}

这正是 Rust 的所有权系统所要解决的问题。Rust 知道&s所在的范围,因此可以防止它逃逸。然而,这是一个简单的案例,即使是 C 语言的编译器也能合理地抓住。随着代码越来越大,指针被送入各种函数,事情变得越来越复杂。最终,C 语言编译器会倒下,无法进行足够的转义分析来证明你的代码不健全。因此,它将被迫接受你的程序,假设它是正确的。

这种情况永远不会发生在 Rust 上,Rust 要求程序员来向编译器证明一切是正确的。

当然,Rust 围绕所有权的故事要比仅仅验证引用不脱离其所有者的范围要复杂得多,这是因为确保指针始终有效要比这复杂得多。例如,在这段代码中:

#![allow(unused)]
fn main() {
let mut data = vec![1, 2, 3];
// 获取内部元素的引用
let x = &data[0];

// 不好! `push` 操作导致 `data` 的存储空间重新分配了
// 悬挂指针!释放后使用!哎呀!
//(这在 Rust 中无法编译通过)
data.push(4);

println!("{}", x);
}

简单的作用域分析不足以防止这个 bug,因为data事实上确实存活得足够久,满足我们的需求。然而,当我们对它有一个引用时,它被改变了。这就是为什么 Rust 要求任何引用都要冻结引用者和其所有者。

引用

有两种类型的引用:

  • 共享的引用:&
  • 可变引用:&mut

它们遵守以下规则:

  • 一个引用的生命周期不能超过它所引用对象的生命周期
  • 一个可变的引用不能有别名

这就是引用所遵循的整个模型。

当然,我们也许应该定义别名的含义:

error[E0425]: cannot find value `aliased` in this scope
 --> <rust.rs>:2:20
  |
2 |     println!("{}", aliased);
  |                    ^^^^^^^ not found in this scope

error: aborting due to previous error

不幸的是,Rust 还没有真正定义其别名模型。🙀

在我们等待 Rust 的设计者明确他们语言的语义时,让我们用下一节来讨论下在一般场景下别名到底是什么,以及它为什么重要。

别名

首先,让我们先说一些重要的注意事项:

  • 为了便于讨论,我们将使用最广泛的别名定义。Rust 的定义可能会有更多限制,以考虑到可变性和有效性。

  • 我们将假设一个单线程的、无中断的执行,我们还将忽略像内存映射硬件这样的东西。Rust 假定这些事情不会发生,除非你明确告诉它会发生。更多细节,请参阅并发性章节

所以,我们现行的定义是:如果变量和指针指向内存的重叠区域,那么它们就是别名

为什么别名很重要

为什么我们需要关注别名呢?

让我们看下这个例子:

#![allow(unused)]
fn main() {
fn compute(input: &u32, output: &mut u32) {
    if *input > 10 {
        *output = 1;
    }
    if *input > 5 {
        *output *= 2;
    }
    // 记住一点: 如果 `input>10`,那么 `output` 永远为 `2`
}
}

我们希望能够把它优化成下面这样的函数:

#![allow(unused)]
fn main() {
fn compute(input: &u32, output: &mut u32) {
    let cached_input = *input; // 将 `*input` 中的内容保存在寄存器中
    if cached_input > 10 {
        // 如果输入比 10 大, 优化之前的代码会将 output 设置为 1,然后乘以 2,
        // 结果一定返回 `2` (因为 `>10` 包括了 `>5` 的情况),
        // 因此这里可以进行优化,
        // 不对 output 重复赋值,直接将其设置为 2
        *output = 2;
    } else if cached_input > 5 {
        *output *= 2;
    }
}
}

在 Rust 中,这种优化应该是可行的。但对于几乎任何其他语言来说,它都不是这样的(除非是全局分析)。这是因为这个优化依赖于知道别名不会发生,而大多数语言在这方面是相当宽松的。具体来说,我们需要担心那些使“输入”和“输出”重叠的函数参数,如compute(&x, &mut x)

如果按照这样的输入,我们实际上执行的代码如下:

                    //  input ==  output == 0xabad1dea
                    // *input == *output == 20
if *input > 10 {    // true  (*input == 20)
    *output = 1;    // 同时覆盖了 input 引用的内容,因为它们实际上引用了同一块内存
}
if *input > 5 {     // false (*input == 1)
    *output *= 2;
}
                    // *input == *output == 1

我们的优化函数对于这个输入会产生*output == 2,所以在这种情况下,我们的优化就无法实现了。

在 Rust 中,我们知道这个输入是不可能的,因为&mut不允许被别名。所以我们可以安全地认为这种情况不会发生,并执行这个优化。在大多数其他语言中,这种输入是完全可能的,因此必须加以考虑。

这就是为什么别名分析很重要的原因:它可以让编译器进行有用的优化! 比如:

  • 通过证明没有指针访问该值的内存来保持寄存器中的值
  • 通过证明某些内存在我们上次读取后没有被写入,来消除读取
  • 通过证明某些内存在下一次写入之前从未被读过,来消除写入
  • 通过证明读和写之间不相互依赖来对指令进行移动或重排序

这些优化也用于证明更大的优化的合理性,如循环矢量化、常数传播和死代码消除。

在前面的例子中,我们利用&mut u32不能被别名的事实来证明对*output的写入不可能影响*input。这让我们把*input缓存在一个寄存器中,省去了读的过程。

通过缓存这个读,我们知道在> 10分支中的写不能影响我们是否采取> 5分支,使我们在*input > 10时也能消除一个读-修改-写(加倍*output)。

关于别名分析,需要记住的关键一点是,写是优化的主要危险。也就是说,阻止我们将读移到程序的任何其他部分的唯一原因是我们有可能将其与写到同一位置重新排序。

例如,在下面这个修改后的函数中,我们不需要担心别名问题,因为我们已经将唯一一个写到*output的地方移到了函数的最后。这使得我们可以自由地重新排序在它之前发生的对*input的读取:

#![allow(unused)]
fn main() {
fn compute(input: &u32, output: &mut u32) {
    let mut temp = *output;
    if *input > 10 {
        temp = 1;
    }
    if *input > 5 {
        temp *= 2;
    }
    *output = temp;
}
}

我们仍然依靠别名分析来假设temp没有别名input,但是证明要简单得多:局部变量的值不能被在它被声明之前就存在的东西所别名。这是每一种语言都可以自由做出的假设,因此这个版本的函数可以在任何语言中按照我们想要的方式进行优化。

这就是为什么 Rust 将使用的“别名”的定义可能涉及到一些有效性和可变性的概念:如果没有任何实际写入内存的情况发生,我们实际上并不关心别名是否发生。

当然,Rust 的完整别名模型还必须考虑到函数调用(可能会改变我们看不到的东西)、原始指针(它本身没有别名要求)和 UnsafeCell(它让&的引用被改变)等东西。

生命周期

Rust 通过生命周期来执行相关的规则。生命周期是指一个引用必须有效的代码区域,这些区域可能相当复杂,因为它们对应着程序中的执行路径。这些执行路径中甚至可能存在空洞(译者注: 空洞是指一个引用的生命周期可能不是一个连续的代码区域,中间可能有跳跃),因为我们可能会先使一个引用失效,之后再重新初始化并使用它。包含引用(或假装包含)的类型也可以用生命周期来标记,这样 Rust 就可以防止它们也被失效。

在我们大多数例子中,生命周期将与作用域重合,这是因为我们的例子很简单。下面将介绍它们不重合的更复杂的情况。

在一个函数体中,Rust 通常不需要你明确地命名所涉及的生命周期。这是因为一般来说,在本地环境中谈论生命周期是没有必要的;Rust 拥有所有的信息,并且可以尽可能地以最佳方式解决所有问题。Rust 还会引入许多匿名作用域和临时变量, 你不必显式写出它们, 代码也可以跑通。

然而,一旦你跨越了函数的边界,你就需要开始考虑生命周期了。生命周期是用撇号表示的:'a'static。为了尝试使用生命周期,我们将假装我们被允许用生命周期来标记作用域,并尝试手动解一下本章开头例子的语法糖。

我们之前的例子使用了一种激进的语法糖——甚至是高果糖玉米糖浆——因为明确地写出所有东西是非常繁琐的。所有的 Rust 代码都依赖于积极的推理和对“显而易见”的东西的删除。

一个特别有趣的语法糖是,每个let语句都隐含地引入了一个作用域。在大多数情况下,这其实并不重要。然而,这对那些相互引用的变量来说确实很重要。作为一个简单的例子,让我们对这段简单的 Rust 代码进行完全解糖:

#![allow(unused)]
fn main() {
let x = 0;
let y = &x;
let z = &y;
}

借用检查器总是试图最小化生命周期的范围,所以它很可能会脱糖为以下内容:

// NOTE: `'a: {` 和 `&'b x` 不是有效的语法,这里只是为了说明 lifetime 的概念
'a: {
    let x: i32 = 0;
    'b: {
        // y 的生命周期为 'b,因为这已经足够好
        let y: &'b i32 = &'b x;
        'c: {
            // 'c 同上所示
            let z: &'c &'b i32 = &'c y; // "a reference to a reference to an i32" (with lifetimes annotated)
        }
    }
}

哇,这真是……太可怕了!让我们花点时间感谢 Rust 让这一切变得简单。

实际上,传递一个引用到外部作用域将导致 Rust 推断出一个更大的生命周期。

#![allow(unused)]
fn main() {
let x = 0;
let z;
let y = &x;
z = y;
}
'a: {
    let x: i32 = 0;
    'b: {
        let z: &'b i32;
        'c: {
            // y 的生命周期一定为 'b,因为对 x 的引用被传递到了 'b 这个作用域
            let y: &'b i32 = &'b x;
            z = y;
        }
    }
}

例子:超出所有者生命周期的引用

让我们看看之前的那些例子:

#![allow(unused)]
fn main() {
fn as_str(data: &u32) -> &str {
    let s = format!("{}", data);
    &s
}
}

解语法糖后:

fn as_str<'a>(data: &'a u32) -> &'a str {
    'b: {
        let s = format!("{}", data);
        return &'a s;
    }
}

as_str的这个签名接收了一个具有某个生命周期的 u32 的引用,并返回一个可以存活同样长的 str 的引用。我们已经大致能猜到为什么这个函数签名可能是个麻烦了,这意味着我们要找的那个 str 要在 u32 的引用所处的作用域上,或者甚至在更大的作用域上。这要求有点高。

然后我们继续计算字符串s,并返回它的一个引用。由于我们的函数的契约规定这个引用必须超过'a,这就是我们推断出的引用的生命周期。不幸的是,s被定义在作用域'b中,所以唯一合理的方法是'b包含'a,这显然是错误的,因为'a必须包含函数调用本身。因此,我们创建了一个引用,它的生命周期超过了它的引用者,这正是我们所说的引用不能做的第一件事。编译器理所当然地直接报错。

为了更清楚地说明这一点,我们可以扩展这个例子:

fn as_str<'a>(data: &'a u32) -> &'a str {
    'b: {
        let s = format!("{}", data);
        return &'a s
    }
}

fn main() {
    'c: {
        let x: u32 = 0;
        'd: {
            // 这里引入了一个匿名作用域,因为借用不需要在整个 x 的作用域内生效,
            // 这个函数必须返回一个在函数调用之前就存在的某个字符串的引用,事实显然不是这样
            println!("{}", as_str::<'d>(&'d x));
        }
    }
}

当然,这个函数的正确写法是这样的:

#![allow(unused)]
fn main() {
fn to_string(data: &u32) -> String {
    format!("{}", data)
}
}

我们必须在函数里面产生一个拥有所有权的值才能返回! 我们唯一可以返回一个&'a str的方法是,它在&'a u32的一个字段中,但显然不是这样的。

(实际上我们也可以直接返回一个字符串字面量,作为一个全局的字面量可以被认为是在堆栈的底部;尽管这对我们的实现有一点限制)。

示例:别名一个可变引用

来看另一个例子:

#![allow(unused)]
fn main() {
let mut data = vec![1, 2, 3];
let x = &data[0];
data.push(4);
println!("{}", x);
}
'a: {
    let mut data: Vec<i32> = vec![1, 2, 3];
    'b: {
        // 'b 这个生命周期范围如我们所愿地小(刚好够 println!)
        let x: &'b i32 = Index::index::<'b>(&'b data, 0);
        'c: {
            // 这里有一个临时作用域,我们不需要更长时间的 &mut 借用
            Vec::push(&'c mut data, 4);
        }
        println!("{}", x);
    }
}

这里的问题更微妙、更有趣。我们希望 Rust 拒绝这个程序,理由如下:我们有一个存活的共享引用xdata的一个子集,当我们试图把data的可变引用传给push时。这将创建一个可变引用的别名,而这将违反引用的第二条规则。

然而,这根本不是 Rust 认为这个程序有问题的原因。Rust 不理解x是对data的一个子集的引用。它根本就不理解Vec。它看到的是,x必须在'b范围内保持存活才能被打印;接下来,Index::index的签名要求我们对data的引用必须在'b范围内存活。当我们试图调用push时,它看到我们试图构造一个&'c mut data。Rust 知道'c包含在'b中,并拒绝了我们的程序,因为&'b data必然还存活着!

在这里我们看到,和我们真正想要保证的引用规则语义相比,生命周期系统要粗略得多。在大多数情况下,这完全没问题,因为它使我们不用花整天的时间向编译器解释我们的程序。然而,这确实意味着有部分程序对于 Rust 的真正的语义来说是完全正确的,但却被拒绝了,因为 lifetime 太傻了。

生命周期所覆盖的区域

一个引用(有时称为borrow)从它被创建到最后一次使用都是存活的。被 borrow 的值的生命周期只需要超过引用的生命周期就行。这看起来很简单,但有一些微妙之处。

下面的代码可以成功编译,因为在打印完x之后,它就不再需要了,所以它是悬空的还是别名的都无所谓(尽管变量x技术上一直存活到作用域的最后):

#![allow(unused)]
fn main() {
let mut data = vec![1, 2, 3];
let x = &data[0];
println!("{}", x);
// 这是可行的,因为不再使用 x,编译器也就缩短了 x 的生命周期
data.push(4);
}

然而,如果该值有一个析构器,析构器就会在作用域的末端运行。而运行析构器被认为是一种使用——显然是最后一次使用。所以,这将会编译报错:

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct X<'a>(&'a i32);

impl Drop for X<'_> {
    fn drop(&mut self) {}
}

let mut data = vec![1, 2, 3];
let x = X(&data[0]);
println!("{:?}", x);
data.push(4);
// 编译器会在这里自动插入 drop 函数,也就意味着我们会访问 x 中引用的变量,因此编译失败
}

让编译器相信x不再有效的一个方法是在data.push(4)之前使用drop(x)

此外,可能会有多种最后一次的引用使用,例如在一个条件的每个分支中:

#![allow(unused)]
fn main() {
fn some_condition() -> bool { true }
let mut data = vec![1, 2, 3];
let x = &data[0];

if some_condition() {
    println!("{}", x); // 这是该分支中最后一次使用 x 这个引用
    data.push(4);      // 因此在这里 push 操作是可行的
} else {
    // 这里不存在对 x 的使用,对于这个分支来说,
    // x 创建即销毁
    data.push(5);
}
}

生命周期中可以有暂停,或者你可以把它看成是两个不同的借用,只是被绑在同一个局部变量上。这种情况经常发生在循环周围(在循环结束时写入一个变量的新值,并在下一次迭代的顶部最后一次使用它)。

#![allow(unused)]
fn main() {
let mut data = vec![1, 2, 3];
// x 是可变的(通过 mut 声明),因此我们可以修改 x 指向的内容
let mut x = &data[0];

println!("{}", x); // 最后一次使用这个引用
data.push(4);
x = &data[3]; // x 在这里借用了新的变量
println!("{}", x);
}

Rust 曾经一直保持着借用的生命,直到作用域结束,所以这些例子在旧的编译器中可能无法编译。此外,还有一些边界条件,Rust 不能正确地缩短借用的有效部分,即使看起来应该这样做,也不能编译。这些问题将随着时间的推移得到解决。

生命周期的局限

让我们来看以下代码:

#[derive(Debug)]
struct Foo;

impl Foo {
    fn mutate_and_share(&mut self) -> &Self { &*self }
    fn share(&self) {}
}

fn main() {
    let mut foo = Foo;
    let loan = foo.mutate_and_share();
    foo.share();
    println!("{:?}", loan);
}

人们可能期望它能被编译成功,我们调用mutate_and_share,它可以暂时可变借用foo,但随后只返回一个共享引用。因此我们期望foo.share()能够成功,因为foo不应该被可变借用。

然而,当我们试图编译它时:

error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
  --> src/main.rs:12:5
   |
11 |     let loan = foo.mutate_and_share();
   |                --- mutable borrow occurs here
12 |     foo.share();
   |     ^^^ immutable borrow occurs here
13 |     println!("{:?}", loan);

这是为啥?好吧,我们得到的推理和上一节例 2完全一样。我们对程序进行解语法糖后,可以得到如下结果:

struct Foo;

impl Foo {
    fn mutate_and_share<'a>(&'a mut self) -> &'a Self { &'a *self }
    fn share<'a>(&'a self) {}
}

fn main() {
    'b: {
        let mut foo: Foo = Foo;
        'c: {
            let loan: &'c Foo = Foo::mutate_and_share::<'c>(&'c mut foo);
            'd: {
                Foo::share::<'d>(&'d foo);
            }
            println!("{:?}", loan);
        }
    }
}

由于loan的生命周期和mutate_and_share的签名,生命周期系统被迫将&mut foo扩展为'c的生命周期。然后当我们试图调用share时,它看到我们试图别名&'c mut foo,然后就炸了!

根据我们真正关心的引用语义,这个程序显然是正确的,但是生命周期系统太蠢了(原话是粗糙),无法处理这个问题。

不正确地缩减借用

下面的代码无法编译成功,因为 Rust 发现map变量被借用了两次,并且不能推断出在第二次借用之前,第一次借用已经不需要了,所以保守地退回到使用整个作用域作为第一次借用的生命周期。不过不用担心,这个问题最终会得到解决:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use std::hash::Hash;
fn get_default<'m, K, V>(map: &'m mut HashMap<K, V>, key: K) -> &'m mut V
where
    K: Clone + Eq + Hash,
    V: Default,
{
    match map.get_mut(&key) {
        Some(value) => value,
        None => {
            map.insert(key.clone(), V::default());
            map.get_mut(&key).unwrap()
        }
    }
}
}

由于所施加的生命周期限制,&mut map的生命周期与其他可变的借用重叠,导致编译错误:

error[E0499]: cannot borrow `*map` as mutable more than once at a time
  --> src/main.rs:12:13
   |
4  |   fn get_default<'m, K, V>(map: &'m mut HashMap<K, V>, key: K) -> &'m mut V
   |                  -- lifetime `'m` defined here
...
9  |       match map.get_mut(&key) {
   |       -     --- first mutable borrow occurs here
   |  _____|
   | |
10 | |         Some(value) => value,
11 | |         None => {
12 | |             map.insert(key.clone(), V::default());
   | |             ^^^ second mutable borrow occurs here
13 | |             map.get_mut(&key).unwrap()
14 | |         }
15 | |     }
   | |_____- returning this value requires that `*map` is borrowed for `'m`

生命周期省略

为了使常见的模式更符合人体工程学,Rust 允许在函数签名中省略生命周期。

生命周期位置是指在一个类型中可以写入生命周期的任何地方。

&'a T
&'a mut T
T<'a>

生命周期位置可以作为“输入”或“输出”出现:

  • 对于fn定义、fn类型以及 Trait FnFnMutFnOnce,输入是指形式参数的类型,而输出是指结果类型。所以fn foo(s: &str) -> (&str, &str)在输入位置有一个生命周期,在输出位置有两个生命周期。请注意,fn方法定义的输入位置不包括方法的impl头中出现的生命周期(对于默认方法,也不包括 trait 头中出现的生命周期)
  • 对于impl头,所有类型都是输入。所以impl Trait<&T> for Struct<&T>在输入位置上省略了两个生命周期,而impl Struct<&T>则省略了一个

省略规则如下:

  • 在输入位置的每一个被省略的生命周期都成为一个独立的生命周期参数
  • 如果正好有一个输入生命周期的位置(无论是否被省略),该生命周期将被分配给所有被省略的输出生命周期
  • 如果有多个输入生命周期位置,但其中一个是&self&mut self,那么self的生命周期将被分配给所有被省略的输出生命周期
  • 否则,省略一个输出生命周期是一个错误

示例:

fn print(s: &str);                                      // elided
fn print<'a>(s: &'a str);                               // expanded

fn debug(lvl: usize, s: &str);                          // elided
fn debug<'a>(lvl: usize, s: &'a str);                   // expanded

fn substr(s: &str, until: usize) -> &str;               // elided
fn substr<'a>(s: &'a str, until: usize) -> &'a str;     // expanded

fn get_str() -> &str;                                   // ILLEGAL

fn frob(s: &str, t: &str) -> &str;                      // ILLEGAL

fn get_mut(&mut self) -> &mut T;                        // elided
fn get_mut<'a>(&'a mut self) -> &'a mut T;              // expanded

fn args<T: ToCStr>(&mut self, args: &[T]) -> &mut Command                  // elided
fn args<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command // expanded

fn new(buf: &mut [u8]) -> BufWriter;                    // elided
fn new(buf: &mut [u8]) -> BufWriter<'_>;                // elided (with `rust_2018_idioms`)
fn new<'a>(buf: &'a mut [u8]) -> BufWriter<'a>          // expanded

不受约束的生命周期

不安全的代码经常会凭空产生引用或生命周期,这种生命周期是以无约束的形式出现在世界中的。最常见的原因是对原始指针的解引用,这产生了一个具有无约束生命周期的引用。这样的生命周期会随着上下文的要求而变大。这实际上比简单地标记为'static更强大,因为例如&'static &'a T将无法通过类型检查,但无约束的生命周期将根据需要完美地塑造为&'a &'a T。然而,对于大多数意图和目的来说,这样的无约束生命周期可以被看作是'static

几乎没有引用是'static的,所以这可能是错误的。transmutetransmute_copy是另外两个主要的违规者。我们应该尽可能快地约束一个无约束的生命周期,特别是当跨越函数边界的时候。

给定一个函数,任何不来自输入的输出生命周期都是无约束的,比如说:

fn get_str<'a>(s: *const String) -> &'a str {
    unsafe { &*s }
}
fn main() {
    let soon_dropped = String::from("hello");
    let dangling = get_str(&soon_dropped);
    drop(soon_dropped);
    println!("Invalid str: {}", dangling); // Invalid str: gӚ_`
}

避免无约束生命周期的最简单方法是在函数边界使用生命周期省略。如果一个输出的生命周期被省略了,那么它必须被一个输入的生命周期所约束。当然,它也可能被错误的生命周期所约束,但这通常只会引起编译错误,而不是让内存安全被简单地违反。

在一个函数中,对生命周期的约束更容易出错。约束生命周期的最安全和最简单的方法是从一个具有约束的生命周期的函数中返回它。然而,如果这样做是不可接受的,可以将引用放在一个有特定生命周期的位置。不幸的是,我们不可能命名一个函数中涉及的所有生命周期。

Higher-Rank Trait Bounds (HRTBs)

Rust 的Fn trait 有一些黑魔法,例如,我们可以写出下面的代码:

struct Closure<F> {
    data: (u8, u16),
    func: F,
}

impl<F> Closure<F>
    where F: Fn(&(u8, u16)) -> &u8,
{
    fn call(&self) -> &u8 {
        (self.func)(&self.data)
    }
}

fn do_it(data: &(u8, u16)) -> &u8 { &data.0 }

fn main() {
    let clo = Closure { data: (0, 1), func: do_it };
    println!("{}", clo.call());
}

如果我们试图天真地用与生命周期部分相同的方式来对这段代码进行解语法糖,我们会遇到一些麻烦:

// NOTE: `&'b data.0` and `'x: {` is not valid syntax!
struct Closure<F> {
    data: (u8, u16),
    func: F,
}

impl<F> Closure<F>
    // where F: Fn(&'??? (u8, u16)) -> &'??? u8,
{
    fn call<'a>(&'a self) -> &'a u8 {
        (self.func)(&self.data)
    }
}

fn do_it<'b>(data: &'b (u8, u16)) -> &'b u8 { &'b data.0 }

fn main() {
    'x: {
        let clo = Closure { data: (0, 1), func: do_it };
        println!("{}", clo.call());
    }
}

我们究竟应该如何表达F的 trait 约束上的生命周期?我们需要在那里提供一些生命周期,但是我们关心的生命周期在进入call的主体之前是不能被命名的! 而且,这并不是什么固定的生命周期;call可以与&self在这一时刻上的任一生命周期一起使用。

要完成这个事情,需要使用到高阶 Trait 约束(HRTB)的魔力。我们的解语法糖方式如下:

where for<'a> F: Fn(&'a (u8, u16)) -> &'a u8,

或者:

where F: for<'a> Fn(&'a (u8, u16)) -> &'a u8,

(其中Fn(a, b, c) -> d本身只是不稳定的真正的*Fn特性的语法糖)

for<'a>可以理解为“对于所有'a的可能”,并且基本上产生一个无限的 F 必须满足的 trait 约束的列表。不过不用紧张,在Fn trait 之外,我们遇到 HRTB 的地方不多,即使是那些地方,我们也有一个很好的魔法糖来处理普通的情况。

最终,我们可以把原本的代码重写成更加显式的样子:

struct Closure<F> {
    data: (u8, u16),
    func: F,
}
impl<F> Closure<F>
    where for<'a> F: Fn(&'a (u8, u16)) -> &'a u8,
{
    fn call(&self) -> &u8 {
        (self.func)(&self.data)
    }
}
fn do_it(data: &(u8, u16)) -> &u8 { &data.0 }
fn main() {
    let clo = Closure { data: (0, 1), func: do_it };
    println!("{}", clo.call());
}

子类型化和变异性

Rust 使用生命周期来追踪借用和所有权。 但是,原生的生命周期实现可能过于严格,或者会允许未定义行为。

为了实现对生命周期的灵活使用并防止滥用,Rust 使用 子类型变异

让我们从一个例子开始。

// 注意:debug 需要两个具有相同生命周期的参数
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 的生命周期比 '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)。

这表示生命周期 Sub 的范围要包含 Super 的范围,并且 Sub 的范围有可能更大。

现在,为了使生命周期子类型化,我们需要先定义一个生命周期:

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

然后我们就可以定义它们之间的关系:

当且仅当 'long 是一个 完全包含 'short 的代码区域时,'long <: 'short

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

虽然在本章后面,子类型化比这要复杂和微妙得多,但这个简单的规则在大多数情况下都适用。除非您编写不安全的代码,否则编译器将为您自动处理所有的特殊情况。

但这是 Rustonomicon。我们正在编写不安全的代码,所以我们需要了解这些东西是如何真正工作的,以及我们如何搞乱它。

回到我们上面的例子,我们可以说 '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 的生命周期比 'static 短
        debug(hello, world); // hello 从 `&'static str` 静默收缩为 `&'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}"); // 使用在被释放后的值 😿
}

assign 中,我们将 hello 引用设置为指向 world。 但是 worldprintln 使用 hello 之前就已经超出了作用域!

这是一个典型的在释放后使用错误!

我们第一反应可能是怪 assign 的实现,但实际上这里并没有什么错误。一个值想要赋值到一个具有相同类型的 T 也不奇怪。

所以,问题在于,我们不能假设 &mut &'static str 也可以转换成 &mut &'b str。 这意味着,即使 'static'b 的子类型,&mut &'static str不能&mut &'b str 的子类型。

变异性 是 Rust 引用通过它们的泛型参数,来定义引用之间的子类型关系。

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

类型 F变异性 代表了其输入子类型如何影响其输出子类型。

在 Rust 中有三种变异性,假设 SubSuper 的子类型:

  • F协变的,如果 F<Sub>F<Super> 的子类型(子类型属性被传递)(译者注:这里被传递的意思是尖括号里面的子类型关系(Sub <: Super)被传递到尖括号外(F<Sub> <: F<Super>))
  • F逆变的,如果 F<Super>F<Sub> 的子类型(子类型属性被 "反转")(译者注:即尖括号里面的子类型关系(Sub <: Super)在尖括号外面被反转(F<Super> <: F<Sub>))
  • 否则,F不变的 (不存在子类型关系)(译者注:即尖括号里面的子类型关系不会影响尖括号外面的子类型关系)

让我们回想上面的例子,如果 'a'b 的子类型,我们可以将 &'a T 视作是 &'b T 的子类型,因而&'a T对于 'a 上是协变的。

此外,我们注意到不能将 &mut &'a T 视为 &mut &'b T 的子类型,因此我们可以说 &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 相同的变异性属性 (译者注:因为具有内部可变性的UnsafeCell<T> Cell<T>等,都可以通过仅仅使用 &T 进行 &mut 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 <: 'b 时成立),&'static str 才可以变为 &'b str。这是正确的,因此编译器很乐意继续编译这段代码。

事实证明,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 的变量时,我们销毁了唯一记住它生命周期为'static的东西!我们不再需要 hello 的生命周期更长了!

现在还剩一件事要解释:函数指针。

要了解为什么 fn(T) -> U 应该在 U 上是协变的,请思考一下这个签名:

fn get_str() -> &'a str;

该函数声明可以生成一个由某个生命周期 'a 绑定的 str。类似地,我们可以使用以下签名来定义一个函数:

fn get_static() -> &'static str;

所以当函数被调用时,它只期望一个生命周期至少为 'a&str 的值,至于这个值的生命周期是不是比 'a 更长,并不重要。

然而,相同的逻辑不能应用于函数参数。思考一下:

fn store_ref(&'a str);

fn store_static(&'static str);

第一个函数可以接受任何字符串引用,只要它的生命周期包含 'a,但第二个函数不能接受一个生命周期小于 'static 的字符串引用,这将导致冲突。变异性不适用于此。但是,如果我们将其反过来,实际上确实行得通!如果我们需要一个可以处理 &'static str 的函数,一个可以处理任意引用生命周期的函数肯定可以很好地工作。

让我们看看实践中的例子

thread_local! {
    pub static StaticVecs: RefCell<Vec<&'static str>> = RefCell::new(Vec::new());
}

/// 将给定的输入保存到一个thread local的 `Vec<&'static str>`
fn store(input: &'static str) {
    StaticVecs.with_borrow_mut(|v| v.push(input));
}

/// 用有着相同生命周期的参数 `input` 去调用给定的函数
fn demo<'a>(input: &'a str, f: fn(&'a str)) {
    f(input);
}

fn main() {
    demo("hello", store); // "hello" 是 'static。可以正常调用 `store`

    {
        let smuggle = String::from("smuggle");

        // `&smuggle` 的生命周期并非· `'static`。
        // 如果我们用 `&smuggle` 调用 `store`,
        // 我们将把一个无效的生命周期推入 `StaticVecs`。
        // 因此,`fn(&'static str)` 不能是 `fn(&'a str)` 的子类型
        demo(&smuggle, store);
    }

    // use after free 😿
    StaticVecs.with_borrow(|v| println!("{v:?}"));
}

这就是为什么函数类型,与语言中的其他内容不同,是逆变的。

现在,你对于标准库提供的类型的变异性应有了充分的理解,但是如何确定定义的类型的变异性呢? 不太规范地说,结构体继承了其字段的变异性。如果一个结构体 MyType 有一个泛型参数 A,并且在字段 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,     // 对 'a 和 A 是协变的
    b: &'b mut B, // 对 'b 是协变的,对 B 是不变的

    c: *const C,  // 对 C 是协变的
    d: *mut D,    // 对 D 是不变的

    e: E,         // 对 E 是协变的
    f: Vec<F>,    // 对 F 是协变的
    g: Cell<G>,   // 对 G 是不变的

    h1: H,        // 本来也会对 H 是协变的,但...
    h2: Cell<H>,  // 对 H 是不变的,因为不变性在所有冲突中都是胜利者

    i: fn(In) -> Out,       // 对 In 是逆变的,对 Out 是协变的

    k1: fn(Mixed) -> usize, // 本来会对 Mixed 是逆变的,但...
    k2: Mixed,              // 对 Mixed 是不变的,因为不变性在所有冲突中都是胜利者
}
}

现在你对 Rust 中的子类型和变异性概念应该有了更深入的理解。尽管本章涵盖了许多概念,但通过编译器和类型系统所提供的严密检查来确保这些规则得到遵循和安全操作。当编写泛型代码时,要确保您正确理解子类型化和变异性,以避免出现意外错误和潜在安全问题。

丢弃检查

我们已经看到了生命周期如何为我们提供了一些相当简单的规则来确保我们永远不会读到悬空的引用。但是到目前为止, outlives 是一种包容的关系。也就是说,当我们谈论'a: 'b时,'a可以和'b的寿命一样长。乍一看,这似乎是一个无意义的特点。没有什么东西会和另一个东西同时被丢弃,对吗?这就是为什么我们对以下let语句解语法糖:

let x;
let y;

解语法糖:

{
    let x;
    {
        let y;
    }
}

有一些更复杂的情况不可能用作用域来解语法糖,但顺序是被定义好的——变量按其定义的相反顺序丢弃,结构体和元组的字段按其定义的顺序丢弃。在 RFC 1857 中有一些关于丢弃顺序的更多细节。

让我们来试试:

let tuple = (vec![], vec![]);

左边的 Vec 先被丢弃。但这是否意味着在借用检查器的眼中,右边 Vec 一定活得更长?这个问题的答案是 No。借用检查器可以分别跟踪元组的字段,但它仍然无法知道 哪个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<'_>`

你可以尝试改变字段的顺序,或者用一个元组来代替struct,但还是不能编译。

实现Drop可以让Inspector在被丢弃时执行一些代码。使得它有可能观察到那些本该和它生命周期一样长的类型实际上是先被销毁的。

有趣的是,只有泛型需要担心这个问题。如果它们不是泛型的,那么它们唯一能承载的寿命就是'static,它将真正地一直活着。这就是为什么这个问题被称为 sound generic drop。健壮的泛型丢弃是由 drop checker 强制执行的。截止到本文写作时,关于丢弃检查器(也被称为dropck)如何验证类型的一些更细微的细节还完全是未知数。然而,“大规则”是我们这一节所关注的微妙之处:

对于一个泛型类型来说,要健壮地实现 drop,其泛型参数必须严格超过它的寿命。

遵守这一规则(通常)是满足借用检查器的必要条件;遵守这一规则是健壮地泛型丢弃的充分不必要条件。即如果你的类型遵守了这个规则,那么它的 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` 刚好在这里析构了,
    // 并且假设析构函数可以确保:该函数确保不会访问对 `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` 刚好在这里析构了,
    // 并且假设析构函数可以确保:该函数确保不会访问对 `days` 的引用
}

然而,上述两种变体在分析fn main时都被借用检查器拒绝了,说days的生命周期不够长。

原因是对main的借用检查分析时,借用检查器并不了解每个InspectorDrop实现的内部情况。就借用检查器在分析main时知道的情况来看,检查器的析构器主体可能会访问这些借用的数据。

因此,丢弃检查器强迫一个值中的所有借用数据的生命周期严格地超过该值的生命周期。

一种逃逸方法

丢弃检查的精确规则在未来可能会减少限制。

目前的分析是故意保守的;它强制一个值中的所有借来的数据的生命周期超过该值的生命周期,这当然是合理的。

未来版本的语言可能会使分析更加精确,以减少正确代码被拒绝为不安全的情况。这将有助于解决诸如上述两个Inspector知道在销毁时不访问借来的数据的情况。

但与此同时,有一个不稳定的属性,可以用来断言(不安全的)泛型的析构器 保证 不访问任何失效数据,即使它的类型赋予它这样的能力。

这个属性被称为may_dangle,是在RFC1327中引入的。要在上面的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, "gadget"));
}

使用这个属性需要将Drop标记为unsafe,因为编译器没有检查隐含的断言,即没有访问潜在的失效数据(例如上面的self.0)。

该属性可以应用于任何数量的生命周期和类型参数。在下面的例子中,我们断言我们没有访问寿命为'b的引用后面的数据,并且T的唯一用途是 move 或 drop,但是从'aU中省略了该属性,因为我们确实访问具有该生命周期和该类型的数据。

#![allow(unused)]
#![feature(dropck_eyepatch)]
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 方法调用

(未来对语言的修改,如 impl 的特化,可能会增加这种间接访问的其他途径。)

下面是一个回调的例子:

#![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 self) {
        // 这里可能隐藏了一个对于 `<T as Display>::fmt` 的调用,
        // 如果 `T` 是 `&'a _` 这种类型,就可能访问了借用的变量
        println!("Inspector({}, {}) unwittingly inspects expired data.",
                 self.0, self.1);
    }
}
}

当然,所有这些访问都可以进一步隐藏在由析构器调用的一些其他方法中,而不是直接写在析构器中。

在上述所有在析构器中访问&'a u8的情况下,添加#[may_dangle]属性使得该类型容易被误用,而借用检查器不会发现,从而导致问题。所以最好不要添加这个属性。

关于丢弃顺序的附带说明

虽然结构内部字段的删除顺序是被定义的,但对它的依赖是脆弱而微妙的。当顺序很重要时,最好使用ManuallyDrop包装器。

这就是关于丢弃检查器的全部内容吗?

事实证明,在编写不安全的代码时,我们通常根本不需要担心为丢弃检查器做正确的事情。然而,有一种特殊情况是需要担心的,我们将在下一节看一下。

幽灵数据

在处理不安全代码时,我们经常会遇到这样的情况:类型或生命周期在逻辑上与结构相关,但实际上并不是字段的一部分。这种情况最常发生在生命周期上。例如,&'a [T]Iter(大约)定义如下:

#![allow(unused)]
fn main() {
struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
}
}

但是由于'a在结构体中是未使用的,所以它是无约束的。由于这在历史上造成的麻烦,在结构定义中,不受约束的生命周期和类型是禁止的,因此我们必须在主体中以某种方式引用这些类型,正确地做到这一点对于正确的变异性和丢弃检查是必要的。

我们使用PhantomData来做这个,它是一个特殊的标记类型。PhantomData不消耗空间,但为了静态分析的目的,模拟了一个给定类型的字段。这被认为比明确告诉类型系统你想要的变量类型更不容易出错,同时也提供了其他有用的东西,例如 auto traits 和 drop check 需要的信息。

Iter 逻辑上包含一堆&'a T,所以这正是我们告诉PhantomData要模拟的。

#![allow(unused)]
fn main() {
use std::marker;

struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
    _marker: marker::PhantomData<&'a T>,
}
}

就是这样,生命周期将被限定,而你的迭代器将在'aT上进行协变。所有的东西都是有效的。

泛型参数和 drop 检查

在过去,曾经有另一个事情是需要仔细思考的,这篇文档曾经这么说:

另一个重要的例子是 Vec,它(大约)定义如下:

#![allow(unused)]
fn main() {
struct Vec<T> {
    data: *const T, // `*const`是可变异的!
    len: usize,
    cap: usize,
}
}

与前面的例子不同的是,看起来一切都和我们想的一样。Vec 的每个通用参数至少在一个字段中出现。很好,可以开始了!

不对,不是这样。

丢弃检查器将慷慨地确定Vec<T>不拥有任何 T 类型的值。这将反过来使它得出结论,它不需要担心 Vec 在其析构器中丢弃任何 T 来确定丢弃检查的合理性。这将反过来允许人们使用 Vec 的析构器来制造不健壮性。

为了告诉 dropck 我们确实拥有 T 类型的值,因此在我们丢弃时可能会丢弃一些 T,我们必须添加一个额外的PhantomData,正如这样:

#![allow(unused)]
fn main() {
use std::marker;

struct Vec<T> {
    data: *const T, // `*const`是可变异的!
    len: usize,
    cap: usize,
    _marker: marker::PhantomData<T>,
}
}

但自从RFC 1238之后,这就不正确也并不需要了。

如果你这么写:

#![allow(unused)]
fn main() {
struct Vec<T> {
    data: *const T, // `*const`是可变异的!
    len: usize,
    cap: usize,
}
#[cfg(any())]
impl<T> Drop for Vec<T> { /* … */ }
}

那么impl<T> Drop for Vec<T>这条语句会让 Rust 知道Vec<T>拥有T类型的值(更准确地说:可能会在Drop实现中使用T类型的值),那么当Vec<T>被 drop 的时候,Rust 就不会允许它们 悬垂

当一个类型已经有了 Drop impl 时,添加一个额外的 _owns_T: PhantomData<T> 字段是多余的,而且没有任何效果,从 dropck(Drop 检查)的角度来看(它仍然会影响变量和自动特征)。

  • (高级边缘情况:如果包含 PhantomData 的类型根本没有 Drop 实现,但仍然有 drop glue(通过拥有另一个带有 drop glue 的字段),那么这里提到的 dropck/#[may_dangle] 规则也同样适用:一个 PhantomData<T> 字段将要求 T 在包含类型作用域结束时可被丢弃)。

但是这在某些场景下,会导致过于严格,这也是为啥标准库使用了一个不稳定并且unsafe的属性来切换回旧的unchecked的 drop 检查行为,也是接下来这个文档所警告的:#[may_dangle]属性。

一个例外:标准库的特殊情况及不稳定的#[may_dangle]

如果你只是写自己的库代码,那你可以跳过这章;但是如果你想知道标准库中真正的Vec是怎么实现的,你会发现它仍然需要_owns_T: PhantomData<T>字段来保证可靠性。

点这里查看原因

思考以下这个例子:

fn main() {
    let mut v: Vec<&str> = Vec::new();
    let s: String = "Short-lived".into();
    v.push(&s);
    drop(s);
} // <- `v`在这里被 drop 了

对于一个经典的impl<T> Drop for Vec<T> {定义,上面这段代码会被编译器拒绝

实际上,在这个例子中,我们的Vec的类型实际上是Vec</* T = */ &'s str>,是一个元素为's生命周期的string 的 Vec,但是由于上面还有一行定义let s: String,它在Vec drop 之前就被 drop 了,所以在Vec被 drop 的时候,'s已经不再有效了,这时候Drop的实际签名为:impl<'s> Drop for Vec<&'s str> {

这意味着,Drop被调用时,它将会面对一个无效的,或者说悬垂(dangling)的生命周期's。这是违背了 Rust 原则的,Rust 原则要求所有的函数中的 Rust 引用都必须有效,解引用操作必须是合法的。

这也是为什么 Rust 会保守地拒绝这段代码。

然而,在真正的Vec中,Drop的实现并不关心&'s str,毕竟它(译者注:&'s str)没有自己的Drop实现(since it has no drop glue of its own),它想做的只是把它自己的 buffer 给释放掉。

换句话说,如果上述这个片段能被 Rust 接受那就再好不过了,我们通过封装Vec,或者说可以依赖于Vec一些特殊的属性:Vec可以承诺当它被 drop 时不会使用它拥有的&'s str

这是一种unsafe的承诺,可以通过#[may_dangle]来表达:

unsafe impl<#[may_dangle] 's> Drop for Vec<&'s str> { /* … */ }

或者,更通用化的:

unsafe impl<#[may_dangle] T> Drop for Vec<T> { /* … */ }

这就是一个unsafe的方法用来摆脱 Rust drop 检查器这个保守的假设——一个 drop 的实例的类型参数不允许是悬垂的。

并且当这样做时,例如在标准库中,我们需要小心T有自己的Drop实现。比如,在这种情况下,想象用struct PrintOnDrop<'s> /* = */ (&'s str);替换&'s str,这将具有Drop impl,其内部的&'s str将被解引用并打印到屏幕上。

实际上,Drop for Vec<T> {,在释放自己的 Buffer 之前,确实必须在每个T类型的元素具有自定义Drop实现时递归地删除它;在 PrintOnDrop<'s>的情况下,这意味着Vec<PrintOnDrop<'s>>Drop必须在释放 Buffer 之前递归地删除PrintOnDrop<'s>的元素。

所以当我们说's #[may_dangle] 时,这是一个过于宽松的说法。我们更期望这么说说:“'s可能会悬垂,前提是它不涉及一些Drop自定义实现”。或者,更一般地说,“T可能会悬空,前提是它不涉及某些Drop自定义实现”。每当**我们拥有一个T**时,这种“例外的例外”是一种普遍的情况。这就是为什么 Rust 的#[may_dangle]足够聪明,当泛型参数以拥有的方式 被 struct 的某个字段所保存时,会被禁用。(原文:That's why Rust's #[may_dangle] is smart enough to know of this opt-out, and will thus be disabled when the generic parameter is held in an owned fashion by the fields of the struct.)

这就是为什么最终标准库是这么写的:

#![allow(unused)]
fn main() {
#[cfg(any())]
// 我们拉勾说好,当 drop `Vec`的时候不去用`T`
unsafe impl<#[may_dangle] T> Drop for Vec<T> {
    fn drop(&mut self) {
        unsafe {
            if mem::needs_drop::<T>() {
                /* … 除了这里,也就是说,… */
                ptr::drop_in_place::<[T]>(/* … */);
            }
            // …
            dealloc(/* … */)
            // …
        }
    }
}

struct Vec<T> {
    // … 除非事实上`Vec`拥有了`T`类型的元素,并且可能在 drop 时 drop 它们
    _owns_T: core::marker::PhantomData<T>,
    ptr: *const T, // `*const`是可变异的(但这本身并不能表达对`T`的所有权)
    len: usize,
    cap: usize,
}
}

拥有内存分配的原始指针是如此普遍的模式,以至于标准库为自己整了一个名为Unique<T>的类型:

  • 包装一个*const T,用于变异
  • 包括一个PhantomData<T>
  • 根据包含的 T 自动派生Send/Sync
  • 空指针的优化,将指针标记为NonZero

PhantomData模式表

下面是一个关于所有可以使用PhantomData的神奇方式的表格: (covariant:协变,invariant:不变,contravariant:逆变)

Phantom typevariance of 'avariance of TSend/Sync
(or lack thereof)
dangling 'a or T in drop glue
(e.g., #[may_dangle] Drop)
PhantomData<T>-covariantinheriteddisallowed ("owns T")
PhantomData<&'a T>covariantcovariantSend + Sync
requires
T : Sync
allowed
PhantomData<&'a mut T>covariantinvariantinheritedallowed
PhantomData<*const T>-covariant!Send + !Syncallowed
PhantomData<*mut T>-invariant!Send + !Syncallowed
PhantomData<fn(T)>-contravariantSend + Syncallowed
PhantomData<fn() -> T>-covariantSend + Syncallowed
PhantomData<fn(T) -> T>-invariantSend + Syncallowed
PhantomData<Cell<&'a ()>>invariant-Send + !Syncallowed
  • 注意: opt-out Unpin 自动特性需要专用的 PhantomPinned 类型。

拆分 Borrows

在处理复合结构时,可变引用的互斥属性会有很大的限制。借用检查器理解一些基本的东西,但是很容易就会出现问题。它对结构有足够的了解,知道有可能同时借用一个结构中不相干的字段。所以现在这个方法是可行的:

#![allow(unused)]
fn main() {
struct Foo {
    a: i32,
    b: i32,
    c: i32,
}

let mut x = Foo {a: 0, b: 0, c: 0};
let a = &mut x.a;
let b = &mut x.b;
let c = &x.c;
*b += 1;
let c2 = &x.c;
*a += 10;
println!("{} {} {} {}", a, b, c, c2);
}

然而 borrowck 完全不理解数组或 slice,所以这会挂:

#![allow(unused)]
fn main() {
let mut x = [1, 2, 3];
let a = &mut x[0];
let b = &mut x[1];
println!("{} {}", a, b);
}
error[E0499]: cannot borrow `x[..]` as mutable more than once at a time
 --> src/lib.rs:4:18
  |
3 |     let a = &mut x[0];
  |                  ---- first mutable borrow occurs here
4 |     let b = &mut x[1];
  |                  ^^^^ second mutable borrow occurs here
5 |     println!("{} {}", a, b);
6 | }
  | - first borrow ends here

error: aborting due to previous error

虽然 borrowck 能理解这个简单的案例是合理的,但对于 borrowck 来说,要理解像树这样的一般容器类型的不连通性显然是没有希望的,尤其是当不同的键确实映射到相同的值时。

为了“教导” borrowck 我们正在做的事情是正确的,我们需要使用到不安全的代码。例如,可变 slice 暴露了一个split_at_mut函数,它消耗这个 slice 并返回两个可变 slice。一个用于索引左边的所有内容,一个用于右边的所有内容。直观地讲,我们知道这是安全的,因为这些分片不会重叠,因此可以进行别名操作。然而,这个实现需要一些不安全代码:

#![allow(unused)]
fn main() {
use std::slice::from_raw_parts_mut;
struct FakeSlice<T>(T);
impl<T> FakeSlice<T> {
fn len(&self) -> usize { unimplemented!() }
fn as_mut_ptr(&mut self) -> *mut T { unimplemented!() }
pub fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
    let len = self.len();
    let ptr = self.as_mut_ptr();

    unsafe {
        assert!(mid <= len);

        (from_raw_parts_mut(ptr, mid),
         from_raw_parts_mut(ptr.add(mid), len - mid))
    }
}
}
}

这实际上是有点微妙的。为了避免对同一个值进行两次&mut,我们明确地通过原始指针构造全新的切片。

然而,更微妙的是产生可变引用的迭代器如何工作。迭代器 trait 定义如下:

#![allow(unused)]
fn main() {
trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
}

考虑到这个定义,Self::Item 与self没有联系。这意味着我们可以连续多次调用next,并将所有的结果并发地保留下来。这对逐值迭代器来说是非常好的,因为它有这样的语义。这对共享引用来说也很好,因为它们允许对同一事物有任意多的引用(尽管迭代器需要和被共享的事物是一个独立的对象)。

但是可变的引用让这变得一团糟。乍一看,它们似乎与这个 API 完全不兼容,因为它将产生对同一个对象的多个可变引用!

然而它实际上有效的,正是因为迭代器是一次性的对象。IterMut 产生的所有东西最多只能产生一次,所以我们实际上不会产生对同一块数据的多个可变引用。

也许令人惊讶的是,对于许多类型,可变迭代器不需要实现不安全的代码。

例如,这里有一个单向链表:

fn main() {}
type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    elem: T,
    next: Link<T>,
}

pub struct LinkedList<T> {
    head: Link<T>,
}

pub struct IterMut<'a, T: 'a>(Option<&'a mut Node<T>>);

impl<T> LinkedList<T> {
    fn iter_mut(&mut self) -> IterMut<T> {
        IterMut(self.head.as_mut().map(|node| &mut **node))
    }
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        self.0.take().map(|node| {
            self.0 = node.next.as_mut().map(|node| &mut **node);
            &mut node.elem
        })
    }
}

下面是一个可变的 slice:

fn main() {}
use std::mem;

pub struct IterMut<'a, T: 'a>(&'a mut[T]);

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        let slice = mem::take(&mut self.0);
        if slice.is_empty() { return None; }

        let (l, r) = slice.split_at_mut(1);
        self.0 = r;
        l.get_mut(0)
    }
}

impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        let slice = mem::take(&mut self.0);
        if slice.is_empty() { return None; }

        let new_len = slice.len() - 1;
        let (l, r) = slice.split_at_mut(new_len);
        self.0 = l;
        r.get_mut(0)
    }
}

接着是一个二叉树:

fn main() {}
use std::collections::VecDeque;

type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    elem: T,
    left: Link<T>,
    right: Link<T>,
}

pub struct Tree<T> {
    root: Link<T>,
}

struct NodeIterMut<'a, T: 'a> {
    elem: Option<&'a mut T>,
    left: Option<&'a mut Node<T>>,
    right: Option<&'a mut Node<T>>,
}

enum State<'a, T: 'a> {
    Elem(&'a mut T),
    Node(&'a mut Node<T>),
}

pub struct IterMut<'a, T: 'a>(VecDeque<NodeIterMut<'a, T>>);

impl<T> Tree<T> {
    pub fn iter_mut(&mut self) -> IterMut<T> {
        let mut deque = VecDeque::new();
        if let Some(root) = self.root.as_mut() {
            deque.push_front(root.iter_mut());
        }
        IterMut(deque)
    }
}

impl<T> Node<T> {
    pub fn iter_mut(&mut self) -> NodeIterMut<T> {
        NodeIterMut {
            elem: Some(&mut self.elem),
            left: self.left.as_deref_mut(),
            right: self.right.as_deref_mut(),
        }
    }
}


impl<'a, T> Iterator for NodeIterMut<'a, T> {
    type Item = State<'a, T>;

    fn next(&mut self) -> Option<Self::Item> {
        self.left.take().map(State::Node).or_else(|| {
            self.elem
                .take()
                .map(State::Elem)
                .or_else(|| self.right.take().map(State::Node))
        })
    }
}

impl<'a, T> DoubleEndedIterator for NodeIterMut<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        self.right.take().map(State::Node).or_else(|| {
            self.elem
                .take()
                .map(State::Elem)
                .or_else(|| self.left.take().map(State::Node))
        })
    }
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;
    fn next(&mut self) -> Option<Self::Item> {
        loop {
            match self.0.front_mut().and_then(Iterator::next) {
                Some(State::Elem(elem)) => return Some(elem),
                Some(State::Node(node)) => self.0.push_front(node.iter_mut()),
                None => {
                    self.0.pop_front()?;
                }
            }
        }
    }
}

impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        loop {
            match self.0.back_mut().and_then(DoubleEndedIterator::next_back) {
                Some(State::Elem(elem)) => return Some(elem),
                Some(State::Node(node)) => self.0.push_back(node.iter_mut()),
                None => {
                    self.0.pop_back()?;
                }
            }
        }
    }
}

所有这些都是完全安全的,并且可以在稳定的 Rust 上运行!这最终落在了我们之前看到的简单结构案例中。Rust 知道你可以安全地将一个可变的引用分割成子字段。然后我们可以通过 Options(或者在分片的情况下,用空分片替换)来消耗掉这个引用并进行编码。

评论 (0)