29_rust_面向对象编程特性
面向对象编程特性
面向对象的特性
封装
rust默认情况下是私有的,使用pub关键字让方法和成员公开,访问也通过方法来实现。
继承
使对象可沿用另外一些对象的数据和行为。不过rust没有继承的概念,但rust通过默认trait方法来进行代码共享,也可使用同名方法覆盖原有实现。
多态
rust使用泛型和trait约束(限定参数化多态bounded parametric)实现了多态的概念。
例子:使用trait对象存储不同类型的值
需求:创建一个GUI 工具,会遍历某个元素的列表,依次调用元素的 draw 方法进行绘制,例如:Bufton、TextField 等元素。
如果在其他面向对象语言里,会定义一个 Component 父类,里面定义了 draw 方法,然后定义 Button、TextField 等类,继承与 Component 类。但rust语言无继承的功能,所以要采用其他方法。
为共有行为定义一个trait
rust避免将struct或enum称之为对象,因为它们与impl块分开的。
trait对象则有些类似于其它语言的对象,某种程度上组合了数据与行为。与传统对象不同的地方是无法为trait对象添加数据。
trait对象被专门用于抽象某些共有行为,没有其他语言的对象那么通用。
使用cargo new hello_cargo创建hello_cargo项目,
lib.rs的内容
pub trait Draw { // 公共trait,有一个draw方法
fn draw(&self);
}
pub struct Screen { // 用于存放所有实现了Draw trait的实例,这样就可统一调用draw方法了
pub comps: Vec<Box<dyn Draw>>, // 表示Box里元素都实现了Draw trait,只要实现了Draw trait的对象都可存入
}
impl Screen {
pub fn run(&self) {
for c in self.comps.iter() {
c.draw();
}
}
}
/*******泛型实现版本*******/
// 如果使用泛型替代Box的实现,一次只能存放一种类型,无法实现类似多态的功能
// pub struct Screen<T: Draw> { // 泛型实现
// pub comps: Vec<T>, // 如果T为Button类型,就只能存放Button这一种类型了,无法存放多种类型
// }
// impl<T> Screen<T>
// where
// T: Draw,
// {
// pub fn run(&self) {
// for c in self.comps.iter() {
// c.draw();
// }
// }
// }
pub struct Button {
pub w: u32,
pub h: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
println!("draw button: {},{},{}", self.label, self.w, self.h);
}
}
main.rs的内容
use hello_cargo::Draw;
use hello_cargo::{Button, Screen};
struct SelBox {
w: u32,
h: u32,
op: Vec<String>,
}
impl Draw for SelBox {
fn draw(&self) {
println!("draw select box: {:?}, {}, {}", self.op, self.w, self.h);
}
}
fn main() {
let sc = Screen {
comps: vec![ // 可存放多种类型
Box::new(SelBox {w: 1, h: 2, op: vec![String::from("t1"), String::from("t2")],}),
Box::new(Button {w: 2, h: 3, label: String::from("b1"),}),
],
};
sc.run(); // 执行run可调用各自的draw方法
}
/*运行结果
draw select box: ["t1", "t2"], 1, 2
draw button: b1,2,3
*/
trait对象执行的是动态派发。
将trait约束作用于泛型时,rust编译器会执行单态化,编译器会为用来替换泛型类型参数的每个具体类型,生成对应函数和方法的非泛型实现,简单理解就是用了哪种类型,就用哪种类型替换T。通过单态化生成的代码会执行静态派发(static dispatch),在编译过程中确定调用的具体方法。
动态派发(dynamic dispatch):无法在编译过程中确定调用的究竟是哪一种方法,编译器会产生额外的代码(虚表指针)以便在运行时动态寻找对应方法入口。使用trait对象,会执行动态派发,将产生一定的运行时开销,且无法内联优化等优化动作。
Trait对象必须保证对象安全,只能把满足对象安全(object-safe)的trait转化为tait对象,rust采用了一系列规则来判定某个对象是否安全,其中两条:
- 方法的返回类型不是self
- 方法中不包含任何泛型类型参数
面向对象的一种模式:状态模式
状态模式(state pattern):一个值拥有的内部状态由数个状态对象(state object)表达而成,而值的行为则随着内部状态的改变而改变,使用状态模式意味着业务需求变化时,不需要修改持有状态的值的代码,或者使用这个值的代码,只需要更新状态对象内部的代码,以便改变其规则。或者增加一些新的状态对象。
lib.rs内容:
pub struct Post {
state: Option<Box<dyn State>>, // 三种状态,草稿、等待审批、审批通过
content: String,
}
impl Post {
pub fn new() -> Post { // 初始状态是Draft
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 {
self.state.as_ref().unwrap().content(&self)
}
pub fn req_review(&mut self) {
if let Some(s) = self.state.take() { // take()方法取出状态
self.state = Some(s.req_review()) // 这里调用Box<Self>类型参数的函数,s就是一个Box
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
// Box<Self>类型参数作用是为了让只能是Box包裹的实例所调用,普通实例无法调用,且会在调用过程中获取Box<Self>所有权,并使旧的状态失效
fn req_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, post: &'a Post) -> &'a str { "" }
}
struct Draft {}
impl State for Draft {
fn req_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 req_review(self: Box<Self>) -> Box<dyn State> { self }
fn approve(self: Box<Self>) -> Box<dyn State> { Box::new(Publish {}) }
}
struct Publish {}
impl State for Publish {
fn req_review(self: Box<Self>) -> Box<dyn State> { self }
fn approve(self: Box<Self>) -> Box<dyn State> { self }
fn content<'a>(&self, post: &'a Post) -> &'a str { &post.content }
}
main.rs
use hello_cargo::Post;
fn main() {
// 草稿 审批 发布
let mut p = Post::new();
p.add_text("draft text");
assert_eq!("", p.content());
p.req_review();
assert_eq!("", p.content());
p.approve();
assert_eq!("draft text", p.content());
}
由此可见状态模式的一些缺点,一是某些状态之间是相互耦合的,另一是需要重复实现一些逻辑代码。用rust实现,可使用rust语言的特征,将状态和行为编码为不同类型,rust类型检查系统会通过编译时错误阻止用户使用无效状态,以减少重复代码。
lib.rs内容
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
pub struct PendingReviewPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost { content: String::new(), }
}
pub fn content(&self) -> &str {
&self.content // 只有发布之后的才有content方法获取内容
}
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn req_review(self) -> PendingReviewPost{
PendingReviewPost { content: self.content, }
}
}
impl PendingReviewPost {
pub fn approve(self) -> Post {
Post { content: self.content, }
}
}
main.rs内容
use hello_cargo::Post;
fn main() {
// 草稿 审批 发布
let mut p = Post::new();
p.add_text("draft text");
let p = p.req_review();
let p = p.approve();
assert_eq!("draft text", p.content());
}
可见rust了通过所有权等特性实现其他面向对象语言无法实现的代码。