Rust 的面向对象特性

在原文上有删减,原文链接Rust 的面向对象特性

面向对象语言的特征

对象包含数据和行为

The Gang of Four 中对象的定义:

Object-oriented programs are made up of objects. An object packages both data and the procedures that operate on that data. The procedures are typically called methods or operations.

面向对象的程序是由对象组成的。一个 对象 包含数据和操作这些数据的过程。这些过程通常被称为 方法操作

在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被称为对象,但是它们提供了与对象相同的功能。

封装隐藏了实现细节

封装(encapsulation)的思想:对象的实现细节不能被使用对象的代码获取到,封装使得改变和重构对象的内部时无需改变使用对象的代码。Rust 可以使用 pub 关键字来决定模块、类型、函数和方法是公有的,而默认情况下其他一切都是私有的。

AveragedCollection 结构体维护了一个整型列表和集合中所有元素的平均值:

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

在 AveragedCollection 结构体上实现了 add、remove 和 average 公有方法:

//保证变量被增加到列表或者被从列表删除时,也会同时更新平均值
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;
    }
}

继承,作为类型系统与代码共享

继承(Inheritance)是一个很多编程语言都提供的机制,一个对象可以定义为继承另一个对象定义中的元素,这使其可以获得父对象的数据和行为,而无需重新定义。

在 Rust 中,没有宏则无法定义一个结构体继承父结构体的成员和方法,不过 Rust 也提供了其他的解决方案。

选择继承有两个主要的原因:

  • 重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。Rust 代码中可以使用默认 trait 方法实现来进行有限的共享

  • 多态:表现为子类型可以用于父类型被使用的地方,这意味着如果多种对象共享特定的属性,则可以相互替代使用。

Rust 选择了一个不同的途径,使用 trait 对象而不是继承

顾及不同类型值的 trait 对象

定义通用行为的 trait

定义一个带有 draw 方法的 trait Draw:

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

一个 Screen 结构体的定义,它带有一个字段 components,其包含实现了 Draw trait 的 trait 对象的 vector:

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

在 Screen 上实现一个 run 方法,该方法在每个 component 上调用 draw 方法:

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

这与定义使用了带有 trait bound 的泛型类型参数的结构体不同:泛型类型参数一次只能替代一个具体类型,而 trait 对象则允许在运行时替代多种具体类型。

一种 Screen 结构体的替代实现,其 run 方法使用泛型和 trait bound:

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

这限制了 Screen 实例必须拥有一个全是 Button 类型或者全是 TextField 类型的组件列表。如果只需要同质(相同类型)集合,则倾向于使用泛型和 trait bound,因为其定义会在编译时采用具体类型进行单态化。

实现 trait

一个实现了 Draw trait 的 Button 结构体:

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

impl Draw for Button {
    fn draw(&self) {
        // code to actually draw a button
    }
}

另一个使用 gui 的 crate 中,在 SelectBox 结构体上实现 Draw trait:

use gui::Draw;

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

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}

使用 trait 对象来存储实现了相同 trait 的不同类型的值:

use gui::{Button, Screen};

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

这个概念 —— 只关心值所反映的信息而不是其具体类型 —— 类似于动态类型语言中称为 鸭子类型(duck typing)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子!

尝试使用一种没有实现 trait 对象的 trait 的类型:

//这段代码无法通过编译!
//因为 String 没有实现 rust_gui::Draw trait
use gui::Screen;

fn main() {
   let screen = Screen {
       components: vec![Box::new(String::from("Hi"))],
   };

   screen.run();
}

trait 对象执行动态分发

当对泛型使用 trait bound 时编译器所执行的单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了函数和方法的非泛型实现。单态化产生的代码在执行 静态分发(static dispatch)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。这与 动态分发 (dynamic dispatch)相对,这时编译器在编译时无法知晓调用了什么方法。在动态分发的场景下,编译器会生成负责在运行时确定该调用什么方法的代码。

当使用 trait 对象时,Rust 必须使用动态分发,编译器无法知晓所有可能用于 trait 对象代码的类型,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。

面向对象设计模式的实现

状态模式(state pattern)是一个面向对象设计模式,用状态模式增量式地实现一个发布博文的工作流以探索这个概念,博客的最终功能如下:

  • 博文从空白的草案开始。
  • 一旦草案完成,请求审核博文。
  • 一旦博文过审,它将被发表。
  • 只有被发表的博文的内容会被打印

展示了 blog crate 期望行为的代码,代码还不能编译:

//这段代码无法通过编译!
use blog::Post;

fn main() {
    //使用 Post::new 创建一个新的博文草案
    let mut post = Post::new();

    //在草案阶段为博文编写一些文本
    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());
    
    //请求审核博文,content 返回空字符串。
    post.request_review();
    assert_eq!("", post.content());
    
    //博文审核通过被发表,content 文本将被返回
    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

定义 Post 并新建一个草案状态的实例

Post 结构体的定义和新建 Post 实例的 new 函数,State trait 和结构体 Draft:

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            //确保了无论何时新建一个 Post 实例都会从草案开始
            state: Some(Box::new(Draft {})),
            //将 content 设置为新建的空 String
            content: String::new(),
        }
    }
}

//定义了所有不同状态的博文所共享的行为
trait State {}

//状态对象:初始状态
struct Draft {}
//实现 State 状态
impl State for Draft {}

存放博文内容的文本

实现方法 add_text 来向博文的 content 增加文本:

impl Post {
    // --snip--
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

确保博文草案的内容是空的

增加一个 Post 的 content 方法的占位实现,它总是返回一个空字符串 slice:

impl Post {
    // --snip--
    pub fn content(&self) -> &str {
        ""
    }
}

请求审核博文来改变其状态

实现 Post 和 State trait 的 request_review 方法,将其状态由 Draft 改为 PendingReview:

impl Post {
    // --snip--
    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
    }
}

Draft 的 request_review 方法需要返回一个新的,装箱的 PendingReview 结构体的实例,其用来代表博文处于等待审核状态。结构体 PendingReview 同样也实现了 request_review 方法,不过它不进行任何状态转换,它返回自身。

状态模式的优势:无论 state 是何值,Post 的 request_review 方法都是一样的,每个状态只负责它自己的规则

增加改变 content 行为的 approve 方法

approve 方法将与 request_review 方法类似:它会将 state 设置为审核通过时应处于的状态。

为 Post 和 State trait 实现 approve 方法:

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
    }
}

如果对 Draft 调用 approve 方法,并没有任何效果,因为它会返回 self。当对 PendingReview 调用 approve 时,它返回一个新的、装箱的 Published 结构体的实例。

更新 Post 的 content 方法来委托调用 State 的 content 方法:

//这段代码无法通过编译!
impl Post {
    // --snip--
    pub fn content(&self) -> &str {
        //调用 as_ref 会返回一个 Option<&Box<dyn State>>
        self.state.as_ref().unwrap().content(self)
    }
    // --snip--
}

为 State trait 增加 content 方法:

trait State {
    // --snip--
    fn content<'a>(&self, post: &'a Post) -> &'a str {
        ""
    }
}

// --snip--
struct Published {}

impl State for Published {
    // --snip--
    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}

示例完成,通过发布博文工作流的规则实现了状态模式,围绕这些规则的逻辑都存在于状态对象中而不是分散在 Post 之中。

不使用枚举是因为每一个检查枚举值的地方都需要一个 match 表达式或类似的代码来处理所有可能的成员,这相比 trait 对象模式可能显得更重复。

状态模式的权衡取舍

对于状态模式来说,Post 的方法和使用 Post 的位置无需 match 语句,同时增加新状态只涉及到增加一个新 struct 和为其实现 trait 的方法。

完全按照面向对象语言的定义实现这个模式并没有尽可能地利用 Rust 的优势,可以做一些修改将无效的状态和状态转移变为编译时错误。

将状态和行为编码为类型

将状态编码进不同的类型,Rust 的类型检查就会将任何在只能使用发布博文的地方使用草案博文的尝试变为编译时错误:

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());
}

带有 content 方法的 Post 和没有 content 方法的 DraftPost:

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
    }
}

//注意 DraftPost 并没有定义 content 方法
impl DraftPost {
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

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

PendingReviewPost 通过调用 DraftPost 的 request_review 创建,approve 方法将 PendingReviewPost 变为发布的 Post:

impl DraftPost {
    // --snip--
    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,
        }
    }
}

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

热门相关:我和超级大佬隐婚了   黑暗血时代   总裁大人,又又又吻我了   抗战老兵之不死传奇   萌妻太甜:总裁大人,别傲娇