泛型
每种编程语言都有处理概念重复的工具。最常见的重复形式是只为一种类型编写的代码 需要为另一种类型重写。写一个 max 函数用于 int,再为 float 写一个,再为 String 写一个。逻辑完全相同——只有类型不同。
泛型消除了这种重复。它们让你编写_按类型参数化_的函数、类型和枚举——同样的代码 适用于调用者选择的任何类型。由于 Auto(像 Rust 一样)使用_单态化_,运行时开销 为零。编译器为每个具体类型生成专用版本,所以泛型与手写代码一样快。
在本章中,我们将讨论:
- 什么是泛型? —— 零成本抽象、类型参数化
- 泛型函数 ——
<T>语法、类型推断 - 泛型类型 ——
type Box<T>、泛型类型上的方法 - Spec 约束 —— 使用 spec 约束泛型
- 多重约束 —— 组合多个 spec 约束
- 泛型方法 —— 操作泛型类型的方法
- 单态化 —— 泛型如何在编译时变为具体代码
- 泛型枚举 ——
Result<T, E>、Option<T>模式
什么是泛型?
泛型是具体类型的抽象替代品。你可以不编写专门处理 int 的函数,而是编写处理 "某个类型 T"的函数。调用者在调用函数时决定 T 是什么。
我们在本书中已经使用了泛型。?T(Option)、!T(Result)、List<T> 和 Map<K, V> 都是泛型类型。它们由一个或多个类型参数化,决定了它们存储什么类型 的数据。现在我们将学习如何定义自己的泛型。
关键洞察是泛型是一种_编译时_抽象。编译器确切知道使用了哪些具体类型,并为每 个类型生成优化代码。没有运行时类型检查,没有装箱,没有向下转型。泛型代码与你 为每个类型手写单独函数一样快。
泛型函数
泛型函数在函数名和参数列表之间的尖括号 <> 中声明类型参数。
输出:
100
y我们读 fn largest<T>(list []T) T 为"函数 largest 对某个类型 T 是泛型的。" 函数接受一个 T 值的列表并返回一个 T。类型参数 T 是一个占位符——在函数被 调用时,它将被具体类型替换。
同一个函数既适用于 [34, 50, 25, 100, 65](整数列表),也适用于 ['y', 'm', 'a', 'q'](字符列表)。编译器从参数推断 T 的类型——你不需要显式写 largest<int>(numbers)。这称为_类型推断_。
注意 C 中发生了什么:因为 C 没有泛型,编译器必须生成单独的 largest_int 和 largest_char 函数。这本质上就是 Auto 的编译器通过_单态化_自动完成的事情—— 但你永远不需要自己编写这些单独的版本。
在各语言中的映射:
- Rust 使用相同的
<T>语法。唯一的区别是 Rust 的: Clone + PartialOrd约束,我们将在 Spec 约束一节中介绍。 - Python 使用
TypeVar('T')作为泛型类型变量。Python 的泛型在运行时被擦 除——它们只被 mypy 等静态类型检查器检查。 - C 没有泛型。转译器为每个具体类型生成单独的单态化函数。
- TypeScript 使用与 Auto 和 Rust 类似的
<T>语法。泛型在编译时被擦除。
泛型类型
类型也可以由泛型类型参数化。这是你构建容器、包装器和其他可复用数据结构的方式。
输出:
42
helloBox<T> 是一个带有一个类型参数 T 的泛型类型。类型参数出现在类型定义 (type Box<T>)和方法扩展(ext Box<T>)中。new 构造函数接受一个 T 类型的值并返回 Box<T>。get 方法返回内部的 T 类型值。
同一个 Box<T> 定义既适用于 Box<int>(保存 42),也适用于 Box<String> (保存 "hello")。编译器为每个变体生成单独的具体类型。
C 转译器明确展示了这一点:BoxInt 和 BoxStr 是带有单独函数的独立结构体。 在 Auto 和 Rust 中,编译器自动完成这些工作。
当字段需要不同类型时,可以使用多个类型参数:
type Pair<T, U> {
first T
second U
}这里 T 和 U 是独立的类型参数。Pair<int, String> 保存一个整数和一个字 符串,而 Pair<float, bool> 保存一个浮点数和一个布尔值。
Spec 约束
一个在类型参数上使用 > 的泛型函数(如 largest)不能接受任意类型。> 运算符要求类型支持比较。在 Auto 中,我们使用 _spec 约束_来表达这种限制。
_spec 约束_限制哪些类型对类型参数是有效的。语法是 where T: SpecName,意 思是"T 必须实现名为 SpecName 的 spec"。
输出:
(3, 4)spec Printable 定义了一个契约:任何实现 Printable 的类型必须提供一个返回 String 的 to_string 方法。Point 类型使用 ext Point as Printable 实现了 这个 spec。
函数 print_it<T> 有约束 where T: Printable。这意味着编译器只允许你使用实 现了 Printable 的类型调用 print_it。如果你尝试传递 int,你会得到编译时 错误,因为 int 没有实现 Printable。
在 Rust 中,同样的约束写作 <T: Printable>。在 TypeScript 中,它是 <T extends Printable>。在 Python 中,它是使用 Protocol 的 item: Printable。在 C 中,没有泛型——转译器为每个具体类型生成单独的函数。
多重约束
一个类型参数可以同时被多个 spec 约束。在 where 子句中使用逗号分隔约束。
函数 show_and_copy<T> 要求 T 同时实现 Display 和 Clone。where 子句 使用逗号分隔的两个约束:where T: Display, T: Clone。
在 Rust 中,等价写法是 <T: Display + Clone>——+ 将约束组合成一个。Auto 的 逗号分隔语法使每个约束更明确,当有很多约束时更容易阅读。
在 TypeScript 中,交叉类型(&)组合多个约束。在 Python 中,Protocol 不直 接支持交叉,所以转译器使用 # type: ignore 注释。在 C 中,无法表达多个接口约 束——每个具体类型需要自己的函数。
泛型方法
泛型类型上的方法使用相同的 ext Type<T> 语法。类型参数在 ext 之后、类型名 之前声明,就像 Rust 的 impl<T> Type<T> 一样。
输出:
2
1swap 方法使用 ext Pair<T> 定义在 Pair<T> 上。ext 后面声明的 T 告诉 编译器这个方法是为_所有_可能的 T 定义的。方法接受 &self(对 pair 的引用), 构造一个字段交换的新 Pair<T> 并返回它。
因为 Pair<T> 使用单一类型参数,first 和 second 必须是相同类型。 Pair<int> 保存两个整数,Pair<String> 保存两个字符串。你不能在单个 Pair<T> 中混合类型——如果需要混合类型,请使用带有两个类型参数的 Pair<T, U>。
C 转译器生成了 PairInt_swap——int 变体的具体函数。编译器对 Pair<T> 使用 的每个具体类型都这样做。
单态化
Auto 中的泛型是_零成本抽象_。使用它们没有运行时惩罚。编译器通过称为_单态化_ 的过程实现这一点。
单态化的工作原理是为每个使用的具体类型生成泛型函数或类型的单独副本。如果你调 用 identity(42),编译器会生成一个专门用于 int 的 identity 函数。如果你 调用 identity("hello"),它会生成另一个专门用于 String 的版本。
输出:
42
helloidentity 函数非常简单——它返回其参数。但这个原理适用于任何泛型函数,无论多 么复杂。编译器为每个使用的具体类型生成专用版本。
C 代码最明确地展示了这一点:identity_int 和 identity_str 是带有独立实现的 单独函数。这就是 Auto 的编译器在内部做的事情。在最终二进制文件中,没有"泛型" 的 identity 函数——只有具体版本。
这有一个重要的含义:泛型不使用动态分发或运行时类型信息。对 identity(42) 的 调用编译出的机器码与你手写一个非泛型的 fn identity_int(x int) int 完全相同。 抽象是免费的。
代价是,如果一个泛型函数被许多不同类型使用,单态化会增加二进制大小。每个具体 类型都会产生函数的副本。在实践中,这很少成为问题——大多数泛型函数只用于少数几 个类型。
在各语言中的映射:
- Rust 使用相同的单态化策略。行为完全相同。
- Python 不进行单态化。运行时只有一个
identity函数,适用于任何类型。 这很灵活但更慢——Python 必须动态检查类型。 - C 没有泛型,所以转译器手动生成单态化函数。结果与 Auto 的自动单态化相 同。
- TypeScript 在编译时擦除泛型。生成的 JavaScript 有一个适用于任何类型的 单一函数,类似 Python。
泛型枚举
枚举也可以由类型参数化。最重要的例子是 Result<T, E> 和 Option<T>,我们 在本书中一直使用。现在我们将从内部了解它们是如何工作的。
输出:
Result: 3.333333
Error: division by zeroResult<T, E> 是一个带有两个类型参数的泛型枚举:T 用于成功值,E 用于错 误值。Ok 变体携带一个 T,Err 变体携带一个 E。divide 函数返回 Result<float, String>——成功时为浮点数,失败时为字符串错误消息。
is 表达式(Auto 中等价于 Rust 的 match)解构 Result 并让你分别处理每个 变体。编译器确保你处理了 Ok 和 Err 两者——如果你遗漏了一个,就会是编译时 错误。
这个模式是 Auto 中错误处理的基础。每个可能失败的操作返回 Result<T, E>,编译 器强制你处理错误情况。没有未检查的异常,没有因忘记错误处理导致的空指针错误。
C 转译器展示了底层机制:一个带有布尔标签(is_ok)和两个值字段的 ResultFloat 结构体(其中只有一个是有效的)。这是一个_标签联合_——Auto 的枚 举在内部编译成的相同数据结构。
在各语言中的映射:
- Rust 在标准库中有相同的
Result<T, E>枚举。语法和语义完全相同。 - Python 使用
Union[float, str]——一个联合类型。转译器使用isinstance检查代替模式匹配。 - C 使用手动定义的标签联合结构体。程序员必须记得在访问值之前检查
is_ok。 - TypeScript 使用带有
tag字段的可辨识联合类型。模式与 Auto 的is表达式相同。
总结
泛型是 Auto 中编写跨类型工作且不牺牲性能或类型安全的代码的机制。以下是完整 的概览:
| 概念 | Auto | Rust | TypeScript | Python |
|---|---|---|---|---|
| 泛型函数 | fn foo<T>(x T) | fn foo<T>(x: T) | function foo<T>(x: T) | def foo(x: T) |
| 泛型类型 | type Box<T> { value T } | struct Box<T> { value: T } | class Box<T> | class Box(Generic[T]) |
| Spec 约束 | where T: Printable | T: Printable | T extends Printable | Protocol |
| 多重约束 | where T: A, T: B | T: A + B | T extends A & B | -- |
| 泛型方法 | ext Pair<T> { ... } | impl<T> Pair<T> { ... } | 类方法 | 类方法 |
| 泛型枚举 | enum Result<T, E> | enum Result<T, E> | 可辨识联合 | Union |
| 单态化 | 编译时 | 编译时 | 擦除 | 擦除 |
关键要点:
零成本抽象:泛型编译成与手写类型特定函数相同的机器码。没有运行时开销。
类型推断:在大多数情况下,你不需要显式指定类型参数。编译器从参数中推断 它们。
Spec 约束:对类型参数的约束确保泛型代码只能与提供所需功能的类型一起使 用。
单态化:编译器为每个使用的具体类型生成泛型函数的单独副本。这产生快速、 优化的代码,代价是二进制文件略大。
泛型枚举:
Result<T, E>和Option<T>是 Auto 中错误处理和空安全的基 础。
在下一章中,我们将在 Phase 2 总结项目中整合所学的所有知识。