【Rust学习】04_2引用和借用

前言

在这章我们将开始学习Rust的引用和借用,它们是Rust中重要的概念,它们允许我们创建可变引用,以及创建不可变引用。

内容

引用和借用

在下面的示例中,我们必须将 String 返回给调用函数,以便在调用 calculate_length 后仍能使用 String,因为 String 被移动到了 calculate_length 内。

下面是如何定义并使用一个(新的)calculate_length 函数,它以一个对象的引用作为参数而不是获取值的所有权:

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

    let len = calculate_length(&s1);

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

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

首先,请注意变量声明和函数返回值中的所有元组代码都消失了。注意我们传递 &s1 给 calculate_length,同时在函数定义中,我们获取 &String 而不是 String。
这些 & 符号表示引用,它们允许您引用某个值,而无需获得其所有权。

注意:与使用 & 引用相反的操作是 解引用(dereferencing),它使用解引用运算符,*。

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

let s1 = String::from("hello");

let len = calculate_length(&s1);

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

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

fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
    s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
  // 所以什么也不会发生

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

我们将创建一个引用的行为称为 借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。

如果我们尝试修改借来的变量,会发生什么呢?

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

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

现在我们来运行一下,这时候我们会得到一个错误:

error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
  --> src/main.rs:19:5
   |
19 |     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
   |
18 | fn change(some_string: &mut String) {
   |                         +++

正如变量默认情况下是不可变的一样,引用也是不可变的。不允许修改我们引用的内容。

可变引用

们只需进行一些小的调整来修改借来的值,这些调整使用可变引用:

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 函数将改变它所借用的值。

不过可变引用有一个很大的限制:在同一时间,只能有一个对某一特定数据的可变引用。尝试创建两个可变引用的代码将会失败:

 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);

错误如下:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:37:14
   |
35 |     let r1 = &s; // no problem
   |              -- immutable borrow occurs here
36 |     let r2 = &s; // no problem
37 |     let r3 = &mut s; // BIG PROBLEM
   |              ^^^^^^ mutable borrow occurs here
38 |
39 |     println!("{}, {}, and {}", r1, r2, r3);
   |                                -- immutable borrow later used here

我们不能有一个可变的引用,因为我们有一个不可变的引用指向相同的值。

不可变引用的用户不希望值突然从他们下面改变出来!但是,允许多个不可变引用,因为任何只读取数据的人都无法影响其他任何人对数据的读取。

请注意,引用的范围从引入它的地方开始,一直持续到最后一次使用该引用。例如,此代码将进行编译,因为不可变引用的最后一次使用(println!)发生在引入可变引用之前:

    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 编译器尽早指出潜在的错误(在编译时而不是在运行时),并准确地告诉你问题出在哪里。这样,你就不必追踪为什么你的数据不是你想象的那样。

悬垂引用

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

让我们尝试创建一个悬垂引用,Rust 会通过一个编译时错误来避免:

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

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

    &s
}

错误如下:

error[E0106]: missing lifetime specifier
  --> src/main.rs:60:16
   |
60 | 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`
   |
60 | fn dangle() -> &'static String {
   |                 +++++++
help: instead, you are more likely to want to return an owned value
   |
60 - fn dangle() -> &String {
60 + fn dangle() -> String {
   |

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

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

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

fn dangle() -> &String { // dangle 返回一个字符串的引用

    let s = String::from("hello"); // s 是一个新的字符串

    &s // 返回字符串s的引用
} // 这时候 s 的作用域结束,s 被丢弃,s 的内存被释放。
  // Danger!

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

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

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

这样就没有任何错误了。所有权被移动出去,所以没有值被释放。

引用的规则

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

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效的。

接下来,我们将查看一种不同类型的引用:切片(slices)。

posted @ 2024-07-29 20:45:00 王洋 阅读(138) 评论(0)
发表评论
昵称
邮箱
网址