借用检查

Rust 最重要的是所有权和借用模式。基于严格执行借用规则,编译器可以在没有外部垃圾收集的情况下保证内存安全。这是由借用检查器(编译器的子系统)完成的。根据定义,创建的每个资源都有一个生命周期和一个与之关联的所有者,它根据以下规则运行:

  • 每个资源在任何时间点都只有一个所有者。默认情况下,所有者是创建该资源的变量,其生命周期封闭范围的生命周期。其他人可以在需要时借用或者复制资源。请注意,资源可以是变量或者函数中的任何内容。函数从其调用者获得资源的所有权;从函数返回转回所有权。
  • 当所有者的作用域已完成执行时,将删除其拥有的所有资源。这由编译器静态计算,然后编译器相应地生成机器代码。

以下代码段中显示了这些规则的一些示例:

// code/chapter2/ownership-heap.rs

fn main() {
    let s = String::from("Test");
    heap_example(s);   
}

fn heap_example(input: String) {
    let mystr = input;
    let _otherstr = mystr;
    println!("{}", mystr);
}

在 Rust 中, 变量是使用 let 关键字声明的。默认情况下, 所有变量都是不可变的, 可以使用 mut 关键字使其可变。:: 语法是指给定命名空间中的对象, 在本例中为 from 函数。println! 是编译器提供的内置宏;它用于写入标准输出与尾随换行符。函数是使用 fn 关键字定义的。当我们试图编译它, 得到以下错误:

# rustc ownership-heap.rs 
error[E0382]: use of moved value: `mystr` 
    --> ownership-heap.rs:9:20
    | 
  8 | let _otherstr = mystr;
    | --------- value moved here 
  9 | println!("{}", mystr);
    | ^^^^^ value used here after move
    |
    = note: move occurs because `mystr` has type `std::string::String`, which does not implement the `Copy` trait

error: aborting due to previous error

在这个例子中,在函数 heap_example 中创建了一个由变量 mystr 拥有的字符串资源。因此,它的生命周期与它的范围相同。由于编译器在编译时不知道字符串的长度, 因此必须将其放置在堆上。所有者变量是在堆栈上创建的, 并指向堆上的资源。当我们将该资源分配给新变量时, 该资源现在归新变量所有。此时, rust 将把 mystr 标记为无效, 以防止与资源关联的内存可能被多次释放的情况。因此, 编译不能在这里保证内存安全。我们可以强制编译器复制资源, 并让第二个所有者指向新创建的资源。为此, 我们需要 .clone() 名为 mystr 的资源。下面是它的样子:

// code/chapter2/ownership-heap-fixed.rs

fn main() {
    let s = String::from("Test");
    heap_example(s);
}

fn heap_example(input: String) { 
    let mystr = input; 
    let _otherstr = mystr.clone(); 
    println!("{}", mystr); 
}

如预期的那样, 这不会在编译时引发任何错误, 并在运行时打印给定的字符串 "Test"。请注意, 到目前为止, 我们一直在使用 Cargo 来运行我们的代码。因为在这种情况下, 我们只有一个简单的文件, 没有外部依赖关系, 我们将使用 Rust 编译器直接编译我们的代码, 然后手动运行它:

$ rustc ownership-heap-fixed.rs && ./ownership-heap-fixed 
Test 

请考虑下面的代码, 它显示了资源存储在堆栈上的情况:

// code/chapter2/ownership-stack.rs

fn main() {
    let i = 42;
    stack_example(i);
}

fn stack_example(input: i32) {
    let x = input;
    let _y = x;
    println!("{}", x);
}

有趣的是, 尽管它看起来与以前的代码块完全相同, 但这不会引发编译错误。我们直接从命令行使用 Rust 编译器构建和运行:

# rustc ownership-stack.rs && ./ownership-stack 
42

区别在于变量的类型。在这里, 原始所有者和资源都是在堆栈上创建的。重新分配资源后, 会将其复制到新所有者。之所以能够做到这一点, 只是因为编译器知道整数的大小始终是固定的 (因此可以放在堆栈上)。 Rust 提供了一种特殊的方式来表示, 可以通过 Copy 特征将类型放置在堆栈上。我们的示例之所以有效, 只是因为内置整数 (和其他一些类型) 使用此特性进行标记。我们将在后面的章节中更详细地解释特征系统。

人们可能已经注意到, 将长度未知的资源复制到函数可能会导致内存膨胀。在许多语言中, 调用方将向内存位置传递一个指针, 然后传递到函数。Rust 通过使用引用来执行此操作。这样, 您就可以引用资源, 而不实际拥有资源。当函数收到对资源的引用时, 我们说它借用了该资源。在下面的示例中, 函数 heap_example 借用变量 s 所拥有的资源。由于借用不是绝对所有权, 因此借用变量的范围不会影响释放与资源关联的内存的方式。这也意味着不可能在函数中多次释放借来的资源, 因为函数作用域中没有人实际拥有该资源。因此, 在这种情况下, 早期失败的代码起作用:

// code/chapter2/ownership-borrow.rs

fn main() {
    let s = String::from("Test");
    heap_example(&s);
}

fn heap_example(input: &String) {
    let mystr = input;
    let _otherstr = mystr;
    println!("{}", mystr);
}

借用规则还意味着借用是不可改变的,然而,在有些情况下,我们需要可变借用。为了处理这种情况,Rust 允许可变引用(或借用)。这将使我们回到第一个示例中遇到的编译失败的问题,代码如下:

// code/chapter2/ownership-mut-borrow.rs

fn main() {
    let mut s = String::from("Test");
    heap_example(&mut s);
}

fn heap_example(input: &mut String) {
    let mystr = input;
    let _otherstr = &mystr;
    println!("{}", mystr);
}


请注意, 资源只能在作用域中被可变借用一次。编译器将拒绝编译尝试执行其他操作的代码。虽然这看起来可能是一个令人讨厌的错误, 但您需要记住, 在工作的应用程序中, 这些函数通常会从竞争线程调用。如果由于编程错误而出现同步错误, 我们最终将出现数据争用, 其中多个不同同步的线程竞相修改同一资源。此功能有助于防止这种情况。

与引用密切相关的另一个语言功能是生命周期。一个引用只要在范围内就会存活,因此它的生命周期是一整个封闭作用域。在 Rust 中声明的所有变量都可以有一个生命周期的显式省略,将一个名称置于其生命周期。这对于借用检查器来说很有用,可以解释变量的相对生命周期。通常, 不需要为每个变量都设置显式生存期名称, 因为编译器管理该名称。在某些情况下, 这是必需的, 尤其是在自动生命周期确定无法工作的情况下。让我们看一个发生这种情况的例子:

// code/chapter2/lifetime.rs
fn main() {
    let v1 = vec![1, 2, 3, 4, 5];
    let v2 = vec![1, 2];

    println!("{:?}", longer_vector(&v1, &v2));
}

fn longer_vector<'a>(x: &'a[i32], y: &'a[i32]) -> &'a[i32] {
    if x.len() > y.len() { x } else {y }
}


vec! 宏从给定的对象列表中构造一个向量。请注意,与前面的示例不同,我们在此处的函数需要将值返回给调用方。我们需要使用箭头语法指定返回类型,在这里,我们得到两个向量,我们要打印两个向量中最长的一个。longer_vector 函数完成这个功能。它接受两个向量的引用,计算它们的长度,并返回长度较大的向量的引用。编译失败,错误如下:

# rustc lifetime.rs 
error[E0106]: missing lifetime specifier 
    --> lifetime.rs:8:43
    | 
  8 | fn longer_vector(x: &[i32], y: &[i32]) -> &[i32] {
    | ^ expected lifetime parameter
    |
    = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`

error: aborting due to previous error

这告诉我们, 编译器无法确定返回的引用是应引用第一个参数还是第二个参数, 因此无法确定它应该存在多长时间。由于我们无法控制输入, 因此无法在编译时确定这一点。这里的一个关键见解是, 我们不需要在编译时知道所有引用的生命周期。我们需要确保以下事情成立:

  • 这两个输入应该具有相同的生存期, 因为我们要在函数中比较它们的长度
  • 返回值应该与输入参数中更长的一个有着相同的生命周期。

考虑到这两个公理, 这两个输入和返回应该具有相同的生存期。我们可以对此进行注释, 如下面的代码段所示:

fn longer_vector<'a>(x: &'a[i32], y: &'a[i32]) -> &'a[i32] { 
    if x.len() > y.len() { x } else { y } 
}

这与预期的工作一样, 因为编译器可以很好地保证代码的正确性。生存期参数也可以附加到结构和方法定义。有一个特殊的生命周期叫做 'static, 它指的是程序的整个持续时间。

Rust 最近接受了一项建议, 即增加一个名为 'fn 的新的指定生命周期, 其生命周期等于最里面的函数或闭包的范围。