面向对象重塑
如果你用过 Java、Python、C# 或 TypeScript,你可能习惯了"面向对象编程"的某种固定模式:类、继承、接口和方法。Auto 走了一条不同的路。它保留了 OOP 中有用的思想——封装、多态、代码复用——但用五个更简单、更安全、更明确的关键字来替代传统实现。
这是本书最重要的一章。Auto 的 OOP 模型是它与主流语言最大的不同之处,理解它是编写地道 Auto 代码的关键。
在本章中,我们将讨论:
- Auto 中的 OOP —— 为什么 Auto 的模型不同于基于类的 OOP
type作为数据容器 —— 字段 +ext方法,一切的基础is—— 继承 —— 单继承与方法重写has—— 带自动委托的组合 —— 魔法关键字spec—— 行为契约 —— 类似接口或 traitas—— 实现规范 —— 通过 spec 实现多态ext—— 事后扩展 —— 为任意类型添加方法- 孤儿规则覆盖 ——
ext配合as用于外部类型 - 综合运用 —— 使用每个关键字的组合示例
Auto 中的 OOP
传统 OOP 用一个概念——类(class)——来完成三项工作:持有数据、定义行为、表达类型间的关系。Dog extends Animal 意味着 Dog 获得了 Animal 的字段、Animal 的方法,并且可以在任何需要 Animal 的地方使用。这很方便,但会导致紧耦合和脆弱基类问题。
Auto 将这三项工作分配给五个关键字:
| 关键字 | 职责 | 替代物 |
|---|---|---|
type | 持有数据 | class(数据部分) |
ext | 附加行为 | class(方法)/ impl |
is | 单继承 | extends |
has | 组合 + 委托 | 手动转发 |
spec / as | 行为契约 | interface / trait |
这五个关键字构成了 Auto OOP 的金三角:
type和ext给你数据 + 行为(对象)is和has给你两种类型组合方式(关系)spec和as给你多态(契约)
结果是:数据与行为分离、继承是明确的且仅限单继承、组合是自动的、多态是选择性加入的而非隐式的。让我们逐一了解每个部分。
type 作为数据容器
在传统 OOP 中,类将字段和方法捆绑在一起。在 Auto 中,type 只定义数据。你用 ext 单独附加行为。
输出:
5type 块定义了两个字段:x 和 y。ext Point 块附加了三个方法:构造函数 new,以及计算两点之间欧几里得距离的 distance。
数据与行为的分离有一个实际好处:你可以在代码库的任何地方为类型添加方法。ext 块不需要与 type 定义在同一个文件中。这与类不同,类的所有方法必须定义在类体内部。
注意每种目标语言如何映射这种模式:
- Rust 直接映射——
struct对应type,impl对应ext。语法几乎完全相同。 - Python 使用
@dataclass表示数据,在类上定义方法。new静态方法在 Python 中是惯用写法但非必需。 - C 使用
typedef struct表示数据,独立的函数以类型名为前缀。self参数作为指针显式传递。 - TypeScript 使用带构造函数的
class。static new方法是工厂风格构造的惯用写法。
is —— 继承
is 关键字为 Auto 提供单继承。子类型继承父类型的所有字段,并且可以重写方法。
输出:
Rex barks!Dog is Animal 意味着 Dog 继承了 Animal 的 name 字段。Dog 类型同时拥有 name 和 breed。构造 Dog 时,你需要为所有字段——继承的和自有——提供值。
ext Dog 块定义了一个新的 speak 方法,它_重写(override)_了继承的方法。当你调用 d.speak() 时,Auto 调用的是 Dog 的版本,而非 Animal 的版本。这是继承的标准重写行为。
这在各语言中的映射:
- Rust 没有内置继承。Animal 的字段被复制到 Dog 的结构体中。Dog 上的
speak方法替代了 Animal 的版本。 - Python 使用类继承(
Dog(Animal))。方法重写自然生效。 - C 使用嵌入结构体——Dog 包含一个
Animal base字段。对 Dog 的方法调用通过self->base.name访问。 - TypeScript 使用
extends进行类继承,用super()调用父构造函数。
Auto 的 is 刻意限制为_单_继承。一个类型只能 is 另一个类型。如果你需要组合多个来源的行为,请使用 has(组合)和 spec(接口)。
has —— 带自动委托的组合
has 关键字是 Auto 的秘密武器。它将一个类型嵌入另一个类型中,并自动将方法调用转发给嵌入的类型。你永远不需要编写样板委托方法。
输出:
Tesla
Engine (400hp) started在 Auto 中,Car has Engine 意味着 Car 包含一个 Engine,并自动转发 Car 自身未定义的任何 Engine 方法。当你调用 c.start() 时,Auto 在 Car 上查找 start 方法,找不到,就委托给 Engine.start()。
比较你在每种目标语言中必须写的内容:
- Rust 需要手动委托——
impl Car { fn start(&self) { self.engine.start(); } }。Auto 为你自动生成。 - Python 也需要手动委托——Car 上的
start方法调用self.engine.start()。 - C 需要一个
Car_start函数来调用Engine_start(&self->engine)。 - TypeScript 需要 Car 上的
start方法来调用this.engine.start()。
在除 Auto 之外的每种目标语言中,组合都需要样板代码。Auto 的 has 完全消除了这些样板。你只需声明一次关系,编译器就会生成转发代码。
这就是为什么 has 是"魔法关键字"。它给你组合的能力,却没有组合的痛苦。你可以在一个类型声明中 has 多个类型,每个类型都会将自己的方法贡献给容器。
spec —— 行为契约
spec 定义了一组类型必须实现的方法。它是 Auto 中等价于 Java/TypeScript 的接口或 Rust 的 trait 的概念。
spec 只声明方法签名——没有实现。它的意思是"任何想要成为 Drawable 的类型都必须有一个 draw 方法。"spec 本身不知道也不关心哪些类型会实现它。
注意每种语言如何表达这个概念:
- Rust 使用
trait——最接近的等价物。trait Drawable { fn draw(&self); }是直接翻译。 - Python 使用
ABC(抽象基类)配合@abstractmethod。任何继承Drawable的类都必须实现draw。 - C 使用虚函数表结构体——函数指针表。这是所有其他语言内部使用的底层机制。
- TypeScript 使用
interface——与 Auto 的spec最直接的等价物。
spec 只是契约。要让类型满足该契约,需要使用 as 关键字——我们接下来会介绍。但首先,让我们看看类型如何提供实现。
在 Listing 8-4 中,Circle 类型提供了 draw 方法。在实际程序中,编译器会检查 Circle 的方法签名是否与 spec 匹配。如果 Circle 的签名是 fn draw(&self, scale float) 而不是 fn draw(&self),编译器会拒绝——签名不匹配。
as —— 实现规范
as 关键字将类型连接到 spec。它告诉编译器:"这个类型实现了这个 spec。"一旦连接,该类型就可以在期望 spec 的任何地方使用。
输出:
Drawing circle with radius 5
Drawing square with side 3render 函数接受任何 Drawable——它不在乎是 Circle、Square 还是任何未来实现该 spec 的类型。这就是_多态(polymorphism)_:编写适用于行为而非特定类型的代码。
在 Auto 中,编译器推断 Circle 和 Square 实现了 Drawable,因为它们都有签名匹配的 draw 方法。你也可以用 ext Circle as Drawable { ... } 来显式声明——我们将在 Listing 8-7 中看到这种模式。
多态在各语言中的映射:
- Rust 使用 trait 约束:
fn render<T: Drawable>(d: &T)。这是_静态分派_——编译器为每种类型生成render的特化版本。Rust 也支持用dyn Drawable进行动态分派。 - Python 使用鸭子类型。
render函数接受任何有draw方法的对象。不需要显式接口声明。 - C 使用虚函数表——每个类型填充的函数指针结构体。这是手动的动态分派。
- TypeScript 使用
interface。render函数接受任何Drawable,TypeScript 在编译时检查类型是否实现了接口。
is 和 spec/as 的关键区别:is 用于_数据继承_(从父类型获取字段)。spec/as 用于_行为多态_(与共享行为的类型协作,无论其数据结构如何)。
ext —— 事后扩展
ext 关键字让你可以为_任何_类型添加方法——包括你没有定义的类型。你不需要修改原始类型定义。你不需要访问其源代码。只需写一个 ext 块,新方法就会出现在该类型上。
输出:
HELLO!ext String 为内置的 String 类型添加了 shout 方法。在此声明之后,程序中每个字符串值都有了 .shout() 方法。你没有修改 String。你没有子类化它。你只是……扩展了它。
这与类的运作方式有本质区别。在基于类的语言中,你无法在不修改源代码的情况下为现有类添加方法(除非使用丑陋的猴子补丁技巧)。在 Auto 中,扩展是一等公民特性。
这在各语言中的映射:
- Rust 不支持直接扩展
&str,所以转译器生成一个自由函数string_shout。这是 Rust 的惯用做法。 - Python 通过直接赋值来猴子补丁内置的
str类。这在运行时有效,但被视为一种技巧。 - C 使用一个接受字符串和输出缓冲区的自由函数。这是标准的 C 做法。
- TypeScript 使用
String.prototype添加方法,配合declare global块提供类型信息。
Auto 的 ext 是最简洁的方式:它看起来像类型上的方法,编译器在编译时检查它,而且不需要修改原始类型。
孤儿规则覆盖
ext 有一个微妙之处。如果你在自己模块中定义了类型,你可以随意 ext 它。但如果你想为_另一个模块中定义的 spec_ 实现_另一个模块中定义的类型_呢?在 Rust 中,这会被_孤儿规则(orphan rule)_阻止——你不能为外部 trait 实现外部类型。
Auto 的处理方式不同。当你写 ext TypeName as SpecName 时,你明确告诉编译器这个扩展块实现了给定的 spec。即使它们在不同模块中定义,这也同样有效。
输出:
Contents of hello.txtext File as Reader 做了两件事:为 File 扩展了 read 方法,并声明 File 实现了 Reader spec。as 部分就是孤儿规则覆盖——它明确地将类型连接到 spec,即使它们定义在不同的模块中。
没有 as,ext File { fn read(...) { ... } } 只是为 File 添加一个方法。它不会让 File 在函数参数中可用作 Reader。as 关键字才是创建 spec-类型连接的关键。
这种模式在所有目标语言中都能干净地映射:
- Rust 使用
impl Reader for File { ... }——标准的 trait 实现语法。 - Python 让 File 继承 Reader(ABC)并实现
read。 - C 使用独立函数。spec 关系是隐式的。
- TypeScript 在类声明上使用
implements Reader。
综合运用
现在让我们在一个示例中看到所有五个关键字协同工作。这是你在实际 Auto 程序中最常使用的模式。
输出:
Engine ignited: 500hp
BigRig started
BigRig stopped
Payload: 10 tons让我们追踪这个示例中的每一个关键字:
spec Vehicle—— 定义契约:任何作为 Vehicle 的类型都必须有start()和stop()方法。type Engine—— 定义数据:一个power字段。ext Engine—— 附加行为:ignite()方法。type Car has Engine—— Car 拥有 Engine。这意味着 Car 通过自动委托获得 Engine 的所有公共方法。当 Car 调用self.ignite()时,它被转发到Engine.ignite()。ext Car as Vehicle—— Car 实现 Vehicle spec。as关键字使 spec-类型连接变得显式。现在 Car 可以在任何期望 Vehicle 的地方使用。type Truck is Car—— Truck 是 Car。Truck 继承 Car 的字段(model)和 Engine 的字段(power,通过has)。Truck 也继承了所有方法——start、stop和ignite。
当我们构造 Truck { model: "BigRig", Engine { power: 500 }, payload: 10.0 } 时,我们为每个字段提供值:继承的 model、组合的 Engine,以及 Truck 自己的 payload。
当我们调用 t.start() 时,Auto 沿方法链查找:Truck 没有定义 start,所以到 Car(通过 is),在那里找到了 start,然后调用它。在 Car.start() 内部,对 self.ignite() 的调用被委托给 Engine(通过 has)。
这是一个示例中 Auto OOP 模型的完整力量:spec 定义契约,has 实现委托,is 实现继承,ext 附加行为,as 将它们连接起来。
总结
Auto 的 OOP 模型用五个关键字替代传统语言的一个关键字(class)。代价是更明确的代码,换来更清晰的关系和更少的意外。
以下是 Auto 与基于类的 OOP 的比较:
| 概念 | 基于类(Java/C#/Python) | Auto |
|---|---|---|
| 定义数据 | 类中的字段 | type |
| 附加方法 | 类中的方法 | ext |
| 继承 | extends / : | is |
| 组合 | 手动委托 | has(自动委托) |
| 接口 | interface / implements | spec / as |
| 为现有类型添加方法 | 不可能(或猴子补丁) | ext(任何位置) |
| 多继承 | 各有不同(通常受限) | has 多个,is 单个 |
| 多态 | 子类型 | Spec 约束 |
| 封装 | public / private | pub / 默认私有 |
金三角:
type+ext= 数据 + 行为(替代类)is+has= 继承 + 组合(替代 extends 和手动委托)spec+as= 行为契约(替代 interface/trait)
你现在已经了解了 Auto OOP 模型的每一个部分。在下一章中,我们将探讨 Auto 如何处理模块和项目组织——如何使用你学过的关键字来组织大型程序。