面向对象编程(Object-Oriented Programming,OOP)
封装细节
main.rs
use rust_demo::AveragedCollection;fn main() {let mut ac = AveragedCollection::new();println!("ac={:?}", ac);ac.add(3);ac.add(5);ac.add(7);println!("ac={:?}", ac);
}
lib.rs
#[derive(Debug)]// 结构体公有
pub struct AveragedCollection {// 里面的内容私有list: Vec<i32>,average: f64,
}impl AveragedCollection {// 方法公有pub fn new() -> Self {AveragedCollection{list:Vec::new(),average:0.0,}}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;}
}
继承
如果一个语言必须有继承才能被称为面向对象语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承父结构体的成员和方法
Rust 也提供了其他的解决方案
选择继承有两个主要的原因:
(1)重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现,Rust 代码可以使用默认 trait 方法实现来进行共享
(2)使用继承的原因与类型系统有关:子类型可以用于父类型被使用的地方,多态(polymorphism)
rust使用 trait 对象而不是继承
近来继承作为一种语言设计的解决方案在很多语言中失宠:共享多于所需的代码风险,子类不应总是共享其父类的所有特征
示例背景
以GUI库接口为例:通过遍历列表并调用每一个项目的 draw 方法来将其绘制到屏幕上
在拥有继承的语言中,可以定义一个名为 Component 的类,该类上有一个 draw 方法。其他的类比如 Button、Image 和 SelectBox 会从 Component 派生并因此继承 draw 方法。它们各自都可以覆盖 draw 方法来定义自己的行为
Rust的实现方式:
定义一个 Draw trait,其中包含名为 draw 的方法。
定义一个存放 trait 对象(trait object) 的 vector。
trait 对象指向一个实现了指定 trait 的类型的实例,以及一个用于在运行时查找该类型的 trait 方法的表
库实现
trait 对象不同于传统的对象,因为不能向 trait 对象增加数据
trait 对象并不像其他语言中的对象那么通用:
其(trait 对象)具体的作用是允许对通用行为进行抽象
pub trait Draw {fn draw(&self);
}pub struct Screen {// vector 的类型是 Box<dyn Draw>,为一个 trait 对象// 它是 Box 中任何实现了 Draw trait 的类型的替身pub components: Vec<Box<dyn Draw>>,
}impl Screen {pub fn run(&self) {// 模拟GUI的渲染for component in self.components.iter() {component.draw();}}
}// 通用库中实现具体结构体和对应的Draw Trait
pub struct Button {pub width: u32,pub height: u32,pub label: String,
}impl Draw for Button {fn draw(&self) {// 实际绘制按钮的代码}
}
main实现
main中可以增加其他需要参与渲染的特制化的结构体
use gui::Draw;
use gui::{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}
}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();
}
Screen 实例必须拥有一个全是 Button 类型或者全是TextField 类型的组件列表
和泛型类型参数的区别
泛型类型参数一次只能替代一个具体类型,如果只需要同质(相同类型)集合,则倾向于使用泛型和 trait bound,其定义会在编译时采用具体类型进行单态化,即静态分发
trait 对象则允许在运行时替代多种具体类型,当使用 trait 对象时,Rust 必须使用动态分发
动态分发可以通过牺牲少量运行时性能来为你的代码提供一些灵活性
如下示例只能渲染vec{小猫1,小猫2,…},而不能渲染vec{小猫1,小狗2,…}
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();}}
}
Trait 对象要求对象安全
如果一个 trait 中所有的方法有如下属性时,则该 trait 是对象安全的:
(1)返回值类型不为 Self
(2)方法没有任何泛型类型参数
不是对象安全的例子:Clone trait
pub trait Clone {fn clone(&self) -> Self;
}
在 String 实例上调用 clone 方法时会得到一个 String 实例
当调用 Vec 实例的 clone 方法会得到一个 Vec 实例
可以理解为:trait对象需要Self,但是如果某个trait返回Selft,它可以修改泛型参数的类型/trait对象所指对象的方法等,导致trait对象无法用
pub struct Screen {pub components: Vec<Box<dyn Clone>>,
}
面向对象编程
一个增量式的发布博文的工作流
(1)博文从空白的草案开始。
(2)一旦草案完成,请求审核博文。
(3)一旦博文过审,它将被发表。
(4)只有被发表的博文的内容会被打印
状态模式
// cat main.rs
use rust_demo::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());
}
// cat lib.rs
// Post 的方法并不知道这些不同类型的行为:Draft、PendingReview 和 Published
pub struct Post {// state 字段是私有的state: Option<Box<dyn State>>,content: String,
}impl Post {pub fn new() -> Post {Post {// 博文初始状态为草案state: Some(Box::new(Draft {})),content: String::new(),}}// 获取一个 self 的可变引用,通过该方法改变Post实例pub fn add_text(&mut self, text: &str) {self.content.push_str(text);}// 请求审核pub fn request_review(&mut self) {// 调用 take 方法将 state 字段中的 Some 值取出并留下一个 None// Rust 不允许结构体实例中存在值为空的字段,所以才要用Option<Box<dyn State>>类型if let Some(s) = self.state.take() {self.state = Some(s.request_review())}}// 审核通过// 将 state 设置为审核通过时应处于的状态pub fn approve(&mut self) {if let Some(s) = self.state.take() {self.state = Some(s.approve())}}// 读取文本接口pub fn content(&self) -> &str {// as_ref():需要 Option 中值的引用而不是获取其所有权// state 是一个 Option<Box<State>>,调用 as_ref 会返回一个 Option<&Box<State>>// unwrap,这里永远也不会 panic,状态图确保它返回时均是一个Some值// 当调用其 content 时,解引用强制转换会作用于 & 和 Box// 这里原来调用的是trait中的content方法self.state.as_ref().unwrap().content(self)// 改用下面方式实现// self类型:&rust_demo::Post// self.state类型:core::option::Option<alloc::boxed::Box<dyn rust_demo::State>>// curStatRef的类型:core::option::Option<&alloc::boxed::Box<dyn rust_demo::State>>// let curStatRef = self.state.as_ref();// innerWrap的类型:&alloc::boxed::Box<dyn rust_demo::State>// let innerWrap = curStatRef.unwrap();// info类型:&str// let info = innerWrap.content(self);// info}
}// State trait 定义了所有不同状态的博文所共享的行为
trait State {fn request_review(self: Box<Self>) -> Box<dyn State>;fn approve(self: Box<Self>) -> Box<dyn State>;fn content<'a>(&self, post: &'a Post) -> &'a str {// 传入进来的post的类型是&rust_demo::Post""}
}// Draft、PendingReview 和 Published 状态都会实现 State 状态
// 无论 state 是何值,Post 的 request_review 方法都是一样的。每个状态只负责它自己的规则
struct Draft {}impl State for Draft {// 状态流转// 该方法只可在持有这个类型的 Box 上被调用// 获取了 Box<Self> 的所有权使老状态无效化// 将 state 的值移出 Post 而不是借用它// 要将 state 临时设置为 None 来获取 state 值// 而不是使用 self.state = self.state.request_review()// 确保了当 Post 被转换为新状态后不能再使用老 state 值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 {// 状态流转// 该方法只可在持有这个类型的 Box 上被调用fn request_review(self: Box<Self>) -> Box<dyn State> {Box::new(Published {})}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 的引用作为参数,并返回 post 一部分的引用// 所以返回的引用的生命周期与 post 参数相关fn content<'a>(&self, post: &'a Post) -> &'a str {// 传入进来的post的类型是&rust_demo::Post&post.content}
}
缺点:
(1)状态实现了状态之间的转换,一些状态会相互联系:
如果在 PendingReview 和 Published 之间增加另一个状态,比如 Scheduled,
则不得不修改 PendingReview 中的代码来转移到 Scheduled
(2)重复的逻辑:
不同状态之间都需要实现trait的所有接口
Post 中 request_review 和 approve 这两个类似的实现。都委托调用了 state 字段中 Option 值的同一方法
解决办法
草案博文在可以发布之前必须被审核通过。
等待审核状态的博文应该仍然不会显示任何内容
// cat main.rs
use rust_demo::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());
}
// cat lib.rs
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);}// request_review 获取 self 的所有权,消费 DraftPost// 转换为 PendingReviewPost// 这样在调用 request_review 之后就不会遗留任何 DraftPost 实例pub fn request_review(self) -> PendingReviewPost {PendingReviewPost {content: self.content,}}
}pub struct PendingReviewPost {content: String,
}impl PendingReviewPost {// approve 获取 self 的所有权,消费 PendingReviewPost// 转换为 Post// 这样在调用 approve 之后就不会遗留任何 PendingReviewPost 实例pub fn approve(self) -> Post {Post {content: self.content,}}
}// Post 的 new -> DraftPost
// DraftPost 的 request_review -> PendingReviewPost
// PendingReviewPost 的approve -> Post
// 最终只需要Post打印即可
修改 main 来重新赋值 post 使得这个实现不再完全遵守面向对象的状态模式:
状态间的转换不再完全封装在 Post 实现中
得益于类型系统和编译时类型检查,得到无效状态是不可能的
上述的取舍真的牛逼!!!