Skip to content
📖 Auto 版 Rust Book本书目录

Auto 中的面向对象模式 ​

面向对象编程(OOP)是一种程序建模方式。对象作为编程概念最早出现在 20 世纪 60 年代的 Simula 编程语言中。许多相互竞争的定义描述了什么是 OOP,按照某些定义,Auto 是面向对象的,但按照另一些定义则不是。在本章中,我们将探讨通常被认为面向对象的某些特征,以及这些特征如何转化为惯用的 Auto 代码。

Auto 提供了三个实现 OOP 风格模式的关键机制:

  • spec — 规范定义共享行为(类似于 Rust 的 trait)
  • is — 类型组合和行为契约(替代继承)
  • has — 通过组合委托行为

我们还将展示如何在 Auto 中实现面向对象设计模式,并讨论这样做与利用 Auto 自身优势实现解决方案之间的权衡。

面向对象语言的特征 ​

在编程社区中,对于一种语言必须具备哪些特征才能被认为是面向对象的,并没有共识。Auto 受到多种编程范式的影响,包括 OOP;例如,我们在第 13 章中探讨了来自函数式编程的特性。可以说,OOP 语言共享某些常见特征——即对象、封装和继承。让我们看看每个特征意味着什么以及 Auto 是否支持它。

对象包含数据和行为 ​

《设计模式:可复用面向对象软件的基础》(Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著,Addison-Wesley 1994 年出版)一书被俗称为"四人帮"(GoF)书,它这样定义 OOP:

面向对象程序由对象组成。对象__同时封装了数据和操作这些数据的过程。这些过程通常被称为__方法__或__操作。

按照这个定义,Auto 是面向对象的:类型有数据,ext 块为类型提供方法。尽管带有方法的类型在 Auto 中不被称为"对象",但它们提供了四人帮定义的对象所具有的相同功能。

type 关键字定义带有字段的数据结构,ext 块附加行为。这就是 Auto 中传统 OOP 语言类的等价物。

封装隐藏实现细节 ​

与 OOP 相关的另一个方面是_封装_的概念,即对象的实现细节不能被使用该对象的代码访问。因此,与对象交互的唯一方式是通过其公共 API。

在代码清单 18-1 中,list 和 average 字段在 Auto 中默认是私有的。只有标记为 pub 的方法才能从类型外部访问。update_average 方法是私有的——它是一个实现细节。

我们在第 7 章讨论了如何控制封装:我们可以使用 pub 关键字来决定代码中哪些模块、类型、函数和方法应该是公共的,默认情况下其他所有内容都是私有的。

因为我们封装了 AveragedCollection 的实现细节,我们可以在未来轻松更改诸如数据结构等方面。例如,我们可以用 Set<int> 替代 List<int> 作为 list 字段。只要 add、remove 和 average 公共方法的签名保持不变,使用 AveragedCollection 的代码就不需要更改。

如果封装是一种语言被认为是面向对象的必要方面,那么 Auto 满足这个要求。

继承:被组合和规范取代 ​

_继承_是一种机制,通过它对象可以从另一个对象的定义中继承元素,从而获得父对象的数据和行为,无需再次定义。

如果一种语言必须具备继承才能被认为是面向对象的,那么 Auto 不是这样的语言。无法定义一个继承父类型字段和方法实现的类型。

然而,Auto 提供了强大的替代方案:

  1. 带默认实现的 spec — 用于代码复用(类似 Rust 的 trait)
  2. is 组合 — 用于类型关系(替代继承)
  3. has 委托 — 将行为转发给包含的类型

你选择继承主要有两个原因。一是代码复用:你可以为一种类型实现特定行为,继承使你能够为不同的类型复用该实现。在 Auto 中,你可以通过 spec 默认实现实现这一点。

使用继承的另一个原因与类型系统有关:使子类型能够在与父类型相同的地方使用。这也被称为_多态_。在 Auto 中,你通过 spec 约束和泛型类型实现这一点。

多态 ​

_多态_意味着如果多个对象共享某些特征,你可以在运行时互相替换它们。

Auto 使用泛型来抽象不同的可能类型,使用 spec 约束来施加这些类型必须提供什么。这有时被称为_有界参数多态_,与 Rust 采用的方法相同。

is/has/spec 三角关系 ​

Auto 的 OOP 方法以三个协同工作的关键字为中心:

关键字用途OOP 等价物
spec定义共享行为接口 / Trait
is组合类型,声明契约继承(类型关系)
has通过组合委托行为组合 + 委托

spec:定义共享行为 ​

spec 定义了一组类型必须实现的方法。这与 Rust 的 trait 或 Java/Go 中的接口完全相同:

类型使用 spec ... for Type 语法实现规范:

is:类型组合和契约 ​

is 关键字有两个用途:

  1. 组合类型 — 将一个类型的字段嵌入另一个类型
  2. 声明规范契约 — 声明一个类型满足某个规范
使用 `is` 进行类型组合

在 Rust 中,组合需要访问嵌套字段(dog.animal.name)。在 Auto 中,is 扁平化组合——dog.name 可以直接使用,因为 Dog is Animal。

你也可以使用 is 来声明类型实现了某个规范:

auto
type Dog {
    is Animal
    is Summary       // 声明 Dog 实现了 Summary
    breed String
}

has:通过组合委托 ​

has 关键字将方法实现委托给包含的类型:

使用 `has` 进行委托

在 Rust 中,你必须手动编写委托方法(self.engine.start())。Auto 的 has 关键字自动将方法调用转发给包含的类型。

is vs has ​

特性ishas
用途组合字段委托行为
字段访问扁平化(直接)通过字段
方法委托否是
多重性一个类型可以 is 多个类型一个类型可以 has 多个类型
类比继承(数据)组合(行为)

当你想共享数据结构时使用 is。当你想共享行为但不暴露内部类型的字段时使用 has。

使用规范实现多态 ​

规范对象:动态派发 ​

与 Rust 的 dyn Trait 一样,Auto 通过规范对象支持动态派发。当你需要一个都实现同一规范的异构类型集合时,使用规范对象:

dyn Draw 语法创建一个_规范对象_——对实现 Draw 规范的任何类型的引用。这与 Rust 的 dyn Trait 完全类似。

为具体类型实现规范 ​

现在 Screen 可以同时持有 Button 和其他任何实现 Draw 的类型:

使用规范对象的异构类型

静态派发 vs 动态派发 ​

当你使用带规范约束的泛型时,Auto 执行_静态派发_——编译器为每个具体类型生成专门的代码。这与 Rust 中的单态化相同:

auto
// 静态派发:每个 T 获得自己的编译版本
fn draw_all<T Draw>(items List<T>) {
    for item in items {
        item.draw()
    }
}

当你使用规范对象(dyn Draw)时,Auto 执行_动态派发_——调用的方法在运行时通过虚表确定:

auto
// 动态派发:方法在运行时查找
fn draw_all(items List<Box<dyn Draw>>) {
    for item in items {
        item.draw()
    }
}
方面静态派发(泛型)动态派发(dyn)
性能更快(可内联)轻微开销(虚表查找)
灵活性每次调用单一类型每次调用多种类型
二进制大小更大(单态化)更小
何时使用类型统一时类型混合时

Auto 中的状态模式 ​

一个经典的 OOP 设计模式是_状态模式_,其中对象的行为根据其内部状态而改变。让我们实现一个博客文章工作流,它经历以下状态:草稿 → 审核 → 发布。

实现状态模式 ​

使用文章:

使用博客文章状态机

将状态编码到类型系统中 ​

Auto 的 is 关键字提供了一种替代方案:将状态直接编码到类型系统中。我们不为运行时状态转换使用不同的类型,而是为每个状态使用不同的类型:

将状态编码到类型系统中

这种方法有一个显著优势:_类型系统_在编译时防止无效操作。你不能对 DraftPost 调用 content()——它不存在于该类型上。你不能对 DraftPost 调用 approve()——只有 request_review() 可用。编译器强制执行状态转换。

用法:

auto
fn main() {
    let draft = DraftPost(content: "")
    draft.add_text("I ate a salad for lunch today")

    let review = draft.request_review()
    let published = review.approve()

    print(published.content())
}
rust
fn main() {
    let mut draft = DraftPost { content: String::new() };
    draft.add_text("I ate a salad for lunch today");

    let review = draft.request_review();
    let published = review.approve();

    println!("{}", published.content());
}

这是惯用的 Auto 方法:使用类型系统使非法状态不可表示,而不是依赖运行时检查。

is/has/spec vs Rust 的 OOP ​

OOP 概念RustAuto
接口 / Traittraitspec
实现impl Trait for Typespec Trait for Type
动态派发dyn Traitdyn Spec
组合手动:self.inner.method()has:自动委托
字段嵌入手动:self.inner.fieldis:扁平化访问
默认方法trait 带默认方法体spec 带默认方法体
无继承无继承无继承(使用 is/has)
状态模式Box&lt;dyn State&gt;dyn State 或类型状态

Auto 的 is 和 has 关键字消除了 Rust 在组合和委托时所需的样板代码。在 Rust 中,组合两个结构体并委托方法需要手动编写包装方法。在 Auto 中,has 自动生成这些包装器。

面向对象模式速查表 ​

功能AutoRust
定义接口spec Name { }trait Name { }
实现接口spec Name for Type { }impl Name for Type { }
动态派发dyn Specdyn Trait
组合字段is Type手动嵌入
委托方法has Type手动委托
默认方法spec 中的方法体trait 中的方法体
封装pub / 默认私有pub / 默认私有
多态泛型 + spec 约束泛型 + trait 约束

总结 ​

Auto 通过一组功能组合支持面向对象模式:

  1. type 和 ext — 封装数据和行为,像对象一样
  2. 封装 — pub 控制可见性,字段默认私有
  3. spec — 定义共享行为,像接口或 trait
  4. is — 通过扁平化字段访问组合类型
  5. has — 通过自动方法转发委托行为
  6. dyn Spec — 异构集合的动态派发
  7. 类型状态模式 — 将状态编码到类型系统中实现编译时安全

Auto 故意避免继承,转而使用组合(is/has)和行为契约(spec)。这产生了更灵活的设计,类型只共享它们明确选择加入的行为,类型系统在编译时而非运行时捕获无效的状态转换。

在下一章,我们将探讨 Auto 使用 is 关键字的模式匹配,以及它与 Rust 的 match 表达式的比较。

基于 MIT 许可发布。