泛型数据类型
我们可以使用泛型来定义可用于多种具体类型的函数、类型和其他构造。让我们先看看如何定义函数、结构体、枚举和方法,然后再讨论 trait 以及它们如何与泛型协同工作。
首先,让我们回顾一下如何编写一个找出列表中最大整数的函数。
function largest_integer(list: List<integer>) -> integer {
let mutable largest = list[0]
for item in list {
if item > largest {
largest = item
}
}
largest
}
这个函数接受一个 List<integer> 作为参数,并返回列表中的最大 integer。如果我们想找出字符列表或浮点数列表中的最大值,该怎么办?我们必须为每种类型复制并粘贴相同的逻辑!
为了避免这种重复,我们可以使用泛型类型参数来定义一个可以处理任何类型的函数。泛型允许我们抽象出处理特定类型的细节,使我们的代码更灵活和可重用。
在函数定义中使用泛型
当我们使用泛型定义函数时,我们将泛型类型参数放在函数签名中,位于函数名称之后和参数列表之前,用尖括号 <> 括起来。
让我们使用泛型重写 largest 函数,使其可以处理任何类型:
function largest<T>(list: List<T>) -> T {
let mutable largest = list[0]
for item in list {
if item > largest {
largest = item
}
}
largest
}
等等,这里有个问题!我们的 largest 函数现在使用了类型 T,但我们不知道任何类型 T 都可以与 > 运算符比较。为了使这个函数工作,T 需要实现比较 trait。我们将在下一章讨论 trait 时回到这个问题。但首先,让我们探讨一下如何在其他地方使用泛型类型参数。
目前,让我们看看如何在结构体定义中使用泛型。
在结构体定义中使用泛型
我们也可以在一个或多个字段中使用泛型类型参数来定义结构体。语法与函数定义类似:我们将类型参数名称放在结构体名称之后的尖括号中。
type Point<T> = {
x: T,
y: T
}
这个 Point 结构体是泛型的,适用于某种类型 T,并且它有两个字段 x 和 y,它们都是同一类型 T。让我们创建 Point 实例:
let integer_point = { x: 5, y: 10 }
let float_point = { x: 1.0, y: 4.0 }
注意,因为我们只在 Point 结构体的定义中使用了一个泛型类型参数,所以 x 和 y 必须是同一类型。如果我们尝试创建一个 x 是整数而 y 是浮点数的 Point 实例,这将无法工作。
如果我们希望 x 和 y 是不同类型的,我们可以使用多个泛型类型参数:
type Point<T, U> = {
x: T,
y: U
}
现在 x 和 y 可以是不同类型:
let both_integer = { x: 5, y: 10 }
let both_float = { x: 1.0, y: 4.0 }
let integer_and_float = { x: 5, y: 4.0 }
在枚举定义中使用泛型
正如我们在第 4 章中看到的,我们也可以在枚举中使用泛型类型参数来让枚举的变体持有泛型数据类型。让我们回顾一下标准库提供的 Option<T> 枚举:
type Option<T> = Some(T) | None
这个定义现在应该更容易理解了。Option 是一个泛型枚举,适用于某种类型 T,并且它有两个变体:Some,它持有一个 T 类型的值,和 None,它不持有任何值。通过使用 Option 枚举,我们可以表达一个可选值的概念,而且因为 Option 是泛型的,我们可以将这个概念用于任何类型。
正如我们在第 7 章中看到的,Result 枚举是另一个泛型枚举。Result 有两个类型参数:T 和 E:
type Result<T, E> = Ok(T) | Err(E)
Result 枚举用于操作可能成功(返回类型 T 的值)或失败(返回类型 E 的错误值)。
在方法定义中使用泛型
我们也可以在方法定义中使用泛型。让我们为 Point<T> 结构体实现一个方法,名为 x,它返回对字段 x 中数据的引用:
type Point<T> = {
x: T,
y: T
}
function Point::x<T>(self: &Point<T>) -> &T {
&self.x
}
let p = { x: 5, y: 10 }
println("p.x = ", p.x())
注意,我们必须在 impl 之后声明 T,以便我们可以在我们正在为其实现方法的类型 Point<T> 上使用它。通过在 impl 之后的尖括号中声明 T 为泛型类型,X 语言可以识别出尖括号中的类型是泛型类型而不是具体类型。
我们还可以指定对泛型类型的约束,只允许具有特定属性的类型拥有方法。我们将在下一章中讨论如何使用 trait 来做到这一点。
泛型代码的性能
你可能想知道在使用泛型类型参数时是否会有运行时成本。好消息是,X 语言实现泛型的方式使得使用泛型的代码不会比使用具体类型的代码慢!
X 语言通过在编译时对使用泛型的代码进行单态化来实现这一点。单态化是通过填充编译时使用的具体类型将通用代码转换为特定代码的过程。
在这个过程中,编译器做了我们在本章开始时为了创建 largest 函数的两个版本而做的相反的事情:编译器采用通用定义,并为实际代码中使用的具体类型生成特定定义。
例如,让我们看看标准库中的 Option 是如何工作的:
let integer = Some(5)
let float = Some(5.0)
当 X 语言编译这段代码时,它会执行单态化。在这个过程中,编译器读取已在 Option 实例中使用的值,并标识出两种 Option:一种是 integer,另一种是 Float。因此,它将通用定义 Option<T> 扩展为两个特定于类型的定义,用具体类型替换 T。
单态化的结果是,泛型的特定版本就像我们手动复制了定义一样。X 语言的泛型系统意味着我们编写的代码更少,但最终运行的代码更多。
总结
在本章中,我们学习了如何使用泛型类型参数来避免代码重复。我们已经看到了如何在函数、结构体、枚举和方法中使用泛型类型参数!
接下来,让我们讨论 trait,我们可以用它来定义多个类型共有的行为。我们可以将 trait 与泛型类型参数结合使用,将泛型类型参数限制为仅具有特定行为的类型,而不是任何类型。