为什么需要泛型
你很快就会遇到一些应该适用于多种元素类型的类型。由两个值组成的“对”并不关心这两个值是整数、字符串还是某个用户定义的 shape。泛型让你只写一次类型,再用调用方需要的任何元素类型来实例化它。
另一种做法是分别写 IntPair、StringPair、BytePair 等等,很快就会变得枯燥,而且无法组合。在 Zero 中,泛型就是解决这个问题的标准工具。
泛型函数
在函数名和参数列表之间的尖括号中声明类型参数:
fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
return Pair { left: left, right: right }
}
T 和 U 是占位类型,由调用方决定它们具体是什么。
调用时通常不需要写出它们,编译器会根据实参类型推断:
let pair = makePair(40, 2_u8)
这里 T 被推断为 i32(没有后缀的整数字面量的默认类型),U 是 u8(来自 _u8 后缀)。得到的绑定 pair 的类型是 Pair<i32, u8>。
如果推断会得出错误的类型,比如字面量有歧义时,你可以在调用处指定参数:
let pair = makePair<u8, u8>(1, 2)
(不同 Zero 版本中,尖括号调用语法是否与上面完全一致可能有所不同;确切写法请查看最新文档。优先推断这一行为才是稳定的部分。)
泛型 shape
shape 以同样的方式接收类型参数:
shape Pair<T, U> {
left: T,
right: U,
}
每个字段的类型都可以使用这些参数。实例会把它们确定下来:
let intBytes: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words: Pair<String, String> = Pair { left: "hi", right: "there" }
下面这个完整示例同时用到了泛型 shape 和泛型函数,点击“运行”看看类型推断的效果:
一个泛型 shape 声明,一个泛型函数,一个强类型的调用处。不需要写 IntBytePair 这样的样板代码。
类型别名
当同一个参数化类型反复出现时,用 type 给它起个名字:
type BytePair = Pair<u8, u8>
现在,在任何能写类型的地方,BytePair 都可以和 Pair<u8, u8> 互换:
别名只是一种命名功能,它不会创建一个独立的类型。接收 BytePair 的函数同样可以接受 Pair<u8, u8> 类型的值(反之亦然)。
标准库中的泛型
标准库的很多部分都由同一机制驱动。你在实际的 Zero 代码中会看到这些:
Maybe<T>:可选值,要么保存一个T,要么什么都没有。Span<T>:对一组T值的借用切片。Span<u8>是查看字节缓冲区的标准方式。ref<T>和mutref<T>:显式的引用类型,用于需要共享数据而不复制的场景。
你不必一次全部学会。泛型的意义就在于,同一个 shape 适用于你手头的任何元素类型。
什么时候值得用泛型(什么时候不值得)
当你发现自己为不同的元素类型把同一个函数或 shape 写了两遍时,就该用泛型了。在以下情况下,用具体类型更好:
- 函数的逻辑只对某个特定类型有意义(比如专门解析
String的解析器)。 - 你希望类型出现在错误信息中,方便调试。
- 性能特征取决于特定的内存大小。
泛型是有代价的:二进制文件更大(每次实例化都会生成新代码),编译时间也会稍长。对大多数应用代码来说,这点代价可以忽略不计;但当你编写对二进制大小敏感的紧凑型嵌入式代码时,最好心里有数。
关于约束
有些泛型系统允许你约束类型参数(“T 必须支持 ==”、“T 必须实现 Iterator”)。1.0 之前的 Zero 中,约束的设计还在演进:官方仓库中的示例都以最朴素的形式使用泛型,没有复杂的约束条件。随着语言逐渐稳定,约束语法很可能会以一种与语言其他部分一致的、小而规整的形式加入。目前,请写出对你实际传入的任何 T 都能工作的泛型,遇到不支持的操作时,编译器会告诉你。
下一步:枚举
泛型让你可以对类型进行参数化。下一个构件则位于另一端:枚举,这是 Zero 中用于变体不携带额外数据的普通枚举类型。
常见问题
Zero 中的泛型是怎么工作的?
在函数名或 shape 名后面的尖括号中声明类型参数:fun makePair<T, U>(left: T, right: U) -> Pair<T, U> 或 shape Pair<T, U> { left: T, right: U }。调用方可以显式指定参数(Pair<i32, u8>),也可以让编译器根据调用时的实参推断。
Zero 中的 shape 可以是泛型的吗?
可以。shape 可以用与函数相同的尖括号语法接收类型参数:shape Pair<T, U> { left: T, right: U }。每个字段的类型都可以使用这些参数。写出参数化的类型就能构成实例,例如 Pair<i32, u8>。
调用泛型函数时必须指定类型参数吗?
通常不需要。编译器会根据实参类型推断。调用 makePair(40, 2_u8) 就够了:T 变成 i32,U 变成 u8。当推断结果不对,或者你想在调用处写明类型时,可以显式指定参数。
Zero 中的类型别名是什么?
类型别名是较长类型表达式的简写名称。type BytePair = Pair<u8, u8> 让你可以在原本要写 Pair<u8, u8> 的任何地方写 BytePair。别名纯粹是命名功能:它不会引入新类型,只是提供引用已有类型的更短方式。
泛型在 Zero 标准库中用在哪里?
到处都是:只要某个类型需要保存或处理任意元素类型,就会用到泛型。比如表示可选值的 Maybe<T>、表示字节切片的 Span<u8>,以及按元素类型参数化的容器类型。用户定义的类型和标准库类型使用的是同一套泛型机制。