【Rust学习】05_引用和借用

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

前言

在这章我们将开始学习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)。

目录
相关文章
|
10天前
|
Rust Linux Go
Rust/Go语言学习
Rust/Go语言学习
|
1月前
|
存储 Rust 安全
【Rust学习】06_切片
所有权、借用和切片的概念确保了 Rust 程序在编译时的内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。
18 1
|
2月前
|
存储 Rust 安全
【Rust学习】04_所有权
所有权是 Rust 最独特的特性,对语言的其余部分有着深远的影响。它使 Rust 能够在不需要垃圾收集器的情况下保证内存安全,因此了解所有权的运作方式非常重要。在本章中,我们将讨论所有权以及几个相关功能:借用、切片以及 Rust 如何在内存中布局数据。
17 1
|
2月前
|
存储 Rust 编译器
【Rust学习】03_通用编程概念
您成功了!这是一个相当大的章节:您了解了变量、标量和复合数据类型、函数、注释、 if 表达式和循环!若要练习本章中讨论的概念。
26 2
|
3月前
|
Rust Linux iOS开发
【Rust学习】01_入门准备
让我们开始您的 Rust 之旅吧!有很多东西要学,但每一段旅程都是从第一步开始的,在本章中,我们将一起来学习以下知识点: - 在 Linux、macOS 和 Windows 上安装 Rust - 编写打印程序 Hello, world! - 使用 cargo Rust 的包管理器和构建系统
68 1
|
3月前
|
存储 移动开发 Rust
【Rust学习】02_猜谜游戏
让我们一起动手完成一个项目,来快速上手 Rust!本章将介绍 Rust 中一些常用概念,并向您展示如何在实际项目中运用它们。您将会学到 let、match、方法、关联函数、引用外部 crate 等知识!后续章节会深入探讨这些概念的细节。
28 0
|
4月前
|
JSON Rust IDE
全网最全的Rust学习资源
学习Rust过程中整理了一些学习资料分享一下。
215 1
|
4月前
|
Rust
Rust编程语言:探索性学习与实践指南
Rust编程语言:探索性学习与实践指南
51 0
|
4月前
|
Rust 编译器
【一起学Rust】Rust学习前准备——注释和格式化输出
【一起学Rust】Rust学习前准备——注释和格式化输出
55 0
|
9月前
|
Rust
Rust编程语言:探索性学习与实践指南
Rust编程语言:探索性学习与实践指南
46 0