引用与借用
清单 4-5 中元组代码的问题在于,我们需要将 String
返回给调用函数,以便在调用 calculate_length
后仍然可以使用 String
,因为 String
已被移动到 calculate_length
中。相反,我们可以提供对 String
值的引用。引用 类似于指针,它是一个地址,我们可以通过该地址访问存储在该地址的数据;该数据由其他变量拥有。与指针不同,引用保证在引用的生命周期内指向特定类型的有效值。
下面是如何定义和使用一个 calculate_length
函数的示例,该函数具有对对象的引用作为参数,而不是获取该值的所有权
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-6 描绘了这个概念。
图 4-6:&String s
指向 String s1
的图示
注意:通过使用 &
进行引用的反面是解引用,它通过解引用运算符 *
完成。我们将在第 8 章中看到解引用运算符的一些用法,并在第 15 章中讨论解引用的细节。
让我们仔细看看这里的函数调用
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
语法让我们创建一个引用 s1
值但不拥有它的引用。因为它不拥有它,所以当引用停止使用时,它指向的值不会被删除。
同样,函数的签名使用 &
来表示参数 s
的类型是引用。让我们添加一些解释性注释
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 is a reference to a String s.len() } // Here, s goes out of scope. But because it does not have ownership of what // it refers to, it is not dropped.
变量 s
有效的作用域与任何函数参数的作用域相同,但是当 s
停止使用时,引用指向的值不会被删除,因为 s
没有所有权。当函数使用引用作为参数而不是实际值时,我们不需要为了返回所有权而返回这些值,因为我们从未拥有过所有权。
我们将创建引用的行为称为借用。就像在现实生活中一样,如果一个人拥有某物,您可以从他们那里借用它。当您完成时,您必须将其归还。您不拥有它。
那么,如果我们尝试修改我们借用的东西会发生什么?尝试清单 4-6 中的代码。剧透警告:它不起作用!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
这是错误信息
$ 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 中的代码,以允许我们修改借用的值,这些调整改为使用可变引用
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
的可变引用的此代码将失败
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
这是错误信息
$ 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
相同数据的可变引用。
防止同时对同一数据进行多个可变引用的限制允许进行更改,但以非常受控的方式进行。这是新 Rustaceans 难以理解的问题,因为大多数语言允许您随时进行更改。拥有此限制的好处是,Rust 可以在编译时防止数据竞争。数据竞争 与竞争条件类似,并且在发生以下三个行为时发生
- 两个或多个指针同时访问相同的数据。
- 至少有一个指针被用来写入数据。
- 没有使用任何机制来同步对数据的访问。
数据竞争会导致未定义的行为,并且当您尝试在运行时跟踪它们时,很难诊断和修复;Rust 通过拒绝编译具有数据竞争的代码来防止此问题!
与往常一样,我们可以使用花括号创建一个新作用域,从而允许多个可变引用,但不能是同时的
fn main() { let mut s = String::from("hello"); { let r1 = &mut s; } // r1 goes out of scope here, so we can make a new reference with no problems. let r2 = &mut s; }
Rust 对组合可变和不可变引用强制执行类似的规则。此代码会导致错误
fn main() {
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);
}
这是错误信息
$ 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!
,发生在引入可变引用之前
fn main() { 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 编译器在早期(在编译时而不是在运行时)指出潜在的错误,并准确地向您显示问题所在。然后,您不必跟踪为什么您的数据不是您认为的那样。
悬垂引用
在具有指针的语言中,通过释放某些内存,同时保留指向该内存的指针,很容易错误地创建悬垂指针——指向内存中可能已分配给其他人的位置的指针。相反,在 Rust 中,编译器保证引用永远不会成为悬垂引用:如果您引用某些数据,则编译器将确保该数据不会在数据引用之前超出作用域。
让我们尝试创建一个悬垂引用,看看 Rust 如何使用编译时错误来阻止它们
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
这是错误信息
$ 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
代码的每个阶段都发生了什么
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
// Danger!
由于 s
是在 dangle
内部创建的,因此当 dangle
的代码完成时,s
将被释放。但是我们尝试返回对它的引用。这意味着此引用将指向无效的 String
。这不好!Rust 不会让我们这样做。
此处的解决方案是直接返回 String
fn main() { let string = no_dangle(); } fn no_dangle() -> String { let s = String::from("hello"); s }
这没有任何问题地工作。所有权被移出,并且没有任何内容被释放。
引用的规则
让我们回顾一下我们讨论的有关引用的内容
- 在任何给定时间,您可以有一个可变引用或任意数量的不可变引用。
- 引用必须始终有效。
接下来,我们将看看另一种引用:切片。