Menu

Zero 泛型:函数和 Shape 的类型参数

Zero 中的泛型怎么用:在函数和 shape 上声明类型参数、调用泛型函数,以及用类型别名把冗长的参数组合变成简洁的名字。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

为什么需要泛型

你很快就会遇到一些应该适用于多种元素类型的类型。由两个值组成的“对”并不关心这两个值是整数、字符串还是某个用户定义的 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>,以及按元素类型参数化的容器类型。用户定义的类型和标准库类型使用的是同一套泛型机制。

Coddy 编程语言插图

和 Coddy 一起学编程

开始