泛型、Spec 与生命周期
每种编程语言都有有效处理概念重复的工具。在 Auto 中,其中一种工具是_泛型_(generics):具体类型或其他属性的抽象替代品。我们可以在编译和运行代码时,无需知道泛型具体代表什么,就能表达泛型的行为或它们之间的关系。
函数可以接受某种泛型类型的参数,而不是像 int 或 String 这样的具体类型,就像函数接受未知值来在多个具体值上运行相同的代码一样。事实上,我们在前面的章节中已经使用了泛型:?T(Option)、!T(Result)、List<T> 和 Map<K, V> 都是泛型类型。
在本章中,我们将探索如何定义自己的泛型类型、函数和方法。然后我们将学习 spec(Auto 中等价于 Rust trait 的概念)来定义共享行为。最后,我们将简要讨论生命周期 —— 以及 Auto 的编译器如何自动处理它们,从而消除对显式生命周期标注的需要。
泛型数据类型
在函数定义中
当定义使用泛型的函数时,我们将泛型放在通常指定数据类型的位置。这使得代码更加灵活,同时避免了重复。
我们读 fn largest<T>(list []T) T 为"函数 largest 对某种类型 T 是泛型的。"该函数接受一个 T 值的列表并返回一个 T。按照惯例,类型参数名称很短,通常只有一个字母,使用 UpperCamelCase —— T("type"的缩写)是默认选择。
语法与 Rust 相同:类型参数放在函数名和参数列表之间的尖括号 <> 中。
在类型定义中
我们可以在类型的字段中定义泛型类型参数:
因为我们只使用了一个泛型类型 T,x 和 y 必须是相同类型。如果每个字段需要不同的类型,请使用多个类型参数:
在方法定义中(ext)
我们可以使用 ext 在泛型类型上定义方法:
注意我们在 ext 之后、Point<T> 之前声明了 T,就像 Rust 的 impl<T> Point<T> 一样。这告诉编译器该方法是为 T 可能代表的任何具体类型定义的。
使用泛型的代码性能
使用泛型没有运行时成本。Auto(像 Rust 一样)使用_单态化_(monomorphization):编译器为每个与泛型一起使用的具体类型生成特定的代码。因此,Point<int> 和 Point<f64> 在编译时成为独立的、优化过的类型。使用泛型没有运行时开销。
用 Spec 定义共享行为
spec("specification"的缩写)定义了特定类型具有的、可以与其他类型共享的功能。Spec 是 Auto 中等价于 Rust trait 的概念(在其他语言中也称为_接口_)。
定义 Spec
类型的行为由我们可以在该类型上调用的方法组成。Spec 定义将方法签名分组在一起,定义一组行为:
spec Summary {
fn summarize(self) String
}这声明了一个名为 Summary 的 spec,带有一个必需方法:summarize,它接受 self 并返回 String。任何实现此 spec 的类型都必须提供自己的 summarize 方法。
在类型上实现 Spec
在 Auto 中,使用 as 来实现 spec:
与 Rust 的关键区别:
| 特性 | Auto | Rust |
|---|---|---|
| 定义 spec/trait | spec Summary { ... } | trait Summary { ... } |
| 在类型上实现 | type Foo as Summary { ... } | impl Summary for Foo { ... } |
| 方法体 | 在单独的 ext 块中 | 在 impl 块内 |
类型声明上的 as 关键字表示"此类型符合该 spec"。实际的方法实现放在 ext 块中。
使用 Spec 作为参数
你可以将 spec 作为参数类型,以接受任何实现了它的类型:
在 Auto 中,fn notify(item Summary) 接受任何类型实现了 Summary spec 的值。这等价于 Rust 的 impl Trait 语法:fn notify(item: impl Summary)。
注意 Auto 的语法更简洁 —— 你不需要 impl 或 Box<dyn Trait> 来传递符合 spec 的值。编译器自动处理调度。
泛型 Spec
Spec 可以有自己的类型参数,就像类型一样:
泛型 spec Container<T> 定义了一个契约:任何实现它的类型必须为内部类型 T 提供 get 和 set 方法。Box<T> 类型实现了 Container<T>,ext 块提供了方法体。
Spec 与 Trait 速查
Auto Rust 含义 spec Foo { ... }trait Foo { ... }定义规范 type Bar as Foo { ... }impl Foo for Bar { ... }在类型上实现 spec fn foo(x SomeSpec)fn foo(x: impl SomeSpec)Spec 作为参数 spec Container<T>trait Container<T>泛型 spec ext Type<T> { ... }impl<T> Type<T> { ... }泛型方法扩展
生命周期:Auto 中自动处理
在 Rust 中,生命周期(lifetimes)是泛型标注,告诉编译器引用应该有效多长时间。它们是必要的,因为 Rust 需要关于引用之间关系的显式信息。
Auto 采用了不同的方法:生命周期是完全自动的。编译器在内部跟踪引用有效性,确保内存安全,而无需你编写任何生命周期标注。
不需要生命周期标注
在 Rust 中,一个返回两个字符串切片中较长者的函数需要显式的生命周期标注:
// Rust 需要显式生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}在 Auto 中,同样的函数完全不需要标注:
Auto 的编译器使用所有权和借用规则(.view、.mut、.move)在编译时跟踪引用有效性,消除了在大多数情况下对显式生命周期参数的需要。
这对你意味着什么
- 函数签名中没有
'a标注 - 类型定义中没有生命周期参数
ext块中没有<'a>声明- 编译器仍然防止悬垂引用和 use-after-free 错误
如果你来自 Rust,可以这样理解:Auto 的编译器激进地应用 Rust 的生命周期省略规则,加上额外的推断,所以你几乎永远不需要显式编写生命周期。
总结
Auto 提供了与 Rust 相同的泛型编程能力,但语法更简洁:
| 概念 | Auto | Rust |
|---|---|---|
| 泛型函数 | fn foo<T>(x T) | fn foo<T>(x: T) |
| 泛型类型 | type Point<T> { x T } | struct Point<T> { x: T } |
| 多类型参数 | type Pair<T, U> | struct Pair<T, U> |
| 泛型方法 | ext Point<T> { ... } | impl<T> Point<T> { ... } |
| Spec (trait) | spec Foo { ... } | trait Foo { ... } |
| 实现 spec | type Bar as Foo { ... } | impl Foo for Bar { ... } |
| Spec 作为参数 | fn foo(x SomeSpec) | fn foo(x: impl SomeSpec) |
| 泛型 spec | spec Storage<T> | trait Storage<T> |
| 生命周期 | 自动 | 显式 'a 标注 |
最显著的简化是 Auto 的自动生命周期推断。在 Rust 需要在函数签名和类型定义上写 <'a> 标注的地方,Auto 的编译器在内部处理这些,让你专注于逻辑而非生命周期管理。
在下一章中,我们将讨论自动化测试。