所有权这块知识量太大了,慢慢嚼(头秃
引用和借用
知识回顾:
...以上省略
Rust 允许我们使用元组返回多个值:
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
// s1 已经被移动,无法使用
println!("The length of '{}' is {}", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回字符串的长度
(s, length)
}
Rust 有一个使用值而不转移所有权的功能,称为引用
上述代码中,存在一个问题:我们必须将String
值返回给调用函数,才能在调用calculate_length
函数之后继续使用该 String
。这是因为String
在传递给calculate_length
函数时被「移动」
了所有权(我说呢,上一篇内容我觉得所有权还是移动了,可是文档给我说有个引用,放到这一节来讲了。gkd,继续学习),这样转来转去脑袋都糊了。
为了避免这种所有权的转移,我们可以提供一个指向String
值的「引用」(reference)
。引用类似于指针,它是一种地址,可以用来访问存储在该地址的数据。但与指针不同的是,引用在其生命周期内始终保证指向一个有效的、特定类型的值。
借用
以下是如何定义和使用calculate_length
将对象引用作为参数而不是获取值的所有权的函数:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
之前的代码中使用了元组来返回String
和它的长度,但现在元组代码被去掉了。这意味着函数不再需要返回所有权,而是可以通过引用的方式访问数据。现在代码中传递给calculate_length
函数的是&s1
,这是一个指向s1
的引用,而不是s1
本身。在函数定义中,参数类型也从String
变成了&String
,表明函数接收的是一个引用:
图4-5:&String s
指向String s1
的示意图
*
运算符表示 解引用(Dereferencing) 操作,它是引用机制的反面。(后续知识点
想象s
是一个保存值的变量(比如一本书)。它的作用域定义了它在代码中可访问的位置,类似于函数参数。
当你向函数传递&s
(s
的引用)时,你是在 借用 这本书,而不是把它送出去。原始变量s
仍然在作用域内,但函数通过引用获得了对这本书的临时访问权限。在 Rust 中,变量 拥有 其数据的所有权。当你将整个值传递给函数(例如s
),所有权就转移到函数。为了 "归还" 所有权,函数通常需要返回值。
使用引用时,没有所有权转移。函数只是借用了数据,因此不需要返回任何东西来归还所有权。就好像借了一本真书一样,你通常可以在 Rust 函数中读取借用的数据。但是,你可能无法修改它(比如在书上写字)。尝试以与借用者访问冲突的方式修改借用数据(例如同时修改),通常会导致错误:
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
错误内容:
$ cargo run
...
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`.
...
以上代码,编译器不允许我们修改引用的内容。
可变引用
就像变量一样,除非使用
mut
关键字显式声明为可变,否则引用默认为不可变。如果需要通过引用修改数据,必须使用&mut
关键字将其明确声明为可变。
上述代码,我们稍微修改下,就能运行啦:
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
我们将s
修改为mut s
,然后,我们使用&mut
创建一个可变引用,在其中调用change
函数,并更新函数参数以接受使用some_string: &mut String
的可变引用。
可变引用 在 Rust 中具有一个重要的限制:如果您拥有一个值的可变引用,则不能再拥有该值的其他任何引用。 这意味着您不能同时拥有多个指向同一个值的可变引用:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
试图同时创建两个指向 s 的可变引用 (r1 和 r2)。
错误内容:
$ cargo run
...
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`.
...
此错误表明此代码无效,因为我们一次不能多次借用s
作为可变对象。 第一个可变借用位于r1
中,并且必须持续到在println!
中使用为止,但在该可变引用的创建和使用之间,我们尝试在r2
中创建另一个可变引用,借用与r1
相同的数据。
为了保证数据安全和一致性,Rust 限制了可变引用的数量。只能在同一个时间点存在一个指向同一数据的可变引用。这对于初学者来说可能比较难以理解,因为大多数语言允许随时修改数据。
不过好处就是这个限制可以帮助 Rust 在编译时就防止数据竞争。
当然,如果需要在不同时间点进行多个修改,可以使用花括号 ({}
) 创建新的作用域,将修改操作封装在不同的作用域内,这使得每个作用域内允许有一个可变引用,但仍然遵守同一时间只能有一个可变引用的限制:
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
println!(" {}", r1);
}
let r2 = &mut s;
println!(" {}", r2);
}
不可同时拥有可变引用和不可变引用,当你拥有一个值的可变引用时,不能再拥有该值的任何其他引用,包括不可变引用。这是为了防止数据被意外修改,并确保所有引用都指向一致的值。
如果强制这么使用:
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
...
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`.
...
So,当我们拥有相同值的不可变引用时,我们也不能拥有可变引用。
不可变引用和可变引用遵循不同规则:
- 不可变引用 只能用于读取数据,保证数据不会在引用期间发生改变。
- 可变引用 可以用于读取和修改数据,但会限制同时存在的引用数量。
为什么不能同时拥有可变引用和不可变引用?
- 用户对不可变引用做出的假设是数据保持不变,如果数据突然改变,可能会导致程序行为出乎意料,引起错误。
- 同时拥有可变引用和不可变引用无法保证数据的一致性。可变引用可以随时修改数据,使得不可变引用看到不一致的值。
- 编译器优化也受到影响。Rust 编译器可以对不可变引用进行优化,假设数据不会改变。如果允许同时拥有可变引用,这些优化就变得不可靠。
为什么允许多个不可变引用?
- 多个不可变引用只会读取数据,不会修改数据。
- 它们之间不会相互影响,因为它们都只是在读取同一个不变的值。
- 这符合数据安全和一致性的原则,多个读取不影响数据本身。
请注意,引用的范围从引入它的位置开始,一直持续到上次使用该引用时。
例如,下列代码则能进行编译,因为最后一次使用不可变引用println!
发生在引入可变引用之前:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, 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 中,编译器保证引用永远不会是悬空引用:如果您引用了某些数据,编译器将确保该数据不会在对该数据的引用之前超出范围。
我们看以下示例程序:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle 返回对 String 的引用
let s = String::from("hello"); // s 是一个new String
&s // 我们返回对String s 的引用
} // 此处,s 超出范围并被删除。 它的内存消失了。 危险!
错误内容:
$ cargo run
...
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`.
...
因为s
是在dangle
内部创建的,所以当dangle
的代码运行结束时,s
将被释放。 但我们试图返回对它的引用。 这意味着这个引用将指向一个无效的String
。 这里 Rust 不允许我们这么做。
因此我们只能直接返回String
:
fn main() {
let _reference_to_nothing = no_dangle();
}
fn no_dangle() -> String {
let s = String::from("hello");
s
}
这样就没有任何问题了,所有权被移出,并且没有任何内容被释放。
引用规则
- 在任何给定时间,您可以拥有一个可变引用或任意数量的不可变引用。
- 引用必须始终有效。
接下来,我们将看看另一种不同类型的引用:切片
引用和借用暂时告一段落,下一节见