Rust权威指南之面向对象编程特性

170 阅读7分钟

一. 简述

面向对象编程简称OOP,是一种程序建模的方法。面向对象编程又很多相互矛盾的定义,其中一部分定义能够把Rust归类为面向对象语言,而另一部分定义则并不这样认为。本章我们将了解Rust如何实现面向对象相关的特性。

二. 面向对象语言的特性

我们认为面向对象的语言通常都包含一下的特性:命名对象、封装及继承。下面我依次了解下Rust中的相关实现。

2.1. 对象包含数据和行为

面向对象的程序由对象组成。对象包装了数据和操作这些数据的流程。这些流程通常被称作方法或操作。

上面的定义,Rust是面向对象的:结构体和枚举包含数据,而impl块则提供了可用于结构体和枚举的方法。虽然带有方法和结构体和枚举没有用被称为对象,但它们却是满足对象定义的所有功能。

2.2. 封装实现细节

另外一个常常伴随着面向对象编程的思想便是封装。调用对象的外部代码无法直接访问对象内部的实现细节,而唯一可以与对象进行交互的方法便是通过它公开的接口。使用对象的代码不应该深入对象的内部去改变数据或行为。封装是的开发者在修改或者重构对象的内部实现是无须改变调用这个对象的外部代码。

Rust权威指南的系列文章中我们介绍过如何控制封装:我们可以使用pub关键字来决定代码的哪些模块、类型、函数和方法是公开的,而默认情况下其他所有内容都是私有的。

2.3. 作为类型系统和代码共享机制的继承

继承机制是的对象可以沿用另一个对象的数据和行为,而无须重复定义代码。如果一门语言必须拥有继承才算面向对象语言,那么Rust不是。你无法在Rust中定义一个继承父结构体字段和方法实现的子结构体。但是在Rust中我们也可以根据使用继承是希望达成的效果来选择其他的Rust解决方案。

选择使用继承有两个原因:其一是实现代码复用;另一个使用继承的原因与类型系统有关,希望子类型能够被应用在一个需要父类型的地方,这也就是所谓的多态

Rust中选择了trait对象来代替继承。让我们看一看trait对象是如何在Rust中实现多态的。

三. 多态的实现

下面我们看一个例子,了解Rust中利用trait对象实现多态的过程。

首先我们先定义一个拥有draw方法的Draw trait:

pub trait Draw {
    fn draw(&self);
}

接着定一个Screen的结构体去持有多个Draw的trait, 并且为Screen定义一个run的方法去逐一调用draw方法。

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        self.components
            .iter()
            .for_each(|item | item.draw())
    }
}

这里大家学过之前的泛型一定会想我是不是可以如下方法去实现呢?完全可以。但是需要注意带有trait约束的泛型参数定义结构体和我们上面的实现工作机制不一样,泛型参数一次只能被替代为一个具体的类型,而trait对象则允许我们在运行时填入多种不同的具体类型。

pub struct ScreenPlus<T: Draw> {
    pub components: Vec<T>,
}

impl<T> ScreenPlus<T> where T: Draw {
    pub fn run(&self) {
        self.components.iter().for_each(|item| item.draw() )
    }
}

接着我们为trait添加一些具体的类型吧!

pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}

impl Draw for Button {
    fn draw(&self) {
        println!("绘制一个按钮")
    }
}

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        println!("绘制一个选择框")
    }
}

最后我们在main方法中创建Screen实例:

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("No"),
                    String::from("Maybe"),
                ]
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                label: String::from("Ok")
            })
        ]
    };
    screen.run()
}

在前面的泛型的文章中,我们了解过Rust编译器会在泛型使用trait约束时执行单态化:编译器会为每一个具体类型生成队对应泛型函数和泛型方法的非泛型实现,并使用这些具体的类型来替代泛型参数。通过单态化生成的代码会执行静态派发,这意味着编译器能够在编译过程中确定你调用的具体方法,这个概念与动态派发相对应,动态派发下编译器无法在编译过程中确定你调用的究竟是哪一个方法。但是Rust会在运行时通过trait对象内部的指针去定位具体调用哪个方法。这个定位过程会产生一些不可避免的运行时开销,而这并不会出现在静态派发中。动态派发还会阻止编译器内联代码,进而使得部分优化操作无法进行。但不管怎么样,动态派发确实能够为上面例子带来额外的灵活性,基于此在实际使用中需要灵活使用。

还有一个注意点就是我们只能将满足对象安全的trait转换为trait对象。Rust中采用了一套较为复杂的规则来决定某个trait是否对象安全。但是在实际应用中,我们只需要关注其中两条规则即可:

  • 方法的返回类型不是Self;
  • 方法中不包含任何泛型参数;

如果一个trait中定义的所有方法满足上面两条规则,那么这个trait就是对象安全的。

四. 实现一种面向对象的设计模式

下面我们实现一种面向对象的设计模式:状态模式。这个模式的特点是:一个值拥有内部状态由数个状态对象表达而成,而值的行为则随着内部状态的改变而改变。

下面我们实现一个博客发布的工作流程来看看Rust如何实现状态模式。

首先我们定义一个Post并新建一个处于草稿状态的新实例:

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 {
        self.state.as_ref().unwrap().content(&self)
    }
    pub fn request_review(&mut self) {
        if let Some(e) = self.state.take() {
            self.state = Some(e.request_review())
        }
    }
    pub fn approve(&mut self) {
        if let Some(e) = self.state.take() {
            self.state = Some(e.approve())
        }
    }
}

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 { "" }
}
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
    }
    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}

测试方法如下:

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());
}

至此按照发布博客的工作流程实现的一套状态模式就完成了。但是上面实现的状态模式由两个缺点:其中一个是状态模式实现了状态间的转移,所以某些状态之间是相互耦合的;另一缺点是我们实现了一些重复的代码逻辑。

严格按照面向对象语言的定义来实现一套状态模式自然是可行的,但这并不能发挥出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
        }
    }
}

最后测试代码如下:

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());
}