Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

引用与借用

代码清单 4-5 中使用元组的做法有个问题:由于 String 被移动到了 calculate_length,为了在调用该函数后继续使用 String,我们不得不将它返回给调用函数。作为替代,我们可以传递 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-6 展示了这个概念。

三张表:s 的表中只有一个指向 s1 表的指针。s1 的表包含其栈数据,并指向堆上的字符串数据。

图 4-6:&String 类型的 s 指向 String 类型的 s1

注意:使用 & 创建引用的相反操作是解引用,通过解引用运算符 * 实现。我们会在第 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 s does not have ownership of what
  // it refers to, the String 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 it 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,让我们能够修改借用的值:

Filename: 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 改为可变变量。然后,在调用 change 函数时使用 &mut s 创建可变引用,并更新函数签名,通过 some_string: &mut String 接收可变引用。这样就明确表示 change 函数会修改它借用的值。

可变引用有一条重要限制:如果你对某个值有一个可变引用,就不能再拥有该值的其他引用。下面这段试图为 s 创建两个可变引用的代码无法通过编译:

Filename: 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!("{r1}, {r2}, and {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!("{r1}, {r2}, and {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}");
}

不可变引用 r1 和 r2 的作用域会在它们最后一次被使用的 println! 之后结束,而这早于可变引用 r3 的创建。它们的作用域并不重叠,因此这段代码是允许的:编译器能够判断在作用域结束之前,这些引用已经不再使用。

借用错误有时确实令人沮丧,但请记住,这是 Rust 编译器在及早指出潜在 bug(在编译时,而不是运行时),并准确告诉你问题所在。这样你就不必再追查数据为何与预期不符。

悬垂引用

在使用指针的语言中,如果释放了一块内存,却保留指向该内存的指针,就很容易错误地创建悬垂指针,也就是指向可能已经被交给其他用途的内存位置的指针。相比之下,在 Rust 中,编译器会保证引用永远不会变成悬垂引用:如果你持有某些数据的引用,编译器会确保数据的作用域不会早于该引用结束。

让我们试着创建悬垂引用,看看 Rust 如何通过编译错误来阻止它:

Filename: 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 代码在每个阶段具体发生了什么:

Filename: 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, so 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
}

这样就没有问题了。所有权被移出函数,没有任何内容被提前释放。

引用规则

让我们回顾一下关于引用的讨论:

  • 任意时刻,你要么拥有一个可变引用,要么拥有任意数量的不可变引用。
  • 引用必须始终有效。

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