Rust 学习笔记 - 面向对象

面向对象

Rust 受到很多种编程范式的影响,也包括面向对象。

面向对象通常包含以下特征:命名对象、封装、继承。面向对象的程序由对象组成。对象包含数据和行为,包装了数据和操作这些数据的过程,这些过程通常被称作方法或操作。

基于此定义:Rust 是面向对象的。

struct、enum 包含数据,impl 块为之提供了方法,但带有方法的 struct、enum 并没有被称为对象。

封装

调用对象外部的代码无法直接访问对象内部的实现细节,唯一可以与对象进行交互的方法就是通过它公开的 API。

pub 关键字决定哪些方法、属性是公开的,其它的默认都是私有的,这就是 Rust 实现封装这个特性的方法。

继承

使对象可以沿用另外一个对象的数据和行为,且无需重复定义相关代码。

Rust 是没有继承的。

通常需要使用继承的原因:

  • 代码复用:Rust 中用默认 trait 方法来进行代码共享
  • 多态:Rust 中使用泛型和 trait 约束(限定参数化多态 bounded parametric)来实现多态

很多新语言都不适用继承作为内置的程序设计方案了。

实例

创建一个 GUI 工具:它会遍历某个元素的列表,依次调用元素的 draw 方法进行绘制,例如: Button、TextField 等元素。

在面向对象语言里:定义一个 Component 父类,里面定义了 draw 方法,定义 Button、TextField 等类,继承于 Component 类。

使用 Rust 来实现的话,方法就会有一些不一样。

为共有行为定义一个 trait

Rust 避免将 struct 或 enum 称为对象,因为它们与 impl 块是分开的。

trait 对象有些类似于其它语言中的对象,因为它们在某种程度上组合了数据与行为。

trait 对象与传统对象又有不同的地方:无法为 trait 对象添加数据。trait 对象被专门用于抽象某些共有行为,它没有其它语言中的对象那么通用。

下面来实现一下这个 GUI 工具:

// lib.rs
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();
        }
    }
}

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

impl Draw for Button {
    fn draw(&self) {
        // 绘制一个按钮
    }
}
复制代码
// main.rs
// adder 是 crate 的名称
use adder::Draw;
use adder::{Button, Screen};

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

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

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: 75,
                height: 10,
                label: String::from("OK"),
            }),
        ]
    };

    screen.run();
}
复制代码

动态派发

Trait 对象执行的是动态派发(dynamic dispatch)。

将 trait 约束作用于泛型时,Rust 编译器会执行单态化:编译器会为我们用来替换泛型类型参数的每一个具体类型生成对应函数和方法的非泛型实现。

通过单态化生成的代码会执行静态派发(static dispatch),在编译过程中确定调用的具体方法。

动态派发则无法在编译过程中确定调用的究竟是哪一种方法,编译器会产生额外的代码以便在运行时找出希望调用的方法。

使用 trait 对象,会执行动态派发,产生运行时开销,阻止编译器内联方法代码,使得部分优化操作无法进行。

对象安全

Trait 对象必须保证对象安全,只能把满足对象安全(object-safe)的 trait 转化为 trait 对象。

Rust 采用一系列规则来判定某个对象是否安全,只需记住两条:

  1. 方法的返回类型不是 Self
  2. 方法中不包含任何泛型类型参数
pub trait Clone {
    fn clone(&self) -> Self;
}

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

pub struct Screen {
    pub components: Vec<Box<dyn Clone>>,
    // 报错:the trait `Clone` cannot be made into an object consider moving `clone` to another trait
}
复制代码

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

状态模式(state pattern)是一种面向对象设计模式:一个值拥有的内部状态由数个状态对象(state object)表达而成,而值的行为则随着内部状态的改变而改变。

使用状态模式意味着:

  • 业务需求变化时,不需要修改持有状态的值的代码,或者使用这个值的代码
  • 只需要更新状态对象内部的代码,以便改变其规则,或者增加一些新的状态对象。
// main.rs
use blog::Post;

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

    // 新发布,还未通过审核,所以 content 应该为空
    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());
}
复制代码
pub struct Post {
    state: Option<Box<dyn State>>, // 发布的状态,需要实现 State 这个 trait
    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(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    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>;
    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> {
        Box::new(PendingReview {})
    }

    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
    }
}
复制代码

状态模式的取舍权衡,缺点:某些状态之间是相互耦合的,需要重复实现一些逻辑代码。

严格按照面向对象这一套方式来实现是可以的,但是这不能发挥出 Rust 的威力。我们其实可以将状态和行为编码为类型。这样 Rust 类型检查系统会通过编译时错误来阻止用户使用无效的状态。

// main.rs
use blog::Post;

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

    // 新发布,还未通过审核,所以 content 应该为空
    post.add_text("I ate a salad for lunch today");

    // 审核阶段,所以 content 应该为空
    let pending_review_post = post.request_review();

    // 审核通过,content 成功发布出来
    let post = pending_review_post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}
复制代码
// 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);
    }

    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, }
    }
}
复制代码

总结

Rust 不仅能够实现面向对象的设计模式,还可以支持更多的模式,例如:将状态和行为编码为类型。

面向对象的经典模式并不总是 Rust 编程实践中的最佳选择,因为 Rust 具有其所有权等其它面向对象语言没有的特性。

おすすめ

転載: juejin.im/post/7041096042607017991