Skip to content
📖 Auto 编程语言(主书)本书目录

面向对象重塑 ​

如果你用过 Java、Python、C# 或 TypeScript,你可能习惯了"面向对象编程"的某种固定模式:类、继承、接口和方法。Auto 走了一条不同的路。它保留了 OOP 中有用的思想——封装、多态、代码复用——但用五个更简单、更安全、更明确的关键字来替代传统实现。

这是本书最重要的一章。Auto 的 OOP 模型是它与主流语言最大的不同之处,理解它是编写地道 Auto 代码的关键。

在本章中,我们将讨论:

  • Auto 中的 OOP —— 为什么 Auto 的模型不同于基于类的 OOP
  • type 作为数据容器 —— 字段 + ext 方法,一切的基础
  • is —— 继承 —— 单继承与方法重写
  • has —— 带自动委托的组合 —— 魔法关键字
  • spec —— 行为契约 —— 类似接口或 trait
  • as —— 实现规范 —— 通过 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 单独附加行为。

带字段和 `ext` 方法的 `type`

输出:

5

type 块定义了两个字段: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 提供单继承。子类型继承父类型的所有字段,并且可以重写方法。

使用 `is` 的单继承与方法重写

输出:

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 的秘密武器。它将一个类型嵌入另一个类型中,并自动将方法调用转发给嵌入的类型。你永远不需要编写样板委托方法。

使用 `has` 的组合与自动方法委托

输出:

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` 定义为行为契约

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 的任何地方使用。

使用 `as` 实现 spec 以获得多态

输出:

Drawing circle with radius 5
Drawing square with side 3

render 函数接受任何 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 块,新方法就会出现在该类型上。

使用 `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。即使它们在不同模块中定义,这也同样有效。

使用 `ext ... as` 为类型实现 spec

输出:

Contents of hello.txt

ext 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 程序中最常使用的模式。

一个程序中的全部五个 OOP 关键字

输出:

Engine ignited: 500hp
BigRig started
BigRig stopped
Payload: 10 tons

让我们追踪这个示例中的每一个关键字:

  1. spec Vehicle —— 定义契约:任何作为 Vehicle 的类型都必须有 start() 和 stop() 方法。

  2. type Engine —— 定义数据:一个 power 字段。

  3. ext Engine —— 附加行为:ignite() 方法。

  4. type Car has Engine —— Car 拥有 Engine。这意味着 Car 通过自动委托获得 Engine 的所有公共方法。当 Car 调用 self.ignite() 时,它被转发到 Engine.ignite()。

  5. ext Car as Vehicle —— Car 实现 Vehicle spec。as 关键字使 spec-类型连接变得显式。现在 Car 可以在任何期望 Vehicle 的地方使用。

  6. 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 / implementsspec / as
为现有类型添加方法不可能(或猴子补丁)ext(任何位置)
多继承各有不同(通常受限)has 多个,is 单个
多态子类型Spec 约束
封装public / privatepub / 默认私有

金三角:

  • type + ext = 数据 + 行为(替代类)
  • is + has = 继承 + 组合(替代 extends 和手动委托)
  • spec + as = 行为契约(替代 interface/trait)

你现在已经了解了 Auto OOP 模型的每一个部分。在下一章中,我们将探讨 Auto 如何处理模块和项目组织——如何使用你学过的关键字来组织大型程序。

基于 MIT 许可发布。