问题的起始
最近在看《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
模块中的 Cell
和 RefCell
是实现内部可变性的容器,在保持容器不被 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_review
将 move
分配在堆上的 Box<self>
,而 Rust 不允许你将值从堆中 move
,除非你实现 Copy
,否则原值还剩下什么呢?The book 里面使用 Option::take()
将所有权移出,并为原值保留了 None
。
其实已经讲得很清楚了,但我还要问一句:move
发生在哪里?
self.state = self.state.request_review();
这一句中的 self.state.request_review();
使得结构体 Post
的 state
字段被 request_review()
方法赋了新值,并取得了 state
的所有权,此时 self.state
的所有权转移进了 request_review()
方法内,self.state
变成未初始化状态,而 Rust 编译器禁止使用未经初始化的变量,因为这会产生未定义行为。
因此编译器报错,还指出了我们没有为 std::boxed::Box<dyn State>
实现 Copy
trait,如果实现了这个 trait,self.state
在 self.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()
这一步时 move
了 self.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()) } } }
人家的 state
用 Option
给包住了,而且取值和赋值是分开写的,没有写在一行里,从 take
的签名:pub fn take(&mut self) -> Option<T>
中看出 take
用的是可变借用而不是 self
,也就没有 move
了,同时生命周期推断也能发挥作用,当然就规避掉了这些问题。
问题解决!感谢这些在网上无私分享知识的人们!