引用与借用
清单 4-5 中元组代码的问题在于,我们必须将` String
`返回给调用函数,以便在调用` calculate_length
`之后仍能使用` String
`,因为` String
`已被移入` calculate_length
`。相反,我们可以提供对` String
`值的引用。引用类似于指针,它是一个地址,我们可以通过该地址访问存储在该地址的数据;这些数据由其他变量拥有。与指针不同,引用在其生命周期内保证指向特定类型的有效值。
以下是如何定义和使用一个以对象引用作为参数而不是获取值所有权的 calculate_length
函数:
首先,注意到变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们将` &s1
`传递给` calculate_length
`,并且在它的定义中,我们取` &String
`而不是` String
`。这些&符号代表引用,它们允许你引用某个值而不获取其所有权。图 4-6 描绘了这一概念。
注意:与使用 &
进行引用相反的是解引用,这是通过解引用操作符 *
实现的。我们将在第 8 章中看到解引用操作符的一些用法,并在第 15 章中讨论解引用的细节。
让我们仔细看看这里的函数调用:
&s1
语法允许我们创建一个引用,该引用指向 s1
的值但不拥有它。由于它不拥有该值,当引用不再使用时,它所指向的值不会被丢弃。
同样,函数的签名使用 &
来表示参数 s
的类型是一个引用。让我们添加一些解释性注释:
变量 ` s
` 的有效范围与任何函数参数的范围相同,但当 ` s
` 停止使用时,引用所指向的值不会被丢弃,因为 ` s
` 并不拥有所有权。当函数使用引用作为参数而不是实际值时,我们不需要返回这些值以归还所有权,因为我们从未拥有过所有权。
我们将创建引用的行为称为借用。就像现实生活中一样,如果某人拥有某物,你可以从他们那里借用。当你用完后,必须归还。你并不拥有它。
那么,如果我们尝试修改借用的内容会发生什么呢?试试清单 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 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` (bin "ownership") due to 1 previous error
正如变量默认是不可变的,引用也是如此。我们不允许修改引用的内容。
可变引用
我们可以通过一些小的调整来修复清单 4-6 中的代码,允许我们修改借用的值,具体方法是使用可变引用:
首先我们将 s
更改为 mut
。然后我们使用 &mut s
创建一个可变引用,在其中调用 change
函数,并将函数签名更新为接受带有 some_string: &mut String
的可变引用。这非常清楚地表明 change
函数将改变它所借用的值。
可变引用有一个很大的限制:如果你有一个值的可变引用,你就不能有其他对该值的引用。这段尝试为 s
创建两个可变引用的代码将会失败:
错误如下:
$ 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
相同的数据。
防止同时对同一数据进行多个可变引用的限制允许进行可变操作,但以非常受控的方式进行。这是新 Rustacean 们常常感到困惑的地方,因为大多数语言允许你随时进行可变操作。这种限制的好处是,Rust 可以在编译时防止数据竞争。数据竞争类似于竞态条件,当以下三种行为发生时就会发生:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针正用于写入数据。
- 没有使用任何机制来同步对数据的访问。
数据竞争会导致未定义行为,并且在运行时尝试追踪它们时可能难以诊断和修复;Rust 通过拒绝编译存在数据竞争的代码来防止这一问题!
一如既往,我们可以使用花括号来创建一个新的作用域,从而允许多个可变引用,只是不能同时存在:
Rust 对可变和不可变引用的组合执行了类似的规则。此代码会导致错误:
错误如下:
$ 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!("{}, {}, and {}", r1, r2, 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!
`)发生在可变引用引入之前:
不可变引用 r1
和 r2
的作用域在它们最后一次使用的 println!
之后结束,这是在可变引用 r3
创建之前。这些作用域没有重叠,因此这段代码是允许的:编译器可以判断出在作用域结束之前,引用不再被使用。
尽管借用错误有时可能令人沮丧,但请记住,这是 Rust 编译器在早期(编译时而非运行时)指出潜在错误,并准确显示问题所在。这样,你就不必费心去追踪数据为何与预期不符了。
悬空引用
在拥有指针的语言中,很容易错误地创建一个悬空指针——即指向可能已被分配给其他人的内存位置的指针——通过释放某些内存同时保留指向该内存的指针。相比之下,在 Rust 中,编译器保证引用永远不会成为悬空引用:如果你有对某些数据的引用,编译器将确保数据在引用之前不会超出作用域。
让我们尝试创建一个悬空引用,看看 Rust 如何通过编译时错误来防止它们:
错误如下:
$ 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 {
|
error[E0515]: cannot return reference to local variable `s`
--> src/main.rs:8:5
|
8 | &s
| ^^ returns a reference to data owned by the current function
Some errors have detailed explanations: E0106, E0515.
For more information about an error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 2 previous errors
此错误信息涉及我们尚未介绍的一个特性:生命周期。我们将在第 10 章详细讨论生命周期。但是,如果你忽略关于生命周期的部分,这条信息确实包含了为什么这段代码有问题的关键:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
让我们仔细看看在 dangle
代码的每个阶段究竟发生了什么:
因为 s
是在 dangle
内部创建的,当 dangle
的代码执行完毕时, s
将被释放。但我们试图返回一个对它的引用。这意味着这个引用将指向一个无效的 String
。这不行!Rust 不会允许我们这样做。
这里的解决方案是直接返回 String
:
这没有任何问题。所有权被移出,且没有任何内容被释放。
引用规则
让我们回顾一下我们讨论过的关于引用的内容:
- 在任何给定时间,你可以拥有一个可变引用或任意数量的不可变引用。
- 引用必须始终有效。
接下来,我们将探讨另一种引用类型:切片。