引用与借用
References and Borrowing
清单 4-5 中,元组代码的问题在于,我们必须将那个 String
返回给调用函数,以便咱们在调用 calculate_length
后,仍能使用这个 String
,因为这个 String
已被迁移到 calculate_length
中。相反,我们可以提供一个到该 String
值的引用。引用,reference 与指针类似,其是个我们可以沿着他,访问存储在其中数据的地址;其中的数据为其他变量所有。与指针不同的是,可以保证某个引用在其生命周期内,始终指向某个特定类型的有效值。
下面是如何定义和使用,以对象引用作为参数,而非取得值所有权的 calculate_length
函数:
文件名:src/main.rs
fn main() { let s1 = String::from("hello"); let length = calculate_length(&s1); println! ("字符串 '{}' 的长度为:{}", s1, length); } fn calculate_length(s: &String) -> usize { s.len() }
首先,请注意变量声明和函数返回值中的所有元组代码都不见了。其次,请注意我们将 &s1
传入到 calculate_length
,且在其定义中,我们使用了 &String
而不是 String
。这些 &
符合表示了 引用,而他们允许咱们,在取得某个值所有权的情况下,对其进行引用。下图 4-5 描述了这一概念。
图 4-5:指向 String s1
的 &String s
图示
注意:与使用
&
进行引用相反的是,解引用,dereferencing,他是通过解引用操作符*
实现的。我们将在第 8 章,看到解引用操作符的一些用法,并在第 15 章讨论解引用的细节。
我们来仔细看看这里的函数调用:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); }
通过 &s1
这种语法,我们可以创建出一个指向 s1
值,但不拥有他的引用。由于这个引用不拥有 s1
,因此其停止使用时,他所指向的值,不会被丢弃。
同样,那个函数的签名,使用了 &
来表明参数 s
的类型是个引用。我们来添加一些解释性注释:
#![allow(unused)] fn main() { fn calculate_length(s: &String) -> usize { // s 是个到某 String 的引用 s.len() } // 这里,s 会超出作用域。但由于他没有其指向值的所有权,因此该 // 值不会被丢弃。 }
变量 s
有效的作用域,与任何的函数参数作用域相同,但当 s
停止使用时,该引用所指向的值并不会丢弃,因为 s
没有所有权。当函数将引用而非实际值作为参数时,我们就不再需要返回值,来归还所有权,因为我们从未拥有过所有权。
我们把这种创建出某个引用的行为,称为 借用,borrowing。这如同现实生活中,如果某人拥有某样东西,咱们可以向其借用这件东西。用毕时,咱们必须归还。咱们并不拥有这件东西。
那么,如果我们试图修改借来的东西,会发生什么情况呢?请尝试下面清单 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:尝试修改某个借用值
下面就是那个报错:
$ cargo run
Compiling ownership_demo v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\ownership_demo)
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_demo` (bin "ownership_demo") due to previous error
正如变量默认是不可变的一样,引用也是如此。我们不能修改我们引用的东西。
可变引用
Mutable References
只需稍作调整,使用 可变引用,mutable reference,咱们即可修改某个借用值,便可修正清单 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
改为了 mut
。然后,我们在调用 change
函数处,用 &mut s
创建了一个可变引用,并更新了函数签名,以 some_string:&mut String
来接受一个可变引用。这就清楚地表明,change
函数将改变其所借用的值。
可变引用有个很大的限制:如果咱们有了到某个值的一个可变引用,就不能对该值有其他引用。下面这段试图创建两个到 s
可变引用的代码,就会失败:
文件名:src/main.rs
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println! ("{}, {}", r1, r2); }
下面是报错信息:
$ cargo run
Compiling ownership_demo v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\ownership_demo)
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_demo` (bin "ownership_demo") due to previous error
这个报错讲到,因为我们不能将 s
作为可变引用,同一实际借用多次,因此这段代码无效。第一次可变借用是在 r1
中,而必须持续到其在 println!
中被使用为止,但在这个可变引用的创建和使用之间,我们试图在 r2
中,创建另一个借用了与 r1
同一数据的可变引用。
防止在同一时间,对同一数据进行多个可变引用的这种限制,允许改变,但改变是在非常受控的方式下进行的。这也是 Rust 新手比较头疼的问题,因为大多数语言,都允许咱们随时改变。有着这种限制的好处是,Rust 可以在编译时,防止数据竞赛。数据竞赛,data race 类似于某种竞赛情形,会在下面这三种行为发生时出现:
-
两个以上的指针同时访问某同一数据;
-
至少有一个指针被用来写该数据;
-
没有同步访问该数据的机制。
数据竞赛会导致未定义的行为,当咱们试图在运行时跟踪他们时,会很难诊断和修复;Rust 通过拒绝编译带有数据竞赛的代码,从而避免了这个问题!
与往常一样,我们可以使用花括号,创建一个新的作用域,从而允许多个可变引用,只要不是 同时 的多个:
#![allow(unused)] fn main() { let mut s = String::from("hello"); { let r1 = &mut s; } // r1 在这里超出了作用域,因此我们可以毫无问题地构造一个新的引用。 let r2 = &mut s; }
Rust 对组合可变引用和不可变引用,也执行类似的规则。下面这段代码会导致一个报错:
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &s; let r3 = &mut s; println! ("{}, {} 与 {}", r1, r2, r3); }
下面是那个报错:
$ cargo run
Compiling ownership_demo v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\ownership_demo)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src\main.rs:6:14
|
4 | let r1 = &s;
| -- immutable borrow occurs here
5 | let r2 = &s;
6 | let r3 = &mut s;
| ^^^^^^ mutable borrow occurs here
7 |
8 | println! ("{}, {}, {}", r1, r2, r3);
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership_demo` (bin "ownership_demo") due to previous error
呼!我们 也 不能在有着到某个值的不可变引用的同时,有着一个到这同一值的可变引用。
某个不可变引用的使用者,不期望该值会突然在他们眼皮底下改变!不过,多个不可变引用是允许的,因为仅在读取数据的使用者,并无影响其他读取该数据使用者的能力。
请注意,引用的作用域从该引用被引入的地方开始,并持续到该引用被最后一次使用处为止。例如,下面这段代码可以编译,因为那个不可变引用的最后一次使用(println!
),发生在那个可变引用被引入之前:
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &s; // 没有问题 let r2 = &s; // 没有问题 println! ("r1 与 r2: {}, {}", r1, r2); // 变量 r1 与 r2 在此处之后将不会使用了 let r3 = &mut s; // 这就没问题了 println! ("r3: {}", r3); }
不可变引用 r1
和 r2
的作用域,在他们最后一次被使用的 println!
之后,可变引用 r3
被创建之前结束。这些作用域不会重叠,因此这段代码会被放行:编译器可以区分出,在作用域结束前的某个点,该引用不再被使用。
译注:由于引用属于大小已知、固定不变的类型,因此他们是保存在栈上的,带有
Copy
特质,故上面的代码中,在let r3 = &mut s;
语句之前,可以无限次使用r1
和r2
这两个不可变引用,他们不会被迁移到println!
宏及其他函数中。
尽管借用方面的报错有时会令人沮丧,但请记住,这是 Rust 编译器在早期(编译时而不是运行时)就指出某个潜在错误,并准确地告诉咱们问题所在。这样,咱们就不必再追踪,为什么咱们的数据和咱们设想的不一样了。
悬空引用
Dangling References
在带有指针的语言中,就很容易错误地创建出 悬空指针,dangling pointer,即在保留了指向某处内存指针的同时,释放了该处内存,从而造成引用了内存中,可能已经给了其他代码的某个位置的指针。相比之下,在 Rust 中,编译器会保证引用,永远不会成为悬空引用:如果咱们有个到某数据的引用,编译器会确保该数据,不会在指向该数据引用超出作用域之前,超出作用域。
我们来尝试创建一个悬挂引用,看看 Rust 如何通过编译时报错,来防止他们:
文件名:src/main.rs
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s }
下面是那个报错:
$ cargo run
Compiling ownership_demo v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\ownership_demo)
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 {
| +++++++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership_demo` (bin "ownership_demo") due to previous error
这条报错信息,涉及我们尚未讲到的一项特性:生命周期。我们将在第 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
#![allow(unused)] fn main() { fn dangle() -> &String { // dangle 返回的是个到某 String 的引用 let s = String::from("hello"); // s 是个新的 String &s // 咱们返回了一个指向那个 String,s 的引用 } // 这里,s 超出了作用域,进而被丢弃。他的内存就没了。 // 危险所在! }
因为 s
是在 dangle
内部创建的,所以当 dangle
的代码结束时,s
将被解除内存分配。然而我们曾试图返回对他的引用。这意味着这个引用,将指向一个无效的 String
。这可不行!Rust 不允许我们这么做。
这里的解决办法,是直接返回那个 String
值:
#![allow(unused)] fn main() { fn dangle() -> String { let s = String::from("hello"); s } }
这会没有任何问题地运作。所有权会被迁出,而不会有任何东西,被解除内存分配。
引用的规则
The Rules of References
我们来回顾一下,我们已讨论过的关于引用的内容:
-
在任何时候,咱们都可以有着一个可变引用,或任意数量的不可变引用;
-
引用必须始终有效。
接下来,我们来看看另一种引用:切片。