👈 14 并发
顾及不同类型值的 trait 对象
定义通用行为的 trait
设想一个 GUI 程序,要遍历一个存储了各种 UI 对象(如 Button
和 TextField
)的列表,并对每一项调用 draw()
打印在屏幕上。
在其他面向对象语言中,可能有一个超类 Component
,它定义了 draw()
,各种 UI 对象类继承自 Component
。
要在 Rust 中实现,可以定义一个 Draw
trait,并将各种实现了 Draw
的对象存储在一个可以存放 trait 对象 的 vector。
定义 Draw
trait:
pub trait Draw {
fn draw(&self);
}
再定义一个结构体 Screen
,其成员变量 components
是一个 vector,存储的类型为 Box<dyn Draw>
,后者是一个 trait 对象,可以指代任何实现了 Draw
trait 的类型:
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
为 Screen
实现 run()
,对 components
中的每个变量调用 draw()
:
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
对比 trait 对象与泛型类型参数(下为泛型类型参数的镜像实现):
pub trait Draw {
fn draw(&self);
}
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
对于泛型类型参数实现,components
只能存储一种类型的变量(编译时对 T
采用具体类型进行单态化),而 trait 对象则实现了“真正的动态”。
实现 trait
定义一个结构体 Button
表示一个 UI 按钮,并对其实现 Draw
trait:
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// 伪实现
println!("a Button of {}*{}: {}", self.width, self.height, self.label)
}
}
再定义一个表示勾选框的 CheckBox
,也实现 Draw
trait:
pub struct CheckBox {
pub checked: bool,
}
impl Draw for CheckBox {
fn draw(&self) {
// 伪实现
println!("{} CheckBox", if self.checked { "a checked" } else { "an unchecked" })
}
}
一切准备就绪,在 main()
中创建一个新的 Screen
实例,其包含两个 Draw
trait 对象——一个 Button
和一个 CheckBox
——然后对实例调用 run()
:
fn main() {
let screen = Screen {
components: vec![
Box::new(
Button {
width: 50,
height: 50,
label: String::from("🦀"),
}
),
Box::new(
CheckBox {
checked: true,
}
),
]
};
screen.run();
}
a Button of 50*50: 🦀
a checked CheckBox
trait 对象执行动态分发
再次对比 trait 对象与泛型类型参数:
- 泛型类型参数:编译时
T
被单态化处理,生成静态类型代码,称为 静态分发(static dispatch)。 - trait 对象:编译时无法得知具体的类型,生成在运行时确定具体方法调用的代码,称为 动态分发(dynamic dispatch)。
面向对象设计模式的实现
状态模式(state pattern) 是一种面向对象设计模式,它定义一系列值的内含状态。这些状态体现为一系列的 状态对象,同时值的行为随着其内部状态而改变。
每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态。
状态模式的优点:业务需求改变时,无需改变值持有状态或使用值的代码,只需更新某个状态对象中的代码以改变其规则,或者是增加更多的状态对象。
下面实现一个博文发布的功能:
- 博文从空白的草案开始。
- 一旦草案完成,请求审核博文。
- 一旦博文过审,它将被发表。
- 只有被发表的博文的内容会被打印。
工作流大致如下:
let mut post = Post::new();
post.add_text("some content");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("some content", post.content());
还未实现的 Post
会使用状态模式并存储处于 3 种博文可处于的状态(草案,等待审核,已发布)之一,用户可对 Post
实例调用相应的方法以改变其状态。
定义 Post
并新建一个草案状态的实例
定义私有 trait State
,以及博文结构体 Post
:
trait State {}
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
再创建 3 个状态类,并分别实现 State
:
struct Draft {}
impl State for Draft {}
struct PendingReview {}
impl State for PendingReview {}
struct Published {}
impl State for Published {}
现在,为 Post
实现 new()
,新建一个 Draft
以确保任何博文都从草稿开始:
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
}
存放博文内容的文本
为 Post
实现 add_text()
:
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
这个方法不是状态模式的一部分,而是功能性方法。
确保博文草案的内容是空的
成员变量 content
是私有的,实现 content()
以进行访问:
pub fn content(&self) -> &str {
""
}
设计上希望仅在博文过审后才显示其内容,因此即使调用了 add_text()
,草稿中的内容也不该被返回。此处直接硬返回一个空串,稍后再补充真正的实现。
请求审核博文来改变其状态
为 State
trait 增加 request_review()
:
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
}
不同于 self
/ &self
/ &mut self
,这里使用了 self: Box<Self>
,意味着 request_review()
只能在持有这个类型的 Box
上被调用。self
获取了 Box<Self>
的所有权,使旧的状态失效,以便 Post
转换到新状态。
为 Post
实现 request_review()
:
impl Post {
// ...
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
}
request_review()
获取 Post
的可变引用,并在其当前状态下调用内部的 request_review()
,后者消费当前状态并返回一个新状态。用 if let
解构 take()
的返回值,后者获取了旧状态的所有权,以使其失效。
再为 Draft
和 PendingReview
实现 request_review()
:
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
}
}
在 Draft
状态申请审核时,返回一个装箱的 PendingReview
实例,而如果继续申请审核,将返回自身,因为此时已经处于等待审核的状态。
无论目前处于哪个状态,都不影响 Post
中 request_review()
的实现,因为每个状态只需负责自身的规则。
增加改变 content
行为的 approve
方法
为 State
trait 增加 approve()
:
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
为 Post
实现 approve()
:
impl Post {
// ...
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
都非常类似于 request_review()
的实现,毕竟都是状态的转换。
再为 PendingReview
和 Published
实现 approve()
,当然,由于修改了 State
的定义,3 种状态都需要实现这两个方法:
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
现在,将 content()
的伪实现替换为真正的实现:
impl Post {
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self) // error
}
}
无法过编:
error[E0599]: no method named `content` found for reference `&Box<dyn State>` in the current scope
--> src\lib.rs:18:38
|
18 | self.state.as_ref().unwrap().content(self)
| ^^^^^^^ method not found in `&Box<dyn State>`
这里的 unwrap()
一定会返回 Some
而非 None
,因为实现时 Post
的所有方法都确保在返回时状态将有一个 Some
值,但是编译器无从知晓这个实际上安全的实现细节,它认为对一个 None
调用 content()
是危险的,属于“我们比编译器知道更多情况”。
要修复这个情况,可以为 State
增加一个方法 content()
,并默认实现返回一个空串:
trait State {
// ...
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
然后在 Published
中重写 content()
:
impl State for Published {
// ...
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
现在,所有代码都能如期工作了。
状态模式的权衡取舍
状态模式的优缺点:
- 优点:逻辑组织清晰,如要查找所有已发布博文的行为,只需查看 Published 的实现。
- 缺点:一些状态之间相互联系,如需在
PendingReview
与Published
之间增加一个Scheduled
状态,可能要改变两个状态。
将状态和行为编码为类型
以上是完全将其他面向对象语言的设计模式照搬进 Rust 的实现,下面反思其中的一些设计以使其更 Rusty。
此前,试图查看 Draft
状态的博文内容将得到一个空串:
trait State {
// ...
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
impl State for Draft {
// 未重写 content()
}
现在修改实现,在编译期就禁止获取 Draft
内容的尝试。
新增一个结构体 Draft
,根本不实现 content()
:
pub struct Draft {
content: String,
}
impl Draft {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
再修改 impl Post
块中 new()
和 content()
的实现:
impl Post {
pub fn new() -> Draft {
Draft {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
// ...
}
实现状态转移为不同类型的转换
实现 PendingReview
,PendingReview::approve()
将消费其自身并返回 Post
实例:
pub struct PendingReview {
content: String,
}
impl PendingReview {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}
同理,实现 Draft::request_review()
,消费其自身并返回 PendingReview
实例:
impl Draft {
// ...
pub fn request_review(self) -> PendingReview {
PendingReview {
content: self.content,
}
}
}
还需修改 main()
以适应上述修改:
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
let post = post.request_review();
let post = post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}