|
|
|
@ -2,7 +2,7 @@
|
|
|
|
|
|
|
|
|
|
> [ch10-01-syntax.md](https://github.com/rust-lang/book/blob/main/src/ch10-01-syntax.md)
|
|
|
|
|
> <br>
|
|
|
|
|
> commit 6a23b2c7ffe392d1329855444a1b3a88e93982b6
|
|
|
|
|
> commit e7c688559e6881257760dfe58567e19e59b14179
|
|
|
|
|
|
|
|
|
|
我们可以使用泛型为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的具体数据类型。让我们看看如何使用泛型定义函数、结构体、枚举和方法,然后我们将讨论泛型如何影响代码性能。
|
|
|
|
|
|
|
|
|
@ -10,7 +10,7 @@
|
|
|
|
|
|
|
|
|
|
当使用泛型定义函数时,本来在函数签名中指定参数和返回值的类型的地方,会改用泛型来表示。采用这种技术,使得代码适应性更强,从而为函数的调用者提供更多的功能,同时也避免了代码的重复。
|
|
|
|
|
|
|
|
|
|
回到 `largest` 函数,示例 10-4 中展示了两个函数,它们的功能都是寻找 slice 中最大值。
|
|
|
|
|
回到 `largest` 函数,示例 10-4 中展示了两个函数,它们的功能都是寻找 slice 中最大值。接着我们使用泛型将其合并为一个函数。
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
@ -22,7 +22,7 @@
|
|
|
|
|
|
|
|
|
|
`largest_i32` 函数是从示例 10-3 中摘出来的,它用来寻找 slice 中最大的 `i32`。`largest_char` 函数寻找 slice 中最大的 `char`。因为两者函数体的代码是一样的,我们可以定义一个函数,再引进泛型参数来消除这种重复。
|
|
|
|
|
|
|
|
|
|
为了参数化新函数中的这些类型,我们也需要为类型参数取个名字,道理和给函数的形参起名一样。任何标识符都可以作为类型参数的名字。这里选用 `T`,因为传统上来说,Rust 的参数名字都比较短,通常就只有一个字母,同时,Rust 类型名的命名规范是骆驼命名法(CamelCase)。`T` 作为 “type” 的缩写是大部分 Rust 程序员的首选。
|
|
|
|
|
为了参数化这个新函数中的这些类型,我们也需要为类型参数取个名字,道理和给函数的形参起名一样。任何标识符都可以作为类型参数的名字。这里选用 `T`,因为传统上来说,Rust 的参数名字都比较短,通常就只有一个字母,同时,Rust 类型名的命名规范是骆驼命名法(CamelCase)。`T` 作为 “type” 的缩写是大部分 Rust 程序员的首选。
|
|
|
|
|
|
|
|
|
|
如果要在函数体中使用参数,就必须在函数签名中声明它的名字,好让编译器知道这个名字指代的是什么。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。为了定义泛型版本的 `largest` 函数,类型参数声明位于函数名称与参数列表中间的尖括号 `<>` 中,像这样:
|
|
|
|
|
|
|
|
|
@ -30,7 +30,7 @@
|
|
|
|
|
fn largest<T>(list: &[T]) -> T {
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
可以这样理解这个定义:函数 `largest` 有泛型类型 `T`。它有个参数 `list`,其类型是元素为 `T` 的 slice。`largest` 函数的返回值类型也是 `T`。
|
|
|
|
|
可以这样理解这个定义:函数 `largest` 有泛型类型 `T`。它有个参数 `list`,其类型是元素为 `T` 的 slice。`largest` 函数会返回一个与 `T` 相同类型的引用。
|
|
|
|
|
|
|
|
|
|
示例 10-5 中的 `largest` 函数在它的签名中使用了泛型,统一了两个实现。该示例也展示了如何调用 `largest` 函数,把 `i32` 值的 slice 或 `char` 值的 slice 传给它。请注意这些代码还不能编译,不过稍后在本章会解决这个问题。
|
|
|
|
|
|
|
|
|
@ -48,13 +48,11 @@ fn largest<T>(list: &[T]) -> T {
|
|
|
|
|
{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-05/output.txt}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
注释中提到了 `std::cmp::PartialOrd`,这是一个 *trait*。下一部分会讲到 trait。不过简单来说,这个错误表明 `largest` 的函数体不能适用于 `T` 的所有可能的类型。因为在函数体需要比较 `T` 类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 `std::cmp::PartialOrd` trait 可以实现类型的比较功能(查看附录 C 获取该 trait 的更多信息)。
|
|
|
|
|
|
|
|
|
|
标准库中定义的 `std::cmp::PartialOrd` trait 可以实现类型的比较功能。在 [“trait 作为参数”][traits-as-parameters] 部分会讲解如何指定泛型实现特定的 trait,不过让我们先探索其他使用泛型参数的方法。
|
|
|
|
|
帮助说明中提到了 `std::cmp::PartialOrd`,这是一个 *trait*。下一部分会讲到 trait。不过简单来说,这个错误表明 `largest` 的函数体不能适用于 `T` 的所有可能的类型。因为在函数体需要比较 `T` 类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 `std::cmp::PartialOrd` trait 可以实现类型的比较功能(查看附录 C 获取该 trait 的更多信息)。标准库中定义的 `std::cmp::PartialOrd` trait 可以实现类型的比较功能。依照帮助说明中的建议,我们限制 `T` 只对实现了 `PartialOrd` 的类型有效后代码就可以编译了,因为标准库为 `i32` 和 `char` 实现了 `PartialOrd`。
|
|
|
|
|
|
|
|
|
|
### 结构体定义中的泛型
|
|
|
|
|
|
|
|
|
|
同样也可以用 `<>` 语法来定义结构体,它包含一个或多个泛型参数类型字段。示例 10-6 展示了如何定义和使用一个可以存放任何类型的 `x` 和 `y` 坐标值的结构体 `Point`:
|
|
|
|
|
同样也可以用 `<>` 语法来定义结构体,它包含一个或多个泛型参数类型字段。示例 10-6 定义了一个可以存放任何类型的 `x` 和 `y` 坐标值的结构体 `Point`:
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
@ -92,7 +90,7 @@ fn largest<T>(list: &[T]) -> T {
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 10-8:使用两个泛型的 `Point`,这样 `x` 和 `y` 可能是不同类型</span>
|
|
|
|
|
|
|
|
|
|
现在所有这些 `Point` 实例都合法了!你可以在定义中使用任意多的泛型类型参数,不过太多的话,代码将难以阅读和理解。当你的代码中需要许多泛型类型时,它可能表明你的代码需要重构,分解成更小的结构。
|
|
|
|
|
现在所有这些 `Point` 实例都合法了!你可以在定义中使用任意多的泛型类型参数,不过太多的话,代码将难以阅读和理解。当你发现代码中需要很多泛型时,这可能表明你的代码需要重构分解成更小的结构。
|
|
|
|
|
|
|
|
|
|
### 枚举定义中的泛型
|
|
|
|
|
|
|
|
|
@ -134,9 +132,11 @@ enum Result<T, E> {
|
|
|
|
|
|
|
|
|
|
这里在 `Point<T>` 上定义了一个叫做 `x` 的方法来返回字段 `x` 中数据的引用:
|
|
|
|
|
|
|
|
|
|
注意必须在 `impl` 后面声明 `T`,这样就可以在 `Point<T>` 上实现的方法中使用它了。在 `impl` 之后声明泛型 `T` ,这样 Rust 就知道 `Point` 的尖括号中的类型是泛型而不是具体类型。因为再次声明了泛型,我们可以为泛型参数选择一个与结构体定义中声明的泛型参数所不同的名称,不过依照惯例使用了相同的名称。`impl` 中编写的方法声明了泛型类型可以定位为任何类型的实例,不管最终替换泛型类型的是何具体类型。
|
|
|
|
|
注意必须在 `impl` 后面声明 `T`,这样就可以在 `Point<T>` 上实现的方法中使用 `T` 了。通过在 `impl` 之后声明泛型 `T`,Rust 就知道 `Point` 的尖括号中的类型是泛型而不是具体类型。我们可以为泛型参数选择一个与结构体定义中声明的泛型参数所不同的名称,不过依照惯例使用了相同的名称。`impl` 中编写的方法声明了泛型类型可以定位为任何类型的实例,不管最终替换泛型类型的是何具体类型。
|
|
|
|
|
|
|
|
|
|
定义方法时也可以为泛型指定限制(constraint)。例如,可以选择为 `Point<f32>` 实例实现方法,而不是为泛型 `Point` 实例。示例 10-10 展示了一个没有在 `impl` 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,`f32`:
|
|
|
|
|
|
|
|
|
|
另一个选择是定义方法适用于某些有限制(constraint)的泛型类型。例如,可以选择为 `Point<f32>` 实例实现方法,而不是为泛型 `Point` 实例。示例 10-10 展示了一个没有在 `impl` 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,`f32`:
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-10/src/main.rs:here}}
|
|
|
|
@ -162,22 +162,22 @@ enum Result<T, E> {
|
|
|
|
|
|
|
|
|
|
### 泛型代码的性能
|
|
|
|
|
|
|
|
|
|
在阅读本部分内容的同时,你可能会好奇使用泛型类型参数是否会有运行时消耗。好消息是:Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失。
|
|
|
|
|
在阅读本部分内容的同时,你可能会好奇使用泛型类型参数是否会有运行时消耗。好消息是泛型并不会使程序比具体类型运行得慢。
|
|
|
|
|
|
|
|
|
|
Rust 通过在编译时进行泛型代码的 **单态化**(*monomorphization*)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
|
|
|
|
|
|
|
|
|
|
编译器所做的工作正好与示例 10-5 中我们创建泛型函数的步骤相反。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。
|
|
|
|
|
这这个过程中,编译器所做的工作正好与示例 10-5 中我们创建泛型函数的步骤相反。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。
|
|
|
|
|
|
|
|
|
|
让我们看看一个使用标准库中 `Option` 枚举的例子:
|
|
|
|
|
让我们看看这如何用于标准库中的 `Option` 枚举:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
let integer = Some(5);
|
|
|
|
|
let float = Some(5.0);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 `Option<T>` 的值并发现有两种 `Option<T>`:一个对应 `i32` 另一个对应 `f64`。为此,它会将泛型定义 `Option<T>` 展开为 `Option_i32` 和 `Option_f64`,接着将泛型定义替换为这两个具体的定义。
|
|
|
|
|
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 `Option<T>` 的值并发现有两种 `Option<T>`:一个对应 `i32` 另一个对应 `f64`。为此,它会将泛型定义 `Option<T>` 展开为两个针对 `i32` 和 `f64` 的定义,接着将泛型定义替换为这两个具体的定义。
|
|
|
|
|
|
|
|
|
|
编译器生成的单态化版本的代码看起来像这样,并包含将泛型 `Option<T>` 替换为编译器创建的具体定义后的用例代码:
|
|
|
|
|
编译器生成的单态化版本的代码看起来像这样(编译器会使用不同于如下假想的名字):
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
@ -198,6 +198,4 @@ fn main() {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
我们可以使用泛型来编写不重复的代码,而 Rust 将会为每一个实例编译其特定类型的代码。这意味着在使用泛型时没有运行时开销;当代码运行,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是 Rust 泛型在运行时极其高效的原因。
|
|
|
|
|
|
|
|
|
|
[traits-as-parameters]: ch10-02-traits.html#trait-作为参数
|
|
|
|
|
泛型 `Option<T>` 被编译器替换为了具体的定义。因为 Rust 会将每种情况下的泛型代码编译为具体类型,使用泛型没有运行时开销。当代码运行时,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是 Rust 泛型在运行时极其高效的原因。
|
|
|
|
|