简介
欢迎来到「引用与借用」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,我们将学习如何在 Rust 中使用引用借用值,而不是获取所有权,这样我们就可以传递和操作数据,而无需将所有权返回给调用函数。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
欢迎来到「引用与借用」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,我们将学习如何在 Rust 中使用引用借用值,而不是获取所有权,这样我们就可以传递和操作数据,而无需将所有权返回给调用函数。
清单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展示了这个概念。
图4-5:&String s
指向 String s1
的示意图
注意:使用
&
进行引用的相反操作是解引用,它是通过解引用运算符*
来完成的。我们将在第8章中看到解引用运算符的一些用法,并在第15章中讨论解引用的细节。
让我们仔细看看这里的函数调用:
let s1 = String::from("hello");
let len = calculate_length(&s1);
&s1
语法让我们创建一个引用,该引用指向 s1
的值,但不拥有它。因为它不拥有该值,所以当引用不再使用时,它所指向的值不会被释放。
同样,函数的签名使用 &
来表明参数 s
的类型是一个引用。让我们添加一些解释性注释:
fn calculate_length(s: &String) -> usize { // s是对String的引用
s.len()
} // 这里,s超出了作用域。但因为它不拥有它所指向的内容,
// 所以String不会被释放
变量 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:尝试修改借用的值
这里是错误信息:
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&`
reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable
reference: `&mut String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so
the data it refers to cannot be borrowed as mutable
就像变量默认是不可变的一样,引用也是如此。我们不允许修改我们拥有引用的东西。
我们可以通过一些小调整来修复清单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
改为可变的。然后,在调用 change
函数时,我们使用 &mut s
创建一个可变引用,并更新函数签名以接受可变引用 some_string: &mut String
。这使得 change
函数会修改它所借用的值这一点非常明确。
可变引用有一个很大的限制:如果你有一个指向某个值的可变引用,那么你就不能有其他指向该值的引用。这段试图为 s
创建两个可变引用的代码将会失败:
文件名:src/main.rs
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
这里是错误信息:
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
这个错误表明这段代码是无效的,因为我们不能一次对 s
进行多次可变借用。第一个可变借用在 r1
中,并且必须持续到它在 println!
中被使用,但在创建那个可变引用和使用它之间,我们试图在 r2
中创建另一个可变引用,它借用了与 r1
相同的数据。
防止同时对同一数据进行多个可变引用的限制允许进行可变操作,但方式非常可控。这是新的Rust使用者会遇到困难的地方,因为大多数语言允许你随时进行可变操作。有这个限制的好处是Rust可以在编译时防止数据竞争。数据竞争类似于竞态条件,当出现以下三种行为时就会发生:
数据竞争会导致未定义行为,并且在运行时试图追踪它们时可能很难诊断和修复;Rust通过拒绝编译存在数据竞争的代码来防止这个问题!
一如既往,我们可以使用花括号创建一个新的作用域,允许有多个可变引用,但不是同时的:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // 这里r1超出了作用域,所以我们可以毫无问题地创建一个新的引用
let r2 = &mut s;
Rust对可变引用和不可变引用的组合也实施了类似的规则。这段代码会导致错误:
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
println!("{r1}, {r2}, and {r3}");
这里是错误信息:
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
哎呀!当我们对同一值有一个不可变引用时,我们也不能有一个可变引用。
不可变引用的使用者并不期望值会突然在他们不知情的情况下发生变化!然而,允许多个不可变引用,因为只是读取数据的人没有能力影响其他人对数据的读取。
请注意,引用的作用域从它被引入的地方开始,并持续到该引用最后一次被使用。例如,这段代码会编译通过,因为不可变引用的最后一次使用,即 println!
,发生在可变引用被引入之前:
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{r1} and {r2}");
// 变量r1和r2在此之后不会再被使用
let r3 = &mut s; // 没问题
println!("{r3}");
不可变引用 r1
和 r2
的作用域在它们最后一次被使用的 println!
之后结束,这是在创建可变引用 r3
之前。这些作用域不重叠,所以这段代码是允许的:编译器可以在作用域结束之前的某个点判断出该引用不再被使用。
尽管有时借用错误可能会令人沮丧,但请记住,是Rust编译器在早期(在编译时而非运行时)指出了一个潜在的错误,并准确地告诉你问题出在哪里。这样你就不必去追查为什么你的数据不是你所期望的那样。
在有指针的语言中,很容易错误地创建一个悬垂指针(dangling pointer)——一个指向可能已被分配给其他人的内存位置的指针,方法是在保留指向该内存的指针的同时释放一些内存。相比之下,在 Rust 中,编译器会确保引用永远不会是悬垂引用:如果你有一个指向某些数据的引用,编译器会确保在对该数据的引用超出作用域之前,该数据不会超出作用域。
让我们尝试创建一个悬垂引用,看看 Rust 如何通过编译时错误来防止它们:
文件名:src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
这里是错误信息:
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 {
| ~~~~~~~~
这个错误信息提到了一个我们还没有涉及的特性:生命周期。我们将在第 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 dangle() -> &String { // dangle 返回一个指向 String 的引用
let s = String::from("hello"); // s 是一个新的 String
&s // 我们返回对 String,即 s 的引用
} // 在这里,s 超出作用域并被释放,所以它的内存也消失了
// 危险!
因为 s
是在 dangle
内部创建的,当 dangle
的代码结束时,s
将被释放。但我们试图返回一个指向它的引用。这意味着这个引用将指向一个无效的 String
。这可不行!Rust 不会让我们这样做。
这里的解决方案是直接返回 String
:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
这样就可以正常工作,不会有任何问题。所有权被转移出去,并且没有任何东西被释放。
让我们回顾一下我们讨论过的关于引用的内容:
接下来,我们将看看另一种引用:切片。
恭喜你!你已经完成了“引用与借用”实验。你可以在LabEx中练习更多实验来提升你的技能。