前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Rust实战系列-生命周期、所有权和借用

Rust实战系列-生命周期、所有权和借用

作者头像
abin
发布2023-03-21 20:30:52
1.7K0
发布2023-03-21 20:30:52
举报
文章被收录于专栏:abin在路上

本文是《Rust in action》学习总结系列的第四部分,更多内容请看已发布文章:

一、Rust实战系列-Rust介绍

二、Rust实战系列-基本语法

三、Rust实战系列-复合数据类型

“理解生命周期在 Rust 中的含义,适应 Rust 的借用检查器(borrow),采用多种方法处理可能遇到的问题,理解“所有者”的职责,理解如何借用其他所有者的值。

本章解释让大多数 Rust 新手头疼的概念:借用检查器。借用检查器会检查对数据的访问是否合法,避免出现安全问题。

学会借用检查器将会提升开发效率,避免和编译器产生冲突。更重要的是,理解借用检查器可以自信地构建大型软件系统,它是“无畏并发”的基础。

本章主要解释借用检查器的工作原理,以及发现错误时如何修改。通过模拟卫星通信的例子来解释不同提供共享数据访问方式之间的权衡。

借用检查器依赖于三个相互关联的概念:生命周期、所有权和借用。

  • 生命周期

值的生命周期是指访问该值有效的时间段。函数的局部变量在函数返回前都有效,全局变量在程序的整个生命周期内都有效

  • 所有权

所有权是一个夸张的比喻。在 Rust 中,所有权与清理不再需要的值有关。例如,当函数返回时,存放局部变量的内存需要被释放。所有者并不能阻止程序其他部分访问他们拥有的值,也不能向 Rust 报告数据被盗用。

  • 借用

借用意味着访问。这是一个令人困惑的术语,因为没有将值还给所有者。“借用”是为了强调虽然 Rust 中的值只有一个所有者,但是程序的多个部分可以共享对这些值的访问。

1. 实现模拟的 CubeSats 卫星地面站

本章在可成功编译的示例代码上进行修改,这个过程可能会导致问题,但不需要修改程序流程。通过修复这些问题,加深对相关概念的理解。

本章的实例是一个 CubeSat 星系,以下是一些术语定义:

(1)CubeSats:微型人造卫星,与传统的卫星相比,增加了对空间研究的可能性。

(2)地面站:运营商和卫星之间的中介,用于接收无线电,检查星座中每颗卫星的状态,并来回传送信息。在代码中,它将作为用户和卫星之间的网关。

(3)星系:在轨卫星的集合

在轨 CubeStats 图示:

在上图中,有三个 CubeSat,为了进行建模,创建三个变量,目前使用整数表示。此外,还需要对地面站进行建模,由于还没向星系发送消息,暂时省略。

为了检查每个卫星的状态,使用函数和可以表示卫星状态消息的枚举类型。

代码语言:javascript
复制
#[derive(Debug)]
enum StatusMessage {
  Ok, // <1>
}
fn check_status(sat_id: u64) -> StatusMessage {
  StatusMessage::Ok // <1>
}

  1. 目前,所有 CubeStat 都正常运行

在生产环境中,check_status() 函数是非常复杂的,当然,对于模拟场景,只需要每次返回相同的值就可以了。将这部分代码整合到程序中,对卫星进行两次“状态检查”,最终得到如下代码:

代码语言:javascript
复制
#![allow(unused_variables)]

#[derive(Debug)]
enum StatusMessage {
  Ok,
}

fn check_status(sat_id: u64) -> StatusMessage {
  StatusMessage::Ok
}

fn main () {
  let sat_a = 0;
  let sat_b = 1;
  let sat_c = 2;

  let a_status = check_status(sat_a);
  let b_status = check_status(sat_b);
  let c_status = check_status(sat_c);
  println!("a: {:?}, b: {:?}, c: {:?}", a_status, b_status, c_status);

  // "waiting" ...
  let a_status = check_status(sat_a);
  let b_status = check_status(sat_b);
  let c_status = check_status(sat_c);
  println!("a: {:?}, b: {:?}, c: {:?}", a_status, b_status, c_status);
}

运行结果如下:

  • 第一个生命周期问题

接下来,通过引入类型安全来了解 Rust 的处理方式。创建一个类型(而不是示例代码中的整数)来模拟卫星,真实的 CubeStat 类型可能包含很多信息,包括位置、射频频段等,示例中只记录标识符。

代码语言:javascript
复制
#[derive(Debug)]
struct CubeSat {
  id: u64,
}

将结构体定义加入代码中:

代码语言:javascript
复制
#[derive(Debug)] // <1>
struct CubeSat {
  id: u64,
}

#[derive(Debug)]
enum StatusMessage {
  Ok,
}

fn check_status(sat_id: CubeSat) -> StatusMessage { // <2>
  StatusMessage::Ok
}

fn main() {
  let sat_a = CubeSat { id: 0 }; // <3>
  let sat_b = CubeSat { id: 1 }; // <3>
  let sat_c = CubeSat { id: 2 }; // <3>

  let a_status = check_status(sat_a);
  let b_status = check_status(sat_b);
  let c_status = check_status(sat_c);
  println!("a: {:?}, b: {:?}, c: {:?}", a_status, b_status, c_status);

  // "waiting" ...
  let a_status = check_status(sat_a);
  let b_status = check_status(sat_b);
  let c_status = check_status(sat_c);
  println!("a: {:?}, b: {:?}, c: {:?}", a_status, b_status, c_status);
}

  1. 修改一:添加结构体 CubeSat 的定义
  2. 修改二:修改 check_status 的参数类型为 CubeSat
  3. 修改三:创建三个 CubeSat 实例

当编译这份代码时,会得到如下提示:

可以看到,第二次使用 sat_a,sat_b 和 sat_c 时提示“使用了已经被移走的值”,同时,建议在 CubeStat 类型上实现 Copy 特征。

“移动”这个词在 Rust 中的含义非常特殊,并不是指物理上(数据)的移动,而是指所有权的移动。所有权是 Rust 社区使用的一个术语,指的是在编译过程中检查每个值是否有效,是否会被干净地清理。

Rust 中的每个值都是所有权,在上面的示例代码中,sat_a,sat_b 和 sat_c 都“拥有”它们引用的数据,当调用 check_status() 时,数据的所有权从 main() 函数范围内的变量转移到 check_status 函数中的 sat_id 变量。这个示例的最大区别是将整数放在了 CubeSat 结构体中,类型变化改变了行为的语义。

以下代码是精简版,重点在 sat_a 和转移所有权的地方:

代码语言:javascript
复制
fn main() {
  let sat_a = CubeSat { id: 0 };      // <1>

  let a_status = check_status(sat_a); // <2>

  let a_status = check_status(sat_a); // <3>
}

  1. 所有权由 CubeSat 对象的创建产生
  2. 将对象的所有权转移到 check_status() 函数,但是没有返回给 main() 函数
  3. 这时,sat_a 不再是该对象的所有者,访问无效

“如果值没有被借用,再次绑定是无效的: 如果 有 JavaScript(从 2015 年开始)等编程语言的使用经验,可能会发现,示例代码中每个 CubeSats 的变量都被重新赋值了。在上面的完整示例中,第 20 行,a_status 被赋值为第一次调用 check_status(sat_a) 的结果,第 26 行,它被重新赋值为第二次调用 check_status(sat_a) 的结果,覆盖了原来的值。 这是合法的 Rust 代码,但也必须注意所有权问题和生命周期。在没有使用借用的情况下,如果覆盖一个在程序中其他位置仍然会用到的值,编译器会拒绝编译程序。

下图展示了控制流、所有权和生命周期之间的关系:

当调用 check_status(sat_a) 的时候,所有权转移到 check_status() 函数,当 check_status() 返回 StatusMessage 时,释放 sat_a 的值,sat_a 的生命周期结束。在第一次调用 check_status() 之后,sat_a 变量仍然在 main() 函数的生命周期内,这时再次访问 sat_a 变量会导致借用检查器报错。

习惯了其他编程语言的编程思维可能难以区分生命周期和作用域之间的区别。

  • 原始类型的特殊行为

进一步了解生命周期之前,先解释一下为什么第一个示例代码能够成功编译运行。在上一份完整示例代码中,唯一的改变是将卫星变量包裹在自定义类型中,而 Rust 中的原始类型默认实现了一些特殊行为(如 Copy 特征)。

实现了 Copy 特征的类型能够被复制,否则会失败。这为开发者提供了便利,但也给新手带来了陷阱。如果直接将代码中使用整数的地方改为结构体,就不能正常工作。

从形式上看,原始类型被称为拥有 Copy 语义,而其他类型拥有 Move 语义。对于 Rust 初学者来说,这种特殊情况似乎是默认的,因为我们通常会先学习原始类型。

以下两份示例代码说明两个概念的区别,第一个能正常编译运行,第二个不能,唯一的区别是使用了不同的类型。

Rust 原始类型(以及所有实现 Copy 的类型)的 Copy 语义中,值可以被访问:

代码语言:javascript
复制
fn use_value(_val: i32) {         // <1>
}

fn main() {
  let a = 123 ;
  use_value(a);

  println!("{}", a);              // <2>
}

  1. use_value() 取得了 _val 参数的所有权,use_value() 函数是通用的,将在下个示例中使用
  2. 在 use_value() 返回后,访问 a 是完全合法的(a 是整数类型)

没有实现 Copy 特征的类型默认具有 Move 语义,当被用作取得所有权函数的参数时,不能从函数外的作用域再次访问这个值:

代码语言:javascript
复制
fn use_value(_val: Demo) {    // <1>
}

struct Demo {
  a: i32,
}

fn main() {
  let demo = Demo { a: 123 };
  use_value(demo);

  println!("{}", demo.a);     // <2>
}

  1. use_value() 获得 _val 的所有权
  2. 在 use_value() 函数执行完后,访问 demo.a 是非法的(没有实现 Copy 语义)

2. 图示

下图使用特定的符号来说明作用域、生命周期和所有权这三个相互关联的概念。

3. 所有者是指什么?它们的职责?

在 Rust 中,所有权的概念是有限的:所有者会在值的生命周期结束时对其进行清理。

当值超出范围或生命周期因其他原因结束时,会调用析构器。解构器是一个函数,通过删除引用和释放内存来清除值。在大多数 Rust 代码中,都看不到对析构函数的直接调用,因为编译器会自动注入这些代码,进而跟踪每个值的生命周期。

如果要为某个类型提供自定义的析构器,需要实现 Drop,通常是在使用了 unsafe 代码块分配内存的时候需要。Drop 有一个方法 drop(&mut self),可以实现必要的清理操作。

这种规定的含义是:值的生命周期不能超过其所有者。这种情况会使得通过引用构建的数据结构(树和图)显得有点“官僚”,如果树的根节点是所有者,在不考虑所有权的情况下,不能删除它。

最后,与洛克[1](Lockean)的个人财产概念不同,所有权并不意味着控制或主权。事实上,值的“所有者”甚至没有对拥有数据的特殊访问权限,也无法阻止程序的其它部分访问。

4. 如何转移所有权

在 Rust 中,有两种方式将所有权从一个变量转移到另一个变量。第一种是赋值,第二种是通过函数传递数据(要么是作为参数,要么是作为返回值)。

重新阅读上面的完整代码,可以看到,sat_a 一开始对 CubeSat 对象有所有权:

代码语言:javascript
复制
fn main() {
  let sat_a = CubeSat { id: 0 };
  // ...”

然后,CubeSat 对象作为参数被传入 check_status() 函数,将所有权转移到本地变量 sat_id:

代码语言:javascript
复制
fn main() {
  let sat_a = CubeSat { id: 0 };
  // ...
  let a_status = check_status(sat_a);
  // ...

另一种可能是 sat_a 在 main() 中把所有权让给了另一个变量(其他编程语言中叫赋值,Rust 中叫变量绑定),就像这样:

代码语言:javascript
复制
fn main() {
  let sat_a = CubeSat { id: 0 };
  // ...
  let new_sat_a = sat_a;
  // ...

最后,如果 check_status() 函数声明发生了变化,也可以将 CubeSat 的所有权传递给调用范围内的一个变量。以下是原始函数:

代码语言:javascript
复制
fn check_status(sat_id: CubeSat) -> StatusMessage {
  StatusMessage::Ok
}

修改之后,通过打印的方式实现消息通知:

代码语言:javascript
复制
fn check_status(sat_id: CubeSat) -> CubeSat {
  println!("{:?}: {:?}", sat_id, StatusMessage::Ok); // <1>
  sat_id // <2>
}
  1. 使用 Debug 格式化语法,自定义类型使用了 #[derive(Debug)]
  2. 通过省略最后一行末尾的分号来返回一个值

当修改后的 check_status() 函数与新的 main() 一起使用时,可以看到 CubeSat 对象的所有权又回到了原始变量。新的代码:

代码语言:javascript
复制
#![allow(unused_variables)]

#[derive(Debug)]
struct CubeSat {
  id: u64,
}

#[derive(Debug)]
enum StatusMessage {
  Ok,
}

fn check_status(sat_id: CubeSat) -> CubeSat {
  println!("{:?}: {:?}", sat_id, StatusMessage::Ok);
  sat_id
}

fn main () {
  let sat_a = CubeSat { id: 0 };
  let sat_b = CubeSat { id: 1 };
  let sat_c = CubeSat { id: 2 };

  let sat_a = check_status(sat_a); // <1>
  let sat_b = check_status(sat_b);
  let sat_c = check_status(sat_c);

  // "waiting" ...
  let sat_a = check_status(sat_a);
  let sat_b = check_status(sat_b);
  let sat_c = check_status(sat_c);
}

  1. 新的 let 将 check_status() 的返回值重新绑定到原来的 sat_a

打印到控制台的行为已经变化,由 check_status() 函数完成,新的 main() 函数执行结果:

以下是直观的表示:

5. 解决所有权问题

Rust 的所有权系统非常好,提供了保证内存安全的方法,不需要垃圾收集器。但是,如果对所有权理解不透彻,在编程的时候可能会遇到问题,特别是受到以往编程经验的影响。

以下四个方法可以解决所有权问题:

(1)在不需要所有权的地方使用引用(&)

(2)复制(Copy)值

(3)重构代码,减少长生命周期对象的数量

(4)将数据包裹在能解决移动问题的类型中

为了理解这些策略,接下来扩展卫星网络的能力,使得地面站和卫星能够发送和接收信息。

忽略实现细节,应该避免出现这样的代码:

代码语言:javascript
复制
base.send(sat_a, "hello!"); // <1>
sat_a.recv();
  1. 将 sat_a 的所有权转移到 base.send() 函数中的局部变量,导致这个值不能再被 main() 函数的其他部分访问。

为了实现这个功能,还需要一些新的类型。在以下示例代码中,为 CubeSat 结构添加了新的字段 Mailbox,CubeSat.mailbox 是一个 mailbox 结构,它的 messages 字段中包含一个 Messages 向量。在示例代码中,Message 是 String 的别名,可以直接使用 String 类型的方法而不需要重新实现。

代码语言:javascript
复制
#[derive(Debug)]
struct CubeSat {
  id: u64,
  mailbox: Mailbox,
}
#[derive(Debug)]
enum StatusMessage {
  Ok,
}

#[derive(Debug)]
struct Mailbox {
  messages: Vec<Message>,
}

type Message = String;

此时,创建 CubeSat 实例变得稍微复杂,因为还需要创建相关的 Mailbox 和 Mailbox 中的 Vec<Message>,以下是示例:

代码语言:javascript
复制
CubeSat {
  id: 100,
  mailbox: Mailbox {
    messages: vec![]
  }
}

另一个需要添加的类型是地面站,目前使用空的结构体,后面会为其添加方法和 Mailbox 字段。以下是地面站结构的定义:

代码语言:javascript
复制
struct GroundStation;

创建地面站实例:

代码语言:javascript
复制
GroundStation {};
  • 在不需要所有权的地方使用引用(&)

使用 Rust 编程时,最常见的改变是减少对高访问级别的要求,尽可能在函数定义中使用借用而不是所有权。对于只读访问,使用 &T,对于读/写访问,使用 &mut T。只有在某些高级场景下需要所有权,比如希望调整参数的生命周期。

以下是两种方法的比较:

发送消息的实现细节在 send 方法中,本质上必须修改 CubeSat 的内部字段 Mailbox,为了简单起见,函数返回 () ,并希望在出现太阳风的情况下也正常工作。

以下是最终流程,向 sat_a 发送消息,并且接收消息:

代码语言:javascript
复制
base.send(sat_a, "hello!".to_string());

let msg = sat_a.recv();
println!("sat_a received: {:?}", msg); // -> Option("hello!")

实现 send 和 recv 方法:

代码语言:javascript
复制
impl GroundStation {
    fn send(&self, to: &mut CubeSat, msg: Message) { // <1>
        to.mailbox.messages.push(msg);               // <2>
    }
}

impl CubeSat {
    fn recv(&mut self) -> Option<Message> {
        self.mailbox.messages.pop()
    }
}
  1. &self 表示 GroundStation.send() 只需要对 self 的只读引用。接受者 to 是对 CubeSat 实例的可变借用(&mut,需要修改 to 中 mailbox.message 的值 ),msg 对其 Message 实例有完全的所有权(函数返回时生命周期结束)
  2. Messag 实例的所有权从 msg 转移到 messages.push() 的局部变量

注意,GroundStation.send() 和 CubeSat.recv() 都需要对 CubeSat 实例的可变访问,因为这两个方法都修改了 CubeSat.messages 向量。此外,将要发送消息的所有权转移到 messages.push() 中,如果在消息被发送后(send 函数返回)再次访问,会出现错误提示。

避免所有权问题的图示:

以下是完整示例代码:

代码语言:javascript
复制
#[derive(Debug)]
struct CubeSat {
  id: u64,
  mailbox: Mailbox,
}

#[derive(Debug)]
struct Mailbox {
  messages: Vec<Message>,
}

type Message = String;

struct GroundStation;

impl GroundStation {
    fn send(&self, to: &mut CubeSat, msg: Message) {
        to.mailbox.messages.push(msg);
    }
}

impl CubeSat {
    fn recv(&mut self) -> Option<Message> {
        self.mailbox.messages.pop()
    }
}

fn main() {
    let base = GroundStation {};
    let mut sat_a = CubeSat { id: 0, mailbox: Mailbox { messages: vec![] } };

    println!("t0: {:?}", sat_a);
    base.send(&mut sat_a, Message::from("hello there!")); // <1>

    println!("t1: {:?}", sat_a);

    let msg = sat_a.recv();
    println!("t2: {:?}", sat_a);

    println!("msg: {:?}", msg);
}
  1. 目前还没有完整的方法来创建 Message 实例,而是利用 String.from() 方法,将 &str 转换为 String(别名 Message)类型。
  • 尽可能少用生命周期长的值

如果使用类似全局变量这种生命周期很长的对象,可能不需要为使用这些值的所有组件都保留对象。取而代之,可以考虑使用更分散的、周期更短的对象,所有权问题可以通过重新设计程序来解决。

在 CubeSat 示例中,不需要处理太多复杂的细节,四个变量 base、sat_a、sat_b 和 sat_c 都在 main() 的生命周期内。在生产环境中,可能要管理数百个组件和千万级别的交互,为了提高效率,需要对组件进行拆分。

短生命周期变量的示例:

其中,发送消息和接收消息都用的短生命周期变量,例如,for 循环中用于存储 Message 的变量。

为了实现这种策略,创建一个返回 CubeSat 标识符的函数。函数的实现细节暂时不考虑,功能是和存储标志符的后端存储进行通信,如数据库。每当需要与卫星通信时,创建一个新的对象,这样就不需要在整个程序运行期间维护 CubeSat 标识符。此外,可以将短生命周期变量的所有权直接转移给其他函数。

代码语言:javascript
复制
fn fetch_sat_ids() -> Vec<u64> { ①
  vec![1,2,3]
}
  1. 返回 CubeSat 标志符向量

同时,为 GroundStation 创建一个方法,按需创建 CubeSat 实例:

代码语言:javascript
复制
impl GroundStation {
  fn connect(&self, sat_id: u64) -> CubeSat {
    CubeSat {
      id: sat_id,
      mailbox: Mailbox {
        messages: vec![]
      }
    }
  }
}

现在,主函数的代码看起来像下面这样:

代码语言:javascript
复制
fn main() {
  let base = GroundStation();

  let sat_ids = fetch_sat_ids();

  for sat_id in sat_ids {
    let mut sat = base.connect(sat_id);

    base.send(&mut sat, Message::from("hello"));
  }
}

现在还有个问题,CubeSat 实例在 for 循环结束时被销毁,包括基地(base)发送给它们的消息(Message)。为了实现短生命周期的设计,需要将这些信息存储在 CubeSat 实例之外的某个地方。在真实环境中,会被存储在设备的内存中。在模拟示例中,将返回的消息存放在程序整个生命周期内都可用的缓冲对象中。

消息使用 Vec<Message>,也就是本章定义的 Mailbox 类型,接下来,为 Message 结构添加发件人收件人字段,这样,CubeSat 实例就可以根据它们匹配的 ID 来接收消息。

代码语言:javascript
复制
#[derive(Debug)]
struct Mailbox {
  messages: Vec<Message>,
}
#[derive(Debug)]
struct Message {
    to: u64,
    content: String,
}

接下来,还需要重新实现发送和接收信息的函数。目前,CubeSat 对象可以访问自己的 Mailbox 对象。地面站中心也有能力发送带有 Mailbox 的消息,这里需要修改,因为每个对象只能存在一个可变的借用。在下面的示例代码中,Mailbox 实例可以修改 Message 向量,当卫星发送消息时,都会得到 Mailbox 的可变借用,然后,调用 Mailbox 的 deliver 函数接收消息。根据 API,可以直接调用 Mailbox 的方法,只是不能访问局部变量。

代码语言:javascript
复制
impl GroundStation {
    fn send(&self, mailbox: &mut Mailbox, to: &CubeSat, msg: Message) { // <1>
        mailbox.post(to, msg);
    }
}

impl CubeSat {
    fn recv(&self, mailbox: &mut Mailbox) -> Option<Message> {          // <2>
        mailbox.deliver(&self)
    }
}

impl Mailbox {
    fn post(&mut self, msg: Message) {                                  // <3>
        self.messages.push(msg);
    }

    fn deliver(&mut self, recipient: &CubeSat) -> Option<Message> {     // <4>
        for i in 0..self.messages.len() {
            if self.messages[i].to == recipient.id {
                let msg = self.messages.remove(i);                      // <5>
                return Some(msg);                                       // <6>
            }
        }

        None                                                            // <7>
    }
}

  1. 发送消息修改为对 Mailbox.post() 的调用,产生了消息的所有权
  2. 接收邮件改对 Mailbox.deliver() 方法的调用,获得 Mailbox 的所有权
  3. Mailbox.post( ) 需要对 self 进行可变访问,并获得 Message 的所有权
  4. Mailbox.deliver() 需要对 CubeSat 的共享引用,以获取 id 字段
  5. 这里有一个和之前用法不同的地方:在迭代集合的过程中对其进行修改,在这里是合法的,因为 self.messages.remove() 的下一行就是 return,编译器会保证下一次迭代不会发生,并允许这次迭代执行完
  6. 找到消息时,根据 Option 类型,用 Some 包裹 Message 并提前 return
  7. 如果没有找到 Message,返回 None

有了这些基础工作,接下来就可以实现完整的程序了,以下是代码:

代码语言:javascript
复制
#![allow(unused_variables)]

#[derive(Debug)]
struct CubeSat {
  id: u64,
}

#[derive(Debug)]
struct Mailbox {
  messages: Vec<Message>,
}

#[derive(Debug)]
struct Message {
    to: u64,
    content: String,
}

struct GroundStation {}

impl Mailbox {
    fn post(&mut self, msg: Message) {
        self.messages.push(msg);
    }

    fn deliver(&mut self, recipient: &CubeSat) -> Option<Message> {
        for i in 0..self.messages.len() {
            if self.messages[i].to == recipient.id {
                let msg = self.messages.remove(i);
                return Some(msg);
            }
        }

        None
    }
}

impl GroundStation {
    fn connect(&self, sat_id: u64) -> CubeSat {
        CubeSat {
            id: sat_id,
        }
    }

    fn send(&self, mailbox: &mut Mailbox, msg: Message) {
        mailbox.post(msg);
    }
}

impl CubeSat {
    fn recv(&self, mailbox: &mut Mailbox) -> Option<Message> {
        mailbox.deliver(&self)
    }
}

fn fetch_sat_ids() -> Vec<u64> {
  vec![1,2,3]
}

fn main() {
  let mut mail = Mailbox { messages: vec![] };

  let base = GroundStation {};

  let sat_ids = fetch_sat_ids();

  for sat_id in sat_ids {
    let sat = base.connect(sat_id);
    let msg = Message { to: sat_id, content: String::from("hello") };
    base.send(&mut mail, msg);
  }

  let sat_ids = fetch_sat_ids();

  for sat_id in sat_ids {
    let sat = base.connect(sat_id);

    let msg = sat.recv(&mut mail);
    println!("{:?}: {:?}", sat, msg);
  }
}

运行结果:

  • 复制值

如果每个对象都有所有者,需要要对软件进行大量的前期规划或重构,一个替代的方案是复制值。通常不推荐这样做,但在某些情况下也是需要的。原始类型(例如:整数)是个很好的例子。对于 CPU 来说,原始类型很容易复制,还不会带来额外开销,在 Rust 中,如果担心所有权被转移的话,可以使用复制。

Rust 中的类型有两种被复制的模式 Clone 和 Copy。当所有权被转移时,Copy 就会隐式地起作用,例如:复制对象 a 的 bit 内容以创建对象 b。Clone 的作用是显式的,实现 Clone 的类型有一个 .Clone() 方法,允许执行创建新类型所需的操作。

Clone 和 Copy 的区别:

为什么 Rust 程序员有时不使用 Copy 呢?要实现 Copy,类型必须实现 Copy 方法,整数和浮点数默认实现了 Copy,而 String 和许多其他类型,如 Vec<T>,都没有实现。没实现的原因也很简单,在复制时,只会复制这些类型的内部指针,而不是这些指针所指向的数据,这可能导致多个指针指向相同数据的情况,编译器难以判断。

(1)实现 Copy

以下是原始示例代码的部分内容:

代码语言:javascript
复制
#[derive(Debug)]
struct CubeSat {
  id: u64,
}

#[derive(Debug)]
enum StatusMessage {
  Ok,
}

fn check_status(sat_id: CubeSat) -> StatusMessage {
  StatusMessage::Ok
}

fn main() {
  let sat_a = CubeSat { id: 0 };

  let a_status = check_status(sat_a);
  println!("a: {:?}", a_status);

  let a_status = check_status(sat_a); // <1>
  println!("a: {:?}", a_status);
}

  1. 第二次调用 check_status(sat_a) 会出错

程序由一些类型组成,这些类型包含自己实现的 Copy 方法,因此可以简单直接地实现:

代码语言:javascript
复制
#[derive(Copy,Clone,Debug)] // <1>
struct CubeSat {
  id: u64,
}
#[derive(Copy,Clone,Debug)] // <1>
enum StatusMessage {
  Ok,
}

  1. 添加 #[derive(Copy)] 告诉编译器要自动添加 Copy 实现

也可以手动实现 Copy 方法,impl 代码块如下:

代码语言:javascript
复制
impl Copy for CubeSat { }

impl Copy for StatusMessage { }

impl Clone for CubeSat { // <1>
  fn clone(&self) -> Self {
    CubeSat { id: self.id } // <2>
  }
}

impl Clone for StatusMessage {
  fn clone(&self) -> Self {
    *self // <3>
  }
}

  1. 实现 Copy 需要实现 Clone
  2. 如果需要,可以自己写创建新对象的过程
  3. 通常,简单地取消对 self 的引用

(2)使用 Clone 和 Copy

已经实现了 Clone 和 Copy,接下来看看如何使用。前面已经讨论过,Copy 是隐式的,每当转移所有权(例如赋值或传递函数参数)时,数据被复制。Clone 需要明确调用.clone()。

在某些情况下,这是非常有用的,以下是示例:

代码语言:javascript
复制
#[derive(Debug,Clone,Copy)] // <1>
struct CubeSat {
  id: u64,
}

#[derive(Debug,Clone,Copy)] // <1>
enum StatusMessage {
  Ok,
}
fn check_status(sat_id: CubeSat) -> StatusMessage {
  StatusMessage::Ok
}

fn main () {
  let sat_a = CubeSat { id: 0 };

  let a_status = check_status(sat_a.clone()); // <2>
  println!("a: {:?}", a_status.clone());      // <2>

  let a_status = check_status(sat_a);         // <3>
  println!("a: {:?}", a_status);              // <3>
}

  1. Copy 意味着 Clone,这两种特征都可以使用
  2. 调用 .clone() 即可克隆对象
  3. Copy 也正常工作
  • 将数据包裹在特定类型中

还有一个常见的策略是使用“wrapper”类型,这些类型对外是 move 语义,但实际上是做一些特别的事情。

Rust 允许程序员选择加入运行时垃圾收集器,为了解释,需要引入新的符号。Rc<T> (Rc 是 Reference Counted 的缩写)表示引用计数类型 T,可以将 GroundStation 的实例包裹在 Rc 中,提供对每个卫星的共享访问,这涉及到对 Rc::new() 静态方法的调用。

以下是示例代码:

代码语言:javascript
复制
use std::rc::Rc; // <1>

#[derive(Debug)]
struct GroundStation {}

fn main() {
  let base: Rc<GroundStation> =
      Rc::new(GroundStation {}); // <2>

  println!("{:?}", base);
}

  1. use 关键字将标准库中的模块导入本地文件
  2. 调用 Rc::new() 时将 GroundStation 实例“wrapper”起来

Rc<T> 实现了 Clone 方法,每次调用 base.clone() 都会增加一个内部计数器,每次 Drop(生命周期结束)都会减少该计数器。当内部计数器减少到 0 时,释放原始实例。

Rc<T> 不允许被修改,为了实现修改功能,需要对“wrapper”再次封装,这就是 Rc<RefCell<T>> 类型。这种内部可变的对象在内部值被修改时对外是不可变的。

在以下示例代码中,尽管变量 base 被标记为不可修改,由于使用了 Rc<RefCell<T>> 类型,仍然能够对其修改,可以通过查看内部 base.radio_freq 值的变化进行验证。

代码语言:javascript
复制
use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct GroundStation {
  radio_freq: f64 // Mhz
}

fn main() {
  let base: Rc<RefCell<GroundStation>> = Rc::new(RefCell::new(
    GroundStation {
      radio_freq: 87.65
    }
  ));

  println!("base: {:?}", base);

  { // introduce a new scope             // <1>
    let mut base_2 = base.borrow_mut();
    base_2.radio_freq -= 12.34;
    println!("base_2: {:?}", base_2);
  }

  println!("base: {:?}", base);

  let mut base_3 = base.borrow_mut();    // <2>
  base_3.radio_freq += 43.21;

  println!("base: {:?}", base);          // <3>
  println!("base_3: {:?}", base_3);
}

  1. 括号表示一个代码片段,在这个片段中,base 首先被借用给 base_2,在代码片段结束时,base_2 的生命周期结束
  2. 由于 base_2 的生命周期已经结束,可以被再次借用
  3. 此时,base 已经被借用给 base_2,因此,再次打印 base 时会显示已被借用

运行结果:

value: borrowed 表示 base 已被其他地方以可修改的方式借用,不再是一般意义上的访问。

为类型中添加更多功能(例如:引用计数而非移动语义)会降低其运行时的性能。当实现 Clone 的成本过高时,使用 Rc<T> 会很方便。

⚠️ 注意:Rc<T> 不是线程级安全的,要保证原子性,可以使用 Arc<T> 替换 Rc<T>,用 Arc<Mutex<T> 替换 Rc<RefCell<T>,Arc 代表原子计数器。

参考资料

[1]

洛克: https://en.wikipedia.org/wiki/John_Locke

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-08-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 abin在路上 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 实现模拟的 CubeSats 卫星地面站
  • 2. 图示
  • 3. 所有者是指什么?它们的职责?
  • 4. 如何转移所有权
  • 5. 解决所有权问题
    • 参考资料
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档