理解Rust 生命周期、所有权和借用机制
本文试图解释初学者都会遇到的一个概念Rust:它的“借用检查机制”。借用检查机制检查所有对数据的访问是否合法。检查所有的数据访问是否合法,可以让Rust避免安全问题。通过避免与编译器发生冲突,了解这个系统的工作原理至少可以加快开发时间。更重要的是,学会使用借用检查机制可以让你自信地构建更大的软件系统。
引出问题
为了简化,我们的示例场景为:通过机器ID查询其状态,检查方法始终返回相同的值。
- 示例代码:
fn main() {
let mac_a_id = 1;
let mac_b_id = 2;
let a_status = show_status(mac_a_id);
let b_status = show_status(mac_b_id);
println!("a:{:?}, b:{:?}", a_status, b_status);
let a_status = show_status(mac_a_id);
let b_status = show_status(mac_b_id);
println!("a:{:?}, b:{:?}", a_status, b_status);
}
#[derive(Debug)]
enum MacStatus {
OK,
}
fn show_status(mac_id: u32) -> MacStatus{
MacStatus::OK
}
程序编译正常,输出结果;
a:OK, b:OK
a:OK, b:OK
下面我们定义结构体Machine,为了简化仅包括id属性。
- 示例代码:
fn main() {
let mac_a_id = Machine{id:1};
let mac_b_id = Machine{id:2};
let a_status = show_status(mac_a_id);
let b_status = show_status(mac_b_id);
println!("a:{:?}, b:{:?}", a_status, b_status);
let a_status = show_status(mac_a_id);
let b_status = show_status(mac_b_id);
println!("a:{:?}, b:{:?}", a_status, b_status);
}
#[derive(Debug)]
enum MacStatus {
OK,
}
#[derive(Debug)]
struct Machine{
id: u32,
}
fn show_status(mac_id: Machine) -> MacStatus{
MacStatus::OK
}
编译程序报错:
error[E0382]: use of moved value: `mac_a_id`
--> src/main.rs:10:32
|
2 | let mac_a_id = Machine{id:1};
| -------- move occurs because `mac_a_id` has type `Machine`, which does not implement the `Copy` trait
...
5 | let a_status = show_status(mac_a_id);
| -------- value moved here
...
10 | let a_status = show_status(mac_a_id);
| ^^^^^^^^ value used here after move
|
note: consider changing this parameter type in function `show_status` to borrow instead if owning the value isn't necessary
--> src/main.rs:26:24
...
通过上面错误信息可以看到:move occurs because
mac_a_idhas type
Machine, which does not implement the
Copy; mac_a_id 是 Machine类型,在第二次调用 show_status(mac_a_id); 时报错了,原因是没有实现Copy特征,编译器给的建议是改变
show_status` 函数参数类型,如果不是必须要所有权,使用借用(borrow)代替。
这时你可能疑惑,为啥前面的示例没有错误。原因是前面mac_a_id变量采用基础类型,基础类型默认都实现了Copy特性。Machine是自定义类型,默认没有实现Copy特性。由于在赋值和函数调用场景中,会发生所有权转移,因此第二次调用时mac_a_id已失效,不能再次被使用了。
- 解决所有权问题
Rust 的所有权系统非常出色。它提供了无需垃圾回收器即可实现内存安全的途径。但是,有一个“但是”。如果你不了解正在发生的事情,所有权系统可能会让你更加迷茫。特别是当你将过去的编程风格应用到新的范式时。
以下四种策略可以帮助解决所有权问题:
- 在不需要完全所有权的地方使用引用
- 对于复制成本可以接受时,使用复制
- 使用包装器帮助处理共享数据所有权
使用引用
修改show_status函数,参数使用引用传递:
fn main() {
let mac_a_id = Machine{id:1};
let mac_b_id = Machine{id:2};
let a_status = show_status(&mac_a_id);
let b_status = show_status(&mac_b_id);
println!("a:{:?}, b:{:?}", a_status, b_status);
let a_status = show_status(&mac_a_id);
let b_status = show_status(&mac_b_id);
println!("a:{:?}, b:{:?}", a_status, b_status);
}
#[derive(Debug)]
enum MacStatus {
OK,
}
#[derive(Debug)]
struct Machine{
id: u32,
}
fn show_status(mac_id: &Machine) -> MacStatus{
MacStatus::OK
}
输出结果一致。show_status(mac_id: &Machine),者意味着仅访问对象,不拥有所有权。
使用复制
每个对象都有一个所有者,这意味着需要对软件进行重大的预先规划和/或重构。正如我们在前一节中所看到的,要摆脱早期的设计决策可能需要做大量的工作。
重构的一种替代方法是简单地复制值。这样做通常是不受欢迎的,但在紧要关头却很有用。基本类型,如整数,就是一个很好的例子。对于CPU来说,复制基本类型的成本很低。事实上,它们是如此便宜,以至于Rust总是复制它们,否则它会担心所有权被转移。
类型可以选择两种复制模式:克隆和复制。当所有权被移动时,复制就会隐式地起作用。对象obj_a的按位被复制以创建对象obj_b。Clone显式地起作用,实现Clone的类型有 obj_a.Clone()
方法,允许执行创建新类型所需的任何操作。
fn main() {
let mac_a_id = Machine{id:1};
let mac_b_id = Machine{id:2};
let a_status = show_status(mac_a_id);
let b_status = show_status(mac_b_id);
println!("a:{:?}, b:{:?}", a_status, b_status);
let a_status = show_status(mac_a_id);
let b_status = show_status(mac_b_id);
println!("a:{:?}, b:{:?}", a_status, b_status);
}
#[derive(Debug)]
enum MacStatus {
OK,
}
#[derive(Clone, Copy, Debug)]
struct Machine{
id: u32,
}
fn show_status(mac_id: Machine) -> MacStatus{
MacStatus::OK
}
要使 #[derive(Clone, Copy, Debug)] 起作用,结构体或枚举的所有成员都必须已实现了Copy。如果其中包括集合类型(如vec,大小不确定)这将不起作用,当然这是我们可以手动实现Copy和Clone。
impl Copy for Machine { }
impl Clone for Machine {
fn clone(&self) -> Self {
CubeSat { id: self. id }
}
}
当数据内容暂用内存较大,复制过程增加资源成本,这时采用引用会比复制更佳。
包装数据
- & 引用
- 当你只是想在不获取所有权的情况下访问一个值,并且不需要共享所有权时,使用普通引用。例如,在函数调用中传递参数,只是为了读取数据而不改变数据的所有权和内容,就可以使用
&
引用。 - 当你需要遵循严格的可变和不可变引用规则,在一个有限的范围内修改数据,并且不涉及共享所有权的情况,也可以使用
&mut
引用。
- 当你只是想在不获取所有权的情况下访问一个值,并且不需要共享所有权时,使用普通引用。例如,在函数调用中传递参数,只是为了读取数据而不改变数据的所有权和内容,就可以使用
- Rc 引用计数智能指针
- 当你需要在多个部分的代码中共享同一份数据的所有权,并且这些部分的代码生命周期可能不同时,
Rc
是很有用的。例如,在一个复杂的数据结构中,多个节点可能需要共享同一个配置值,使用Rc
可以方便地实现共享而不用担心所有权的转移和数据的过早释放。 - 不过要注意,由于
Rc
只提供了不可变共享访问,在需要修改共享数据的场景下,需要结合内部可变性机制,并且要谨慎处理可能出现的运行时错误,比如多个地方同时尝试修改数据导致的借用检查失败(如果使用RefCell
)。
- 当你需要在多个部分的代码中共享同一份数据的所有权,并且这些部分的代码生命周期可能不同时,
但有时变量是结构体的一部分,可能无法克隆该结构体。或者字符串真的很长,你不想克隆它。这是使用Rc的一些原因,它允许您拥有多个所有者。Rc就像一个优秀的办公室职员:Rc记录下谁拥有所有权,多少人拥有所有权。一旦所有者的数量降到0,这个变量就可以消失了。
下面是如何使用Rc。首先想象两个结构体:一个叫City,另一个叫CityData。City有一个城市的信息,而CityData把所有的城市放在一起。
#[derive(Debug)]
struct City {
name: String,
population: u32,
city_history: String,
}
#[derive(Debug)]
struct CityData {
names: Vec<String>,
histories: Vec<String>,
}
fn main() {
let calgary = City {
name: "Calgary".to_string(),
population: 1_200_000,
// Pretend that this string is very very long
city_history: "Calgary began as a fort called Fort Calgary that...".to_string(),
};
let canada_cities = CityData {
names: vec![calgary.name], // This is using calgary.name, which is short
histories: vec![calgary.city_history], // But this String is very long
};
println!("Calgary's history is: {}", calgary.city_history); // ⚠️
}
当然上面代码不起作用,因为canada_cities现在拥有数据,而calgary没有。出错信息如下:
error[E0382]: borrow of moved value: `calgary.city_history`
--> src\main.rs:27:42
|
24 | histories: vec![calgary.city_history], // But this String is very long
| -------------------- value moved here
...
27 | println!("Calgary's history is: {}", calgary.city_history); // ⚠️
| ^^^^^^^^^^^^^^^^^^^^ value borrowed here after move
|
= note: move occurs because `calgary.city_history` has type `std::string::String`, which does not implement the `Copy` trait
我们可以克隆名称:names: vec![calgary.name.clone()],但我们不想克隆city_history,因为它太长了。我们可以用Rc。
use std::rc::Rc;
#[derive(Debug)]
struct City {
name: String,
population: u32,
city_history: Rc<String>,
}
#[derive(Debug)]
struct CityData {
names: Vec<String>,
histories: Vec<Rc<String>>,
}
fn main() {}
要添加新引用,必须克隆Rc。但是等等,我们不是想要避免使用.clone()吗?不完全是:我们不想克隆整个String。但是Rc的克隆只是复制指针——它基本上是免费的。这就像在一盒书上贴上一张贴纸来表明它是两个人的,而不是做一个全新的盒子。
你可以使用item.clone()或Rc::clone(&item)克隆名为item的Rc。所以calgary.city_history有2个所有者。我们可以使用Rc::strong_count(&item)来检查所有者的数量。同时,让我们添加一个新的所有者。现在我们的代码看起来像这样:
use std::rc::Rc;
#[derive(Debug)]
struct City {
name: String,
population: u32,
city_history: Rc<String>, // String inside an Rc
}
#[derive(Debug)]
struct CityData {
names: Vec<String>,
histories: Vec<Rc<String>>, // A Vec of Strings inside Rcs
}
fn main() {
let calgary = City {
name: "Calgary".to_string(),
population: 1_200_000,
// Pretend that this string is very very long
city_history: Rc::new("Calgary began as a fort called Fort Calgary that...".to_string()), // Rc::new() to make the Rc
};
let canada_cities = CityData {
names: vec![calgary.name],
histories: vec![calgary.city_history.clone()], // .clone() to increase the count
};
println!("Calgary's history is: {}", calgary.city_history);
println!("{}", Rc::strong_count(&calgary.city_history));
let new_owner = calgary.city_history.clone();
}
这打印2。new_owner现在是Rc。现在如果我们使用println!(“{}”,Rc: strong_count (&calgary.city_history));,我们得到3。
Rc
(Reference Counting
,引用计数)是一种强指针类型。它通过在内部维护一个引用计数来确保只要还有引用(Rc
指针)指向一个对象,这个对象就不会被销毁。与 Rc
相对的是 Weak
指针(通常与 Rc
一起使用)。Weak
指针不会影响对象的引用计数,它允许对对象进行临时的、非所有权的访问。
弱指针很有用,因为如果两个Rc互相指向对方,它们就不会死亡。这被称为“循环引用”。如果第1项与第2项之间有Rc,第2项与第1项之间也有Rc,它们不可能等于0。在这种情况下,可以使用弱引用。然后Rc将计算引用,但如果它只有弱引用,那么它可能会死亡。使用 Rc::downgrade(&item) 而不是Rc::clone(&item)来创建弱引用。另外,您可以使用 Rc::weak_count(&item) 来查看弱计数。
Rc不允许修改,为此需要包装包装器。Rc<RefCell>是可以用来执行内部可变性的类型,具有内部可变性的对象在修改内部值时呈现不可变的外观。