引用与借用

清单 4-5 中元组代码的问题在于,我们必须将 String 返回给调用函数,以便在调用 calculate_length 之后仍然可以使用 String,因为 String 被移动到了 calculate_length 中。作为替代,我们可以提供 String 值的引用。引用 类似于指针,因为它是一个地址,我们可以通过该地址访问存储在该地址的数据;该数据由其他变量拥有。与指针不同,引用可以保证在该引用的生命周期内指向特定类型的有效值。

下面是如何定义和使用 calculate_length 函数,该函数将对象的引用作为参数,而不是获取值的所有权

文件名:src/main.rs

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

首先,请注意变量声明和函数返回值中的所有元组代码都消失了。其次,请注意我们将 &s1 传递给 calculate_length,并且在其定义中,我们使用 &String 而不是 String。这些符号 & 表示引用,它们允许您引用某个值而不获取其所有权。图 4-5 描述了这个概念。

Three tables: the table for s contains only a pointer to the table
for s1. The table for s1 contains the stack data for s1 and points to the
string data on the heap.

图 4-5:指向 String s1&String s 的示意图

注意:使用 & 进行引用的反义词是解引用,这是通过解引用运算符 * 完成的。我们将在第 8 章中看到解引用运算符的一些用法,并在第 15 章中讨论解引用的细节。

让我们仔细看看这里的函数调用

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

&s1 语法允许我们创建一个引用 s1 值但不拥有它的引用。因为它不拥有它,所以当引用停止使用时,它指向的值不会被丢弃。

同样,函数的签名使用 & 来指示参数 s 的类型是引用。让我们添加一些解释性注释

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
  // it refers to, it is not dropped.

变量 s 的作用域与任何函数参数的作用域相同,但是当 s 停止使用时,引用指向的值不会被丢弃,因为 s 没有所有权。当函数将引用作为参数而不是实际值时,我们不需要返回值来归还所有权,因为我们从未拥有过所有权。

我们将创建引用的操作称为借用。就像在现实生活中一样,如果一个人拥有某样东西,您可以从他们那里借用它。当你用完后,你必须归还它。你并不拥有它。

那么,如果我们尝试修改我们正在借用的东西会发生什么?尝试清单 4-6 中的代码。剧透警告:它不起作用!

文件名:src/main.rs

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

清单 4-6:尝试修改借用的值

错误如下

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
7 | fn change(some_string: &mut String) {
  |                         +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

正如变量默认是不可变的一样,引用也是不可变的。我们不允许修改我们引用的东西。

可变引用

我们可以修复清单 4-6 中的代码,以便我们修改借用的值,只需进行一些小的调整,使用可变引用

文件名:src/main.rs

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

首先,我们将 s 更改为 mut。然后,我们在调用 change 函数的地方使用 &mut s 创建一个可变引用,并更新函数签名以使用 some_string: &mut String 接受可变引用。这非常清楚地表明 change 函数将改变它借用的值。

可变引用有一个很大的限制:如果您有一个对值的 mutable 引用,则不能再拥有对该值的任何其他引用。这段代码尝试创建对 s 的两个可变引用将会失败

文件名:src/main.rs

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);
}

错误如下

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

此错误表明此代码无效,因为我们不能一次多次以可变方式借用 s。第一个可变借用在 r1 中,并且必须持续到它在 println! 中使用为止,但是在创建该可变引用及其使用之间,我们尝试在 r2 中创建另一个可变引用,该引用借用了与 r1 相同的数据。

防止同时对同一数据进行多个可变引用的限制允许进行突变,但方式非常可控。这是 Rust 新手难以理解的地方,因为大多数语言都允许您在任何时候进行突变。拥有此限制的好处是 Rust 可以在编译时防止数据竞争。数据竞争 类似于竞争条件,并且在发生以下三种行为时发生

  • 两个或多个指针同时访问相同的数据。
  • 至少有一个指针被用于写入数据。
  • 没有使用任何机制来同步对数据的访问。

数据竞争会导致未定义的行为,并且当您试图在运行时跟踪它们时,可能难以诊断和修复;Rust 通过拒绝编译具有数据竞争的代码来防止此问题!

与往常一样,我们可以使用花括号创建一个新的作用域,允许多个可变引用,但不是同时

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.

    let r2 = &mut s;
}

Rust 在组合可变引用和不可变引用时强制执行类似的规则。以下代码会导致错误:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);
}

错误如下

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

没错!当我们拥有对同一个值的不可变引用时,我们不能拥有可变引用。

不可变引用的用户不希望值在其下方突然发生变化!但是,允许多个不可变引用,因为任何只读取数据的人都无法影响其他任何人读取数据。

请注意,引用的作用域从其引入的位置开始,并持续到最后一次使用该引用为止。例如,以下代码将编译,因为不可变引用的最后一次使用(即 println!)发生在引入可变引用之前

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{r1} and {r2}");
    // variables r1 and r2 will not be used after this point

    let r3 = &mut s; // no problem
    println!("{r3}");
}

不可变引用 r1r2 的作用域在最后一次使用它们的 println! 之后结束,该位置在创建可变引用 r3 之前。这些作用域不重叠,因此允许使用此代码:编译器可以判断在作用域结束之前的某个时间点不再使用该引用。

即使借用错误有时可能会令人沮丧,但请记住,这是 Rust 编译器在早期(在编译时而不是在运行时)指出潜在错误,并准确地向您显示问题所在。这样,您就不必费力地找出为什么您的数据与您预期的不符。

悬空引用

在使用指针的语言中,很容易错误地创建一个悬空指针,该指针指向内存中可能已分配给其他人的位置,方法是在保留指向该内存的指针的同时释放该内存。相反,在 Rust 中,编译器保证引用永远不会是悬空引用:如果您有对某些数据的引用,则编译器将确保在对数据的引用消失之前,数据不会超出作用域。

让我们尝试创建一个悬空引用,看看 Rust 如何通过编译时错误来防止它们

文件名:src/main.rs

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

错误如下

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
5 | fn dangle() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

此错误消息指的是我们尚未介绍的功能:生命周期。我们将在第 10 章中详细讨论生命周期。但是,如果您忽略有关生命周期的部分,则该消息确实包含了为什么此代码存在问题的关键

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from

让我们仔细看看我们的 dangle 代码在每个阶段到底发生了什么

文件名:src/main.rs

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger!

因为 s 是在 dangle 内部创建的,所以当 dangle 的代码执行完毕后,s 将被释放。但是我们试图返回对它的引用。这意味着此引用将指向无效的 String。这可不行!Rust 不会让我们这样做。

这里的解决方案是直接返回 String

fn main() {
    let string = no_dangle();
}

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

这可以毫无问题地工作。所有权被移出,并且没有任何内容被释放。

引用的规则

让我们回顾一下我们讨论过的有关引用的内容

  • 在任何给定时间,您都可以拥有一个可变引用任意数量的不可变引用。
  • 引用必须始终有效。

接下来,我们将介绍另一种引用:切片。