Rust 引用与借用
文章目录
- 引用与借用
- 悬垂引用(Dangling Reference)
- 引用规则总结
引用与借用
fn main() {let s1 = String::from("hello");let (s2, len) = calculate_length(s1);println!("The length of '{s2}' is {len}.");
}fn calculate_length(s: String) -> (String, usize) {let length = s.len();(s, length)
}
上述的元组代码中,我们必须将 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}."); // The length of 'hello' is 5.
}fn calculate_length(s: &String) -> usize {s.len()
}
首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递 &s1
给 calculate_length
,并且在函数定义中参数类型是 &String
而不是 String
。这些 &
符号表示引用,它们允许你引用某个值而不获取其所有权。下图展示了这个概念。
三张表:s
的表只包含一个指向 s1
的指针;s1
的表包含 s1 的栈数据,并指向堆上的字符串数据。
注意:使用 &
引用的反操作是解引用,通过 *
运算符实现。这与C/C++
是类似的。
让我们更仔细地看看这里的函数调用:
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
不再被使用而被丢弃,因为 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");
}错误如下:
$ 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) {
| +++
更多关于此错误的信息,请尝试 `rustc --explain E0596`。
error: could not compile `ownership` (bin "ownership") due to 1 previous error
正如变量默认是不可变的,引用也是如此。我们不能修改我们通过引用获得的内容。## 可变引用我们可以通过一些小改动,将以上代码修改为允许我们修改被借用的值,只需使用可变引用:文件名:src/main.rs
```rust
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
函数会修改它借用的值。
可变引用有一个重要限制:如果你有某个值的可变引用,就不能再有其他对该值的引用。下面这段试图创建两个可变引用的代码会失败:
文件名:src/main.rs
此代码无法编译!
let mut s = String::from("hello");let r1 = &mut s;
let r2 = &mut s;println!("{}, {}", r1, r2);
错误如下:
$ cargo runCompiling 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
更多关于此错误的信息,请尝试 rustc --explain E0499
。
error: could not compile ownership
(bin “ownership”) due to 1 previous error
这个错误说明代码无效,因为我们不能同时多次可变借用 s。第一次可变借用发生在 r1,并且直到 println! 使用 r1 之前都有效,但在创建 r1 到使用它之间,我们又试图用 r2 可变借用同一数据。
禁止同时存在多个可变引用的限制,使得可变操作受到严格控制。许多新 Rustacean 会对此感到困惑,因为大多数语言允许你随时修改数据。这个限制的好处是 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!("{}, {}, and {}", r1, r2, r3);
错误如下:
$ cargo runCompiling 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; // 没问题| -- 不可变借用发生在这里
5 | let r2 = &s; // 没问题
6 | let r3 = &mut s; // 大问题| ^^^^^^ 可变借用发生在这里
7 |
8 | println!("{}, {}, and {}", r1, r2, r3);| -- 不可变借用稍后在这里被使用
更多关于此错误的信息,请尝试 rustc --explain E0502
。
error: could not compile ownership
(bin “ownership”) due to 1 previous error
我们也不能在有不可变引用的同时拥有可变引用。
不可变引用的使用者不会期望值会被突然修改!但允许多个不可变引用,因为只读不会影响其他人的读取。
注意,引用的作用域从它被引入开始,直到最后一次使用。例如,下面的代码可以编译,因为不可变引用的最后一次使用发生在 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 编译器在编译时(而不是运行时)早早指出潜在 bug,并准确告诉你问题所在。这样你就不用在运行时排查数据为何异常。
悬垂引用(Dangling Reference)
在有指针的语言中,很容易错误地创建悬垂指针——即指向已被释放内存的指针。Rust 则保证引用永远不会悬垂:如果你有某个数据的引用,编译器会确保数据在引用之前不会离开作用域。
让我们尝试创建一个悬垂引用,看看 Rust 如何用编译错误阻止它:
文件名:src/main.rs
此代码无法编译!
fn main() {let reference_to_nothing = dangle();
}fn dangle() -> &String {let s = String::from("hello");&s
}
错误如下:
$ cargo runCompiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier--> src/main.rs:5:16|
5 | fn dangle() -> &String {| ^ 需要命名生命周期参数|= help: 此函数的返回类型包含借用值,但没有可供借用的值
help: 可考虑使用 `'static` 生命周期,但除非返回 const 或 static 的借用值,否则很少用|
5 | fn dangle() -> &'static String {| +++++++
help: 你更可能想返回拥有所有权的值|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {|
error[E0515]: cannot return reference to local variable `s`--> src/main.rs:8:5|
8 | &s| ^^ 返回对当前函数拥有的数据的引用
部分错误有详细解释:E0106、E0515。
更多关于错误的信息,请尝试 rustc --explain E0106
。
error: could not compile ownership
(bin “ownership”) due to 2 previous errors
这个错误信息提到了我们还没讲到的生命周期特性。我们将在第 10 章详细讨论生命周期。不过,忽略生命周期部分,错误信息已经说明了问题的关键:
此函数的返回类型包含借用值,但没有可供借用的值
让我们仔细看看 dangle
代码每一步发生了什么:
文件名:src/main.rs
此代码无法编译!
fn dangle() -> &String { // dangle 返回对 String 的引用let s = String::from("hello"); // s 是新建的 String&s // 返回对 s 的引用
} // 这里 s 离开作用域并被释放,内存消失。危险!
因为 s 在 dangle
内部创建,dangle
结束后 s
会被释放。但我们试图返回对它的引用,这意味着引用会指向无效的 String
。这样不行!Rust
不允许这样做。
解决方法是直接返回 String:
fn no_dangle() -> String {let s = String::from("hello");s
}
这样没有任何问题。所有权被转移出去,没有数据被提前释放。
引用规则总结
让我们回顾一下关于引用的内容:
- 在任意时刻,你要么只能有一个可变引用,要么可以有任意多个不可变引用;
- 引用必须始终有效。