理解Rust 生命周期、所有权和借用机制

本文试图解释初学者都会遇到的一个概念Rust:它的"借用检查机制"。借用检查机制检查所有对数据的访问是否合法。检查所有的数据访问是否合法,可以让Rust避免安全问题。通过避免与编译器发生冲突,了解这个系统的工作原理至少可以加快开发时间。更重要的是,学会使用借用检查机制可以让你自信地构建更大的软件系统。

引出问题

为了简化,我们的示例场景为:通过机器ID查询其状态,检查方法始终返回相同的值。

  • 示例代码:
rust 复制代码
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属性。

  • 示例代码:
rust 复制代码
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
}

编译程序报错:

shell 复制代码
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 typeMachine, 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 的所有权系统非常出色。它提供了无需垃圾回收器即可实现内存安全的途径。但是,有一个"但是"。如果你不了解正在发生的事情,所有权系统可能会让你更加迷茫。特别是当你将过去的编程风格应用到新的范式时。

以下四种策略可以帮助解决所有权问题:

  1. 在不需要完全所有权的地方使用引用
  2. 对于复制成本可以接受时,使用复制
  3. 使用包装器帮助处理共享数据所有权

使用引用

修改show_status函数,参数使用引用传递:

rust 复制代码
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() 方法,允许执行创建新类型所需的任何操作。

rust 复制代码
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。

rust 复制代码
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把所有的城市放在一起。

rust 复制代码
#[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没有。出错信息如下:

shell 复制代码
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。

rust 复制代码
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)来检查所有者的数量。同时,让我们添加一个新的所有者。现在我们的代码看起来像这样:

rust 复制代码
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。

RcReference 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>是可以用来执行内部可变性的类型,具有内部可变性的对象在修改内部值时呈现不可变的外观。

相关推荐
Prejudices4 分钟前
C++如何调用Python脚本
开发语言·c++·python
我狠狠地刷刷刷刷刷17 分钟前
中文分词模拟器
开发语言·python·算法
wyh要好好学习20 分钟前
C# WPF 记录DataGrid的表头顺序,下次打开界面时应用到表格中
开发语言·c#·wpf
AitTech21 分钟前
C#实现:电脑系统信息的全面获取与监控
开发语言·c#
qing_04060323 分钟前
C++——多态
开发语言·c++·多态
孙同学_23 分钟前
【C++】—掌握STL vector 类:“Vector简介:动态数组的高效应用”
开发语言·c++
froginwe1124 分钟前
XML 编辑器:功能、选择与使用技巧
开发语言
Jam-Young30 分钟前
Python的装饰器
开发语言·python
man201738 分钟前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
hlsd#1 小时前
关于 SpringBoot 时间处理的总结
java·spring boot·后端