目录
- 一、面向对象语言特征
- 1.1 对象包含数据和行为
- 1.2 封装
- 1.3 继承作为类型系统与代码共享
- 二、使用trait对象存储不同类型的值
- 2.1 定义共有行为的trait
- 2.2 实现trait
- 2.3 trait对象执行动态派发
- 2.4 trait对象必须保证对象安全
- 三、面向对象设计模式
- 四、状态模式的权衡取舍
- 五、将状态和行为编译为类型
一、面向对象语言特征
1.1 对象包含数据和行为
- 面向对象的程序是由对象组成的;
- 一个对象包含数据和操作这些数据的过程。这些过程通常被称为方法或操作。
- Rust对象是面向对象的
- 结构体和枚举包含数据;
- impl块提供了方法;
- 带有方法的struct,enum并没有被称为对象;
1.2 封装
- 封装: 对象的实现细节不能被使用对象的代码获取,唯一与对象交互的方式是通过对象提供的公有API;
- Rust里可以使用
pub
关键字来决定模块、类型、函数和方法是公有的(默认情况下一切都是私有的);
pub struct AveragedCollection {list: Vec<i32>,average: f64,
}impl AveragedCollection {pub fn add(&mut self, value: i32) {self.list.push(value);self.update_average();}pub fn remove(&mut self) -> Option<i32> {let result = self.list.pop();match result {Some(value) => {self.update_average();Some(value)},None => None,}}pub fn average(&self) -> f64 {self.average}fn update_average(&mut self) {let total: i32 = self.list.iter().sum();self.average = total as f64 / self.list.len() as f64;}
}
AveragedCollection
结构体是公有的,结构体内部的字段仍然是私有的;- 公有方法add、remove是修改AveragedCollection实例的唯一方式;
- 已经封装好
AveragedCollection
的实现细节,可以轻松改变类似数据结构的内容; - 例如使用
HashSet<i32>
代替Vec<i32>
成为list字段的类型;
1.3 继承作为类型系统与代码共享
- Rust中无法定义一个结构体继承父结构体的成员和方法,因此没有继承的概念;
- Rust可以使用默认trait方法实现来进行代码共享;
- Rust可以使用泛型和trait约束实现多态;
二、使用trait对象存储不同类型的值
- 创建GUI工具:遍历某个元素的列表,依次调用元素的draw方法进行绘制;
2.1 定义共有行为的trait
- Rust避免将struct和enum称为对象,因此它们与impl是分开的;
- trait对象有些类似于其它语言中的对象,它们某种程度上组合了数据与行为;
- 无法为trait对象添加数据;
- trait对象被专门用于抽象某些共有行为,没有其它语言的对象那么通用;
pub trait Draw {fn draw(&self);
}pub struct Screen {pub components: Vec<Box<dyn Draw>>,
}impl Screen {pub fn run(&self) {for component in self.components.iter() {component.draw();}}
}
- 定义了一个Draw train,内含方法
draw
; - 定义结构体Screen,内有公共元素components,类型为Vector,里面放了
Box<dyn Draw>
,表示Box里的元素都实现了Draw的trait; - 可以用泛型表示;
pub struct Screen<T: Draw> {pub components: Vec<T>,
}
- 泛型只能放入一种类型,但是
Vec<Box<dyn Draw>>
可以放所有实现了Draw的trait;
2.2 实现trait
- 提供
Button
类型,实现Draw trait;
pub struct Button {pub width: u32,pub height: u32,pub label: String,
}impl Draw for Button {fn draw(&self) {// 实际绘制按钮的代码}
}
- Button 上的 width、height 和 label 字段会和其他组件不同;
- 上述代码都在lib.rs中,下面这个在main.rs
use testrust::Draw;
use testrust::{Screen, Button};struct SelectBox {width: u32,height: u32,options: Vec<String>,
}impl Draw for SelectBox {fn draw(&self) {// code to actually draw a select box}
}
- 创建SelectBox结构体,并且也实现了Draw trait;
- 在main函数中创建一个Screen实例;
- 可以通过将
SelectBox
和Button
放入Box<T>
转变为trait对象来增加组件; - 接着调用
Screen
的 run 方法,它会调用每个组件的 draw 方法;
fn main() {let screen = Screen {components: vec![Box::new(SelectBox {width: 75,height: 10,options: vec![String::from("Yes"),String::from("Maybe"),String::from("No")],}),Box::new(Button {width: 50,height: 10,label: String::from("OK"),}),],};screen.run();
}
- 实现了draw的结构体都可以添加到Vector中;
- 最后调用run进行绘制;
2.3 trait对象执行动态派发
- 对泛型使用trait bound时编译器所进行单态化处理
- 编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。
- 单态化所产生的代码进行静态分发(static dispatch),静态分发发生于编译器在编译时就知晓调用了什么方法;
- 动态分发(dynamic dispatch) 无法在编译过程中确定调用的方法,编译器会生成在运行时确定调用了什么方法的代码;
- trait对象执行动态派发
- 会产生运行时开销;
- 阻止编译器内联方法代码,使得部分优化操作无法进行;
2.4 trait对象必须保证对象安全
- 只有对象安全(object safe) 的 trait 才可以组成 trait 对象;
- trait中所有的方法有如下属性时,可以被认为是对象安全的:
- 返回值类型不为Self;
- 方法没有任何泛型类型参数;
- 标准库中的Clone trait就不是对象安全的;
pub trait Clone {fn clone(&self) -> Self;
}
三、面向对象设计模式
- 状态模式(state pattern) 是一个面向对象设计模式;
- 一个值有某些内部状态,体现为一系列的状态对象,同时值的行为随着其内部状态而改变。
- 使用状态模式时
- 当程序的业务需求改变时,无需修改保存状态值的代码或使用值的代码;
- 只需更新某个状态对象内部的代码,即可改变其规则,也可以增加更多的状态对象;
- 一个增量式的发布博文的工作流:
- 博文从空白的草案开始;
- 一旦草案完成,请求审核博文;
- 一旦博文过审,它将被发表;
- 只有被发表的博文的内容会被打印;
主逻辑
只有main函数位于main.rs中,其它皆位于lib.rs
use blog::Post;fn main() {let mut post = Post::new();post.add_text("I ate a salad for lunch today");assert_eq!("", post.content());post.request_review();assert_eq!("", post.content());post.approve();assert_eq!("I ate a salad for lunch today", post.content());
}
- 创建Post,然后使用
add_text
函数添加内容; - 接着使用
request_review
函数进行审批; - 最后使用
approve
函数进行审批; - 审批通过后可以使用
content
函数查看文件内容;
定义状态与添加/获取内容
- 与crate交叉的唯一的类型是Post;
- Post存放着博文当前所处的三种状态:草案(Draft),等待审核(PendingReview) 和 发布(Published) ;
- 定义Post结构体和new函数;
pub struct Post {state: Option<Box<dyn State>>,content: String,
}impl Post {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 {""}
}trait State{}struct Draft{}impl State for Draft {}
- Post结构体里放实现了State的类型
- 新建的博文状态(state)是个草案(Box::new(Draft {})),内容是空的(String::new()),博文的初始状态是私有的,因此外部无法改变其值;
State trait
定义了所有不同状态的博文所共享的行为,同时三种状态都会实现State
状态;- 通过Post的
add_text
方法修改博客内容; - 由于博客还是草案状态,因此返回的博客内容(
content函数
)应该是空的;
状态修改
- 增加博文的审核功能;
- 将其状态由
Draft
改为PendingReview
;
impl Post {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}
}
- Post中增加
request_review
方法,调用状态内部的同名方法,并返回一个新状态,目前草案状态调整为待审批状态、待审批状态则返回自身; - State trait中的
request_review
方法参数表示该方法只可在持有这个类型的Box上被调用,这个语法获取了Box<Self>
的所有权使旧状态无效,以便Post的状态值可转移为一个新状态; self.state.take()
指将state字符中的some值取出使之成为 None,然后重新赋值;
增加审批方法
审批( request_review)方法会将state设置为发布状态;
impl Post {// --snip--pub fn approve(&mut self) {if let Some(s) = self.state.take() {self.state = Some(s.approve())}}
}trait State {fn request_review(self: Box<Self>) -> Box<dyn State>;fn approve(self: Box<Self>) -> Box<dyn State>;
}struct Draft {}impl State for Draft {// --snip--fn approve(self: Box<Self>) -> Box<dyn State> {self}
}struct PendingReview {}impl State for PendingReview {// --snip--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}
}
- 代码逻辑基本和前面的相同;
- 不同的状态只需要维护自己的代码即可;
更新Post的content方法: 如果状态为 Published则返回博文 content 字段的值;否则返回空字符串切片;
impl Post {pub fn content(&self) -> &str {self.state.as_ref().unwrap().content(self)}
}
trait State {fn content<'a>(&self, post: &'a Post) -> &'a str {""}
}struct Published {}impl State for Published {fn content<'a>(&self, post: &'a Post) -> &'a str {&post.content}
}
- Post的content中调用Option的as_ref方法是需要Option中值的引用而并非所有权;
四、状态模式的权衡取舍
- 缺点在于某些状态之间是相互耦合的且需要重复实现一些逻辑代码;
五、将状态和行为编译为类型
- Rust类型检查系统会通过编译时错误来阻止用户使用无效状态;
pub struct Post {content: String,
}pub struct DraftPost {content: String,
}impl Post {pub fn new() -> DraftPost {DraftPost {content: String::new(),}}pub fn content(&self) -> &str {&self.content}
}impl DraftPost {pub fn add_text(&mut self, text: &str) {self.content.push_str(text);}pub fn request_review(self) -> PendingReviewPost {PendingReviewPost {content: self.content,}}
}pub struct PendingReviewPost {content: String,
}impl PendingReviewPost {pub fn approve(self) -> Post {Post {content: self.content,}}
}
- Post指发布成功之后的,DraftPost就是草稿;
- 当new一个Post时,返回的就是空草稿;
- 草稿可以添加博文,可以请求审批,请求审批返回待审批的Post(PendingReviewPost );
- 待审批的Post只有一个
approve
方法,因此它只需要审批,审批之后返回发布成功的Post; - 因此不同的Post只能做相应的事情,没有多余的方法;
main方法
use blog::Post;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());
}
- 从main函数看出来流程:创建草稿,添加内容,请求审核,获取审批通过的内容;