## 泛型数据类型
> [ch10-01-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch10-01-syntax.md)
>
> commit 55d9e75ffec92e922273c997026bb10613a76578
泛型用于通常我们放置类型的位置,比如函数签名或结构体,允许我们创建可以代替许多具体数据类型的结构体定义。让我们看看如何使用泛型定义函数、结构体、枚举和方法,并且在本部分的结尾我们会讨论泛型代码的性能。
### 在函数定义中使用泛型
定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。
回到`largest`函数上,列表 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从列表 10-3 中提取的寻找 slice 中`i32`最大值的函数。第二个函数寻找 slice 中`char`的最大值:
这里`largest_i32`和`largest_char`有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现。
为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称`T`。任何标识符抖可以作为类型参数名,选择`T`是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。`T`作为“type”是大部分 Rust 程序员的首选。
当需要再函数体中使用一个参数时,必须再函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。
我们将要定义的泛型版本的`largest`函数的签名看起来像这样:
```rust,ignore
fn largest(list: &[T]) -> T {
```
这可以理解为:函数`largest`有泛型类型`T`。它有一个参数`list`,它的类型是一个`T`值的 slice。`largest`函数将会返回一个与`T`相同类型的值。
列表 10-5 展示一个在签名中使用了泛型的统一的`largest`函数定义,并向我们展示了如何对`i32`值的 slice 或`char`值的 slice 调用`largest`函数。注意这些代码还不能编译!
如果现在就尝试编译这些代码,会出现如下错误:
```
error[E0369]: binary operation `>` cannot be applied to type `T`
|
5 | if item > largest {
| ^^^^
|
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
```
注释中提到了`std::cmp::PartialOrd`,这是一个 *trait*。下一部分会讲到 trait,不过简单来说,这个错误表明`largest`的函数体对`T`的所有可能的类型都无法工作;因为在函数体需要比较`T`类型的值,不过它只能用于我们知道如何排序的类型。标准库中定义的`std::cmp::PartialOrd` trait 可以实现类型的排序功能。在下一部分会再次回到 trait 并讲解如何为泛型指定一个 trait,不过让我们先把这个例子放在一边并探索其他那些可以使用泛型类型参数的地方。
### 结构体定义中的泛型
同样也可以使用`<>`语法来定义拥有一个或多个泛型参数类型字段的结构体。列表 10-6 展示了如何定义和使用一个可以存放任何类型的`x`和`y`坐标值的结构体`Point`:
其语法类似于函数定义中的泛型应用。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。
注意`Point`的定义中是使用了要给泛型类型,我们想要表达的是结构体`Point`对于一些类型`T`是泛型的,而且无论这个泛型是什么,字段`x`和`y`**都是**相同类型的。如果尝试创建一个有不同类型值的`Point`的实例,像列表 10-7 中的代码就不能编译:
尝试编译会得到如下错误:
```
error[E0308]: mismatched types
-->
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integral variable, found
floating-point variable
|
= note: expected type `{integer}`
= note: found type `{float}`
```
当我们将 5 赋值给`x`,编译器就知道这个`Point`实例的泛型类型`T`是一个整型。接着我们将`y`指定为 4.0,而它被定义为与`x`有着相同的类型,所以出现了类型不匹配的错误。
如果想要一个`x`和`y`可以有不同类型且仍然是泛型的`Point`结构体,我们可以使用多个泛型类型参数。在列表 10-8 中,我们修改`Point`的定义为拥有两个泛型类型`T`和`U`。其中字段`x`是`T`类型的,而字段`y`是`U`类型的:
现在所有这些`Point`实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。如果你处于一个需要很多泛型类型的位置,这可能是一个需要重新组织代码并分隔成一些更小部分的信号。
### 枚举定义中的泛型数据类型
类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的`Option`枚举,现在这个定义看起来就更容易理解了。让我们再看看:
```rust
enum Option {
Some(T),
None,
}
```
换句话说`Option`是一个拥有泛型`T`的枚举。它有两个成员:`Some`,它存放了一个类型`T`的值,和不存在任何值的`None`。标准库中只有这一个定义来支持创建任何具体类型的枚举值。“一个可能的值”是一个比具体类型的值更抽象的概念,而 Rust 允许我们不引入重复就能表现抽象的概念。
枚举也可以拥有多个泛型类型。第九章使用过的`Result`枚举定义就是一个这样的例子:
```rust
enum Result {
Ok(T),
Err(E),
}
```
`Result`枚举有两个泛型类型,`T`和`E`。`Result`有两个成员:`Ok`,它存放一个类型`T`的值,而`Err`则存放一个类型`E`的值。这个定义使得`Result`枚举能很方便的表达任何可能成功(返回`T`类型的值)也可能失败(返回`E`类型的值)的操作。回忆一下列表 9-2 中打开一个文件的场景,当文件被成功打开`T`被放入了`std::fs::File`类型而当打开文件出现问题时`E`被放入了`std::io::Error`类型。
当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复。
### 方法定义中的枚举数据类型
可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体`Point`。接着我们在`Point`上定义了一个叫做`x`的方法来返回字段`x`中数据的引用:
注意必须在`impl`后面声明`T`,这样就可以在`Point`上实现的方法中使用它了。
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。列表 10-10 中在列表 10-8 中的结构体`Point`上定义了一个方法`mixup`。这个方法获取另一个`Point`作为参数,而它可能与调用`mixup`的`self`是不同的`Point`类型。这个方法用`self`的`Point`类型的`x`值(类型`T`)和参数的`Point`类型的`y`值(类型`W`)来创建一个新`Point`类型的实例:
在`main`函数中,定义了一个有`i32`类型的`x`(其值为`5`)和`f64`的`y`(其值为`10.4`)的`Point`。`p2`则是一个有着字符串 slice 类型的`x`(其值为`"Hello"`)和`char`类型的`y`(其值为`c`)的`Point`。在`p1`上以`p2`调用`mixup`会返回一个`p3`,它会有一个`i32`类型的`x`,因为`x`来自`p1`,并拥有一个`char`类型的`y`,因为`y`来自`p2`。`println!`会打印出`p3.x = 5, p3.y = c`。
注意泛型参数`T`和`U`声明于`impl`之后,因为他们于结构体定义相对应。而泛型参数`V`和`W`声明于`fn mixup`之后,因为他们只是相对于方法本身的。
### 泛型代码的性能
在阅读本部分的内容的同时你可能会好奇使用泛型类型参数是否会有运行时消耗。好消息是:Rust 实现泛型泛型的方式意味着你的代码使用泛型类型参数相比指定具体类型并没有任何速度上的损失。
Rust 通过在编译时进行泛型代码的**单态化**(*monomorphization*)来保证效率。单态化是一个将泛型代码转变为实际放入的具体类型的特定代码的过程。
编译器所做的工作正好与列表 10-5 中我们创建泛型函数的步骤相反。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。
让我们看看一个使用标准库中`Option`枚举的例子:
```rust
let integer = Some(5);
let float = Some(5.0);
```
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给`Option`的值并发现有两种`Option`:一个对应`i32`另一个对应`f64`。为此,它会将泛型定义`Option`展开为`Option_i32`和`Option_f64`,接着将泛型定义替换为这两个具体的定义。
编译器生成的单态化版本的代码看起来像这样,并包含将泛型`Option`替换为编译器创建的具体定义后的用例代码:
Filename: src/main.rs
```rust
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
```
我们可以使用泛型来编写不重复的代码,而 Rust 会将会为每一个实例编译其特定类型的代码。这意味着在使用泛型时没有运行时开销;当代码运行,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是 Rust 泛型在运行时极其高效的原因。