👈 14 并发

顾及不同类型值的 trait 对象

定义通用行为的 trait

设想一个 GUI 程序,要遍历一个存储了各种 UI 对象(如 ButtonTextField)的列表,并对每一项调用 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() 的返回值,后者获取了旧状态的所有权,以使其失效。

再为 DraftPendingReview 实现 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 实例,而如果继续申请审核,将返回自身,因为此时已经处于等待审核的状态。

无论目前处于哪个状态,都不影响 Postrequest_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() 的实现,毕竟都是状态的转换。

再为 PendingReviewPublished 实现 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 的实现。
  • 缺点:一些状态之间相互联系,如需在 PendingReviewPublished 之间增加一个 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
    }
    
    // ...
}

实现状态转移为不同类型的转换

实现 PendingReviewPendingReview::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());
}

👉 16 模式与模式匹配