2022
/ 第 ⁨1⁩ 篇

记一次 Rust 探险

2022-04-17 avatar whx

问题的起始

最近在看《The Book》,第 17 章的 17.3 节里面有这样一个细节:

……

请求审核博文来改变其状态

接下来需要增加请求审核博文的功能,这应当将其状态由 Draft 改为 PendingReview。示例 17-15 展示了这段代码:

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
   // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

……

我们需要将 state 临时设置为 None 来获取 state 值,即老状态的所有权,而不是使用 self.state = self.state.request_review(); 这样的代码直接更新状态值。这确保了当 Post 被转换为新状态后不能再使用老 state 值。

……

这里让我非常困惑,我认为并不是这个原因,因此找了不少解释,以下是总结。

Box 的性质

首先来看可变性。
在 Rust 中,一个变量是否可变,取决于是否用 mut 修饰变量绑定。

如果我们用 let var : T 声明,那么 var 是不可变的;而且 var 内部所有的成员也都是不可变的;
如果我们用 let mut var : T 声明,那么 var 是可变的,相应的它的内部所有成员也都是可变的。
这样的话,如果有个结构体引用 &SomeStruct,则 SomeStruct 的所有字段都是不可变的。
但在实际开发中,确实存在需要结构体中的某个字段可变的情况。针对这种情况,Rust 的标准库中有个 std::cell 模块,通过共享的可变容器允许以受控的方式进行可变性操作。

std::cell 模块中的 CellRefCell 是实现内部可变性的容器,在保持容器不被 drop 的情况下可以修改其中的值;而 Box 虽然一直都被拿来和前两个一起讨论,但 Box 并没在 std::cell 模块中,它对可变性其实没啥限定。
Box 是一个指针,有所有权和生命周期,指向堆上的某个位置,和普通的指针不同的是,它独占了对数据的所有权;
Cell 不是一个指针,Cell<T> 只是把数据 T 包装一下,告诉你它有“内部可变性”,数据还是那些数据。

其次来看所有权规则。
Box<T> 不是 Copy 类型,因为 Box 拥有分配在堆上的缓冲区,非 Copy 类型如果采用逐位复制,那么会使编译器无法分辨哪个值需要对被引用的原始资源负责。

性质已经搞清,接下来我们来看具体问题。

为什么直接赋值不行

我在 stackoverflow 上找到一个有关的问答,回答者把它换成了直接版本:

pub struct Post {
    state: Box<dyn State>,
    content: String,
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>; 
}

impl Post {
    // ... 
    pub fn request_review(&mut self) {
        self.state = self.state.request_review();
    }
    // ... 
}

编译器报错:

self.state = self.state.request_review();
             ^^^^^^ move occurs because `self.state` has type `std::boxed::Box<dyn State>`, which does not implement the `Copy` trait'.

原回答:
This is because calling State::request_review will move Box<self>, which is allocated on heap, and Rust doesn’t allow you to just move values away from heap unless you implement Copy, otherwise what’s left there? The book uses Option::take() to move ownership out and leave None on the place.

翻译如下:
这是因为调用 State::request_reviewmove 分配在堆上的 Box<self>,而 Rust 不允许你将值从堆中 move,除非你实现 Copy,否则原值还剩下什么呢?The book 里面使用 Option::take() 将所有权移出,并为原值保留了 None

其实已经讲得很清楚了,但我还要问一句:move 发生在哪里?

self.state = self.state.request_review();

这一句中的 self.state.request_review(); 使得结构体 Poststate 字段被 request_review() 方法赋了新值,并取得了 state 的所有权,此时 self.state 的所有权转移进了 request_review() 方法内,self.state 变成未初始化状态,而 Rust 编译器禁止使用未经初始化的变量,因为这会产生未定义行为。
因此编译器报错,还指出了我们没有为 std::boxed::Box<dyn State> 实现 Copy trait,如果实现了这个 trait,self.stateself.state.request_review() 之后依然能保持初始化状态,也就可以完成 self.state = 这部分的赋值了。

2023.2.16 追加

再读《The Book》,想到自己还写过这么一篇文章,回来看的时候发现有个地方没说透:

Rust 不允许你将值从堆中 move,除非你实现 Copy

这句话什么意思?还是要看报错信息

   Compiling playground v0.0.1 (/playground)
error[E0507]: cannot move out of `self.state` which is behind a mutable reference
  --> src/lib.rs:12:22
   |
12 |         self.state = self.state.request_review();
   |                      ^^^^^^^^^^ ---------------- `self.state` moved due to this method call
   |                      |
   |                      move occurs because `self.state` has type `Box<dyn State>`, which does not implement the `Copy` trait
   |
note: this function takes ownership of the receiver `self`, which moves `self.state`
  --> src/lib.rs:7:23
   |
7  |     fn request_review(self: Box<Self>) -> Box<dyn State>; 
   |                       ^^^^

For more information about this error, try `rustc --explain E0507`.
error: could not compile `playground` due to previous error

我们的操作会 cannot move out of *self.state* which is behind a mutable reference,也就是说会在可变引用时 move 变量(转移所有权)。
看一下函数签名: fn request_review(&mut self),果然使用的是 Post 的可变引用(也就是 &mut self),然后在 self.state.request_review() 这一步时 moveself.state,如果 self.state 没有实现 Copy 这个 trait,那么根据 Rust 规则,使用可变引用时是不能 move 的,一旦 move 了,对象的内存地址改变了,那引用还有效吗?

  • 从借用规则角度看,可变引用是「独占引用」,也即在存在可变引用的时候是不能其他引用的,如果把值看成一种「可变引用」,那么很明显发生了冲突,之前的可变引用很可能就失效了。
  • 从所有权角度看,可以把可变引用看成持有一段时间的所有权的值的特殊状态,这段时间内原值是不拥有所有权的,所以原值也没有能力去 move
  • 实现了 Copy trait 意味着不会真的转移所有权,而是会原封不动地复制一份出去,不会对原值有影响,因此引用就还会是有效状态。

说了这么多,让我看看书里的操作是什么:

impl Post {
    // --snip--
    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }
}

人家的 stateOption 给包住了,而且取值和赋值是分开写的,没有写在一行里,从 take 的签名:pub fn take(&mut self) -> Option<T> 中看出 take 用的是可变借用而不是 self,也就没有 move 了,同时生命周期推断也能发挥作用,当然就规避掉了这些问题。

问题解决!感谢这些在网上无私分享知识的人们!

参考阅读

  1. why do we need to call take() for Option<T> variable
  2. 所有权和移动
  3. 变量先声明
  4. 聊聊 Rust 中的所有权机制
  5. Rust 中几个智能指针的异同与使用场景
  6. 【Rust 每周一知】如何理解 Rust 中的可变与不可变?