每一个编程语言都有高效处理重复概念的工具。在 Rust 中其工具之一就是 泛型(generics)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。
同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像 i32
或 String
这样的具体类型。
首先,我们将回顾一下提取函数以减少代码重复的机制。接下来,我们将使用相同的技术,从两个仅参数类型不同的函数中创建一个泛型函数。我们也会讲到结构体和枚举定义中的泛型。
之后,我们讨论 trait,这是一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型。
最后介绍 生命周期(lifetimes),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值的同时仍然使编译器能够检查这些引用的有效性。
提取函数来减少重复
在介绍泛型语法之前,首先来回顾一个不使 用泛型处理重复的技术:提取一个函数。当熟悉了这个技术以后,我们将使用相同的机制来提取一个泛型函数!如同你识别出可以提取到函数中重复代码那样,你也会开始识别出能够使用泛型的重复代码。
考虑一下这个寻找列表中最大值的小程序,如示例 10-1 所示:
文件名: src/main.rs
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);
}
示例 10-1:在一个数字列表中寻找最大值的函数
这段代码获取一个整型列表,存放在变量 number_list
中。它将列表的第一项放入了变量 largest
中。接着遍历了列表中的所有数字,如果当前值大于 largest
中储存的值,将 largest
替换为这个值。如果当前值小于或者等于目前为止的最大值,largest
保持不变。当列表中所有值都被考虑到之后,largest
将会是最大值,在这里也就是 100
。
如果需要在两个不同的列表中寻找最大值,我们可以重复示例 10-1 中的代码,这样程序中就会存在两段相同逻辑的代码,如示例 10-2 所示:
文件名: src/main.rs
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);
}
示例 10-2:寻找 两个 数字列表最大值的代码
虽然代码能够执行,但是重复的代码是冗余且容易出错的,并且意味着当更新逻辑时需要修改多处地方的代码。
为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独立。
在示例 10-3 的程序中将寻找最大值的代码提取到了一个叫做 largest
的函数中。这不同于示例 10-1 中的代码只能在一个特定的列表中找到最大的数字,这个程序可以在两个不同的列表中找到最大的数字。
文件名: src/main.rs
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {}", result);
}
示例 10-3:抽象后的寻找两个数字列表最大值的代码
largest
函数有一个参数 list
,它代表会传递给函数的任何具体的 i32
值的 slice
。函数定义中的 list
代表任何 &[i32]
。当调用 largest
函数时,其代码实际上运行于我们传递的特定值上。
总的来说,从示例 10-2 到示例 10-3 中涉及的机制经历了如下几步:
- 找出重复代码。
- 将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。
- 将重复代码的两个实例,改为调用函数。
在不同的场景使用不同的方式,我们也可以利用相同的步骤和泛型来减少重复代码。与函数体可以在抽象 list
而不是特定值上操作的方式相同,泛型允许代码对抽象类型进行操作。
如果我们有两个函数,一个寻找 i32
值的 slice
中的最大项,而另一个寻找 char
值的 slice
中的最大项,该怎么办?该如何消除重复呢?让我们拭目以待!
泛型数据类型
我们可以使用泛型为函数签名或结构体等项创建定义,这样它们就可以用于多种不同的具体数据类型。让我们看看如何使用泛型定义函数、结构体、枚举和方法,然后我们将讨论泛型如何影响代码性能。
在函数定义中使用泛型
当使用泛型定义函数时,本来在函数签名中指定参数和返回值的类型的地方,会改用泛型来表示。采用这种技术,使得代码适应性更强,从而为函数的调用者提供更多的功能,同时也避免了代码的重复。
回到 largest
函数,示例 10-4 中展示了两个函数,它们的功能都是寻找 slice
中最大值。
文件名: src/main.rs
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> char {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {}", result);
}
示例 10-4:两个函数,不同点只是名称和签名类型
largest_i32
函数是从示例 10-3 中摘出来的,它用来寻找 slice
中最大的 i32
。largest_char
函数寻找 slice
中最大的 char
。因为两者函数体的代码一致,我们可以定义一个函数,再引进泛型参数来消除这种重复。
为了参数化新函数中的这些类型,我们也需要为类型参数取个名字,道理和给函数的形参起名一样。任何标识符都可以作为类型参数的名字。这里选用 T
,因为传统上来说,Rust 的参数名字都比较短,通常就只有一个字母,同时,Rust 类型名的命名规范是骆驼命名法(CamelCase)。T
作为 “type” 的缩写是大部分 Rust 开发者的首选。
如果要在函数体中使用参数,就必须在函数签名中声明它的名字,好让编译器知道这个名字指代的是什么。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。为了定义泛型版本的 largest
函数,类型参数声明位于函数名称与参数列表中间的尖括号 <>
中,像这样:
fn largest<T>(list: &[T]) -> T {}
可以这样理解这个定义:函数 largest
有泛型类型 T
。它有个参数 list
,其类型是元素为 T
的 slice
。largest
函数的返回值类型也是 T
。
示例 10-5 中的 largest
函数在它的签名中使用了泛型,统一了两个实现。该示例也展示了如何调用 largest
函数,把 i32
值的 slice
或 char
值的 slice
传给它。请注意这些代码还不能编译,不过稍后在本章会解决这个问题。
文件名: src/main.rs
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
示例 10-5:一个使用泛型参数的 largest
函数定义,尚不能编译
如果现在就编译这个代码,会出现如下错误:
PS E:\github\rust-projects\generate_type> cargo run
Compiling generate_type v0.1.0 (E:\github\rust-projects\generate_type)
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src\main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- T
| |
| T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `generate_type` (bin "generate_type") due to previous error
注释中提到了 std::cmp::PartialOrd
,这是一个 trait。下一部分会讲到 trait。不过简单来说,这个错误表明 largest 的函数体不能适用于 T 的所有可能的类型。因为在函数体需要比较 T 类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 std::cmp::PartialOrd trait
可以实现类型的比较功能。
标准库中定义的 std::cmp::PartialOrd trait
可以实现类型的比较功能。在 “trait 作为参数” 部分会讲解如何指定泛型实现特定的 trait,不过让我们先探索其他使用泛型参数的方法。
结构体定义中的泛型
同样也可以用 <>
语法来定义结构体,它包含一个或多个泛型参数类型字段。示例 10-6 展示了如何定义和使用一个可以存放任何类型的 x
和 y
坐标值的结构体 Point
:
文件名: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
println!("The integer point is ({}, {})", integer.x, integer.y);
println!("The float point is ({}, {})", float.x, float.y);
}
示例 10-6:Point
结构体存放了两个 T
类型的值 x
和 y
其语法类似于函数定义中使用泛型。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。
注意 Point<T>
的定义中只使用了一个泛型类型,这个定义表明结构体 Point<T>
对于一些类型 T
是泛型的,而且字段 x
和 y
都是 相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的 Point<T>
的实例,像示例 10-7 中的代码就不能编译:
文件名: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
示例 10-7:字段 x
和 y
的类型必须相同,因为他们都有相同的泛型类型 T
在这个例子中,当把整型值 5
赋值给 x
时,就告诉了编译器这个 Point<T>
实例中的泛型 T
是整型的。接着指定 y
为 4.0
,它被定义为与 x
相同类型,就会得到一个像这样的类型不匹配错误:
PS E:\github\rust-projects\generate_type> cargo run
Compiling generate_type v0.1.0 (E:\github\rust-projects\generate_type)
error[E0308]: mismatched types
--> src\main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number
For more information about this error, try `rustc --explain E0308`.
error: could not compile `generate_type` (bin "generate_type") due to previous error
如果想要定义一个 x
和 y
可以有不同类型且仍然是泛型的 Point
结构体,我们可以使用 多个泛型类型参数。在示例 10-8 中,我们修改 Point
的定义为拥有两个泛型类型 T
和 U
。其中字段 x
是 T
类型的,而字段 y
是 U
类型的:
文件名: src/main.rs
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point {x: 5, y: 10,};
let both_float = Point {x: 1.0, y: 4.0,};
let integer_and_float = Point {x: 5, y: 4.0,};
}
示例 10-8:使用两个泛型的 Point
,这样 x
和 y
可能是不同类型
现在所有的 Point
实例都合法了!你可以在定义中使用任意多的泛型类型参数,不过太多的话,代码将难以阅读和理解。当你的代码中需要许多泛型类型时,它可能表明你的代码需要重构,分解成更小的结构。