引用与借用

References and Borrowing

清单 4-5 中,元组代码的问题在于,我们必须将那个 String 返回给调用函数,以便咱们在调用 calculate_length 后,仍能使用这个 String,因为这个 String 已被迁移到 calculate_length 中。相反,我们可以提供一个到该 String 值的引用。引用,reference 与指针类似,其是个我们可以沿着他,访问存储在其中数据的地址;其中的数据为其他变量所有。与指针不同的是,可以保证某个引用在其生命周期内,始终指向某个特定类型的有效值。

下面是如何定义和使用,以对象引用作为参数,而非取得值所有权的 calculate_length 函数:

文件名:src/main.rs

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

    let length = calculate_length(&s1);

    println! ("字符串 '{}' 的长度为:{}", s1, length);
}

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

首先,请注意变量声明和函数返回值中的所有元组代码都不见了。其次,请注意我们将 &s1 传入到 calculate_length,且在其定义中,我们使用了 &String 而不是 String。这些 & 符合表示了 引用,而他们允许咱们,在取得某个值所有权的情况下,对其进行引用。下图 4-5 描述了这一概念。

指向 String s1 的 &String s 图示

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

注意:与使用 & 进行引用相反的是,解引用,dereferencing,他是通过解引用操作符 * 实现的。我们将在第 8 章,看到解引用操作符的一些用法,并在第 15 章讨论解引用的细节。

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

#![allow(unused)]
fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);
}

通过 &s1 这种语法,我们可以创建出一个指向 s1 值,但不拥有他的引用。由于这个引用不拥有 s1,因此其停止使用时,他所指向的值,不会被丢弃。

同样,那个函数的签名,使用了 & 来表明参数 s 的类型是个引用。我们来添加一些解释性注释:

#![allow(unused)]
fn main() {
fn calculate_length(s: &String) -> usize {  // s 是个到某 String 的引用
    s.len()
}   // 这里,s 会超出作用域。但由于他没有其指向值的所有权,因此该
    // 值不会被丢弃。
}

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

我们把这种创建出某个引用的行为,称为 借用,borrowing。这如同现实生活中,如果某人拥有某样东西,咱们可以向其借用这件东西。用毕时,咱们必须归还。咱们并不拥有这件东西。

那么,如果我们试图修改借来的东西,会发生什么情况呢?请尝试下面清单 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_demo v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\ownership_demo)
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_demo` (bin "ownership_demo") due to previous error

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

可变引用

Mutable References

只需稍作调整,使用 可变引用,mutable reference,咱们即可修改某个借用值,便可修正清单 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 函数将改变其所借用的值。

可变引用有个很大的限制:如果咱们有了到某个值的一个可变引用,就不能对该值有其他引用。下面这段试图创建两个到 s 可变引用的代码,就会失败:

文件名:src/main.rs

#![allow(unused)]
fn main() {
    let mut s = String::from("hello");

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

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

下面是报错信息:

$ cargo run
   Compiling ownership_demo v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\ownership_demo)
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_demo` (bin "ownership_demo") due to previous error

这个报错讲到,因为我们不能将 s 作为可变引用,同一实际借用多次,因此这段代码无效。第一次可变借用是在 r1 中,而必须持续到其在 println! 中被使用为止,但在这个可变引用的创建和使用之间,我们试图在 r2 中,创建另一个借用了与 r1 同一数据的可变引用。

防止在同一时间,对同一数据进行多个可变引用的这种限制,允许改变,但改变是在非常受控的方式下进行的。这也是 Rust 新手比较头疼的问题,因为大多数语言,都允许咱们随时改变。有着这种限制的好处是,Rust 可以在编译时,防止数据竞赛。数据竞赛,data race 类似于某种竞赛情形,会在下面这三种行为发生时出现:

  • 两个以上的指针同时访问某同一数据;

  • 至少有一个指针被用来写该数据;

  • 没有同步访问该数据的机制。

数据竞赛会导致未定义的行为,当咱们试图在运行时跟踪他们时,会很难诊断和修复;Rust 通过拒绝编译带有数据竞赛的代码,从而避免了这个问题!

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

#![allow(unused)]
fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    }   // r1 在这里超出了作用域,因此我们可以毫无问题地构造一个新的引用。

    let r2 = &mut s;
}

Rust 对组合可变引用和不可变引用,也执行类似的规则。下面这段代码会导致一个报错:

#![allow(unused)]
fn main() {
    let mut s = String::from("hello");

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

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

下面是那个报错:

$ cargo run
   Compiling ownership_demo v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\ownership_demo)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src\main.rs:6:14
  |
4 |     let r1 = &s;
  |              -- immutable borrow occurs here
5 |     let r2 = &s;
6 |     let r3 = &mut s;
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println! ("{}, {}, {}", r1, r2, r3);
  |                             -- immutable borrow later used here

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

呼!我们 不能在有着到某个值的不可变引用的同时,有着一个到这同一值的可变引用。

某个不可变引用的使用者,不期望该值会突然在他们眼皮底下改变!不过,多个不可变引用是允许的,因为仅在读取数据的使用者,并无影响其他读取该数据使用者的能力。

请注意,引用的作用域从该引用被引入的地方开始,并持续到该引用被最后一次使用处为止。例如,下面这段代码可以编译,因为那个不可变引用的最后一次使用(println!),发生在那个可变引用被引入之前:

#![allow(unused)]
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;    // 没有问题
    let r2 = &s;    // 没有问题
    println! ("r1 与 r2: {}, {}", r1, r2);
    // 变量 r1 与 r2 在此处之后将不会使用了

    let r3 = &mut s;    // 这就没问题了
    println! ("r3: {}", r3);
}

不可变引用 r1r2 的作用域,在他们最后一次被使用的 println! 之后,可变引用 r3 被创建之前结束。这些作用域不会重叠,因此这段代码会被放行:编译器可以区分出,在作用域结束前的某个点,该引用不再被使用。

译注:由于引用属于大小已知、固定不变的类型,因此他们是保存在栈上的,带有 Copy 特质,故上面的代码中,在 let r3 = &mut s; 语句之前,可以无限次使用 r1r2 这两个不可变引用,他们不会被迁移到 println! 宏及其他函数中。

尽管借用方面的报错有时会令人沮丧,但请记住,这是 Rust 编译器在早期(编译时而不是运行时)就指出某个潜在错误,并准确地告诉咱们问题所在。这样,咱们就不必再追踪,为什么咱们的数据和咱们设想的不一样了。

悬空引用

Dangling References

在带有指针的语言中,就很容易错误地创建出 悬空指针,dangling pointer,即在保留了指向某处内存指针的同时,释放了该处内存,从而造成引用了内存中,可能已经给了其他代码的某个位置的指针。相比之下,在 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_demo v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\ownership_demo)
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
  |
5 | fn dangle() -> &'static String {
  |                 +++++++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership_demo` (bin "ownership_demo") due to 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

#![allow(unused)]
fn main() {
fn dangle() -> &String {    // dangle 返回的是个到某 String 的引用
    let s = String::from("hello");  // s 是个新的 String

    &s  // 咱们返回了一个指向那个 String,s 的引用
}   // 这里,s 超出了作用域,进而被丢弃。他的内存就没了。
    // 危险所在!
}

因为 s 是在 dangle 内部创建的,所以当 dangle 的代码结束时,s 将被解除内存分配。然而我们曾试图返回对他的引用。这意味着这个引用,将指向一个无效的 String。这可不行!Rust 不允许我们这么做。

这里的解决办法,是直接返回那个 String 值:

#![allow(unused)]
fn main() {
fn dangle() -> String {
    let s = String::from("hello");

    s
}
}

这会没有任何问题地运作。所有权会被迁出,而不会有任何东西,被解除内存分配。

引用的规则

The Rules of References

我们来回顾一下,我们已讨论过的关于引用的内容:

  • 在任何时候,咱们都可以有着一个可变引用,或任意数量的不可变引用;

  • 引用必须始终有效。

接下来,我们来看看另一种引用:切片。

Last change: 2023-12-13, commit: c0d1fe7