基本所有的编程语言都会有用来简化开发,减少重复的工具。泛型(generics)就是 Rust 提供的工具之一 。泛型是具体数据类型的抽象替代,让我们可以在运行时才指定数据类型。也就是说在我们编码时,我们就可以对泛型对象进行操作,比如调用他们的行为方法(trait)或编写和其它泛型之间的逻辑关系,而不需要在编写和编译时指定他们的具体类型。
函数的参数定义中经常会使用到泛型,也就是说我们在定义函数时并不需要知道其参数的具体类型,这样我们可以让其参数可以接受多种类型,而不是像 i32 或 String 这样的具体类型,从而避免为处理每种类型而定义一个函数,减少了重复的编码工作。在前面的例子中我们已经见过 Option<T>、Vec<T>、 HashMap<K, V> 以及 Result<T, E> 等泛型。本章将会探索如何在我们自己编写的类型、函数和方法中使用泛型,包括定义泛型行为的方法 trait 以及引用的生命周期!
通过提取函数减少重复
在介绍泛型之前,我们先来回顾一个不使用泛型来处理重复的技术,提取函数:
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);
}
复制代码
上面的例子中,定义了一个整数列表,存放在变量 number_list 中。接着通过对列表进行遍历获取列表中的最大值,即 100。假设我们需要在两个不同的列表中寻找最大值,我们可以重复这段代码,但是这样程序中就会存在两段相同逻辑的代码:
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);
}
复制代码
虽然上面的代码能够正确执行,但是重复的代码冗余且容易出错的,并且难以维护(当修改代码时需要修改多处地方)。
为了避免重复,我们可以对功能进行抽象,在这个例子中即为抽象出一个以任意整型列表为参数并返回最大值的函数。这将使代码变得更为简洁并抽象出寻找最大值的概念,即 largest 函数:
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);
}
复制代码
总的来说,我们通过以下几步抽象出函数 largest:
下面我们将参照以上步骤使用泛型来减少重复代码,但是函数操作的对象将不再是具体的类型,泛型允许我们对抽象类型进行操作。譬如,如果我们有两个函数,一个寻找一个整型列表中的最大项而另一个寻找字符列表中最大项,应该何消除重复呢?让我们接着往下看。
在函数定义中使用泛型
当使用泛型定义函数时,我们通常在定义参数类型和返回值类型处使用泛型。这在消除重复代码的同时,还将使我们的代码更灵活并提供更多功能。回到 largest 函数上:
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);
}
复制代码
largest_i32 函数是在整型列表中寻找最大值的函数,largest_char 函数是字符列表中寻找最大值的函数。我们可以看到,这两个函数有着完全相同的代码,所以我们可以引入泛型参数来消除“重复的函数”。
首先,就像给普通参数起名字一样,我们需要给泛型起个名字,在 Rust 中习惯使用字母 T ,这是因为 Rust 习惯使用尽量短的变量名,通常就只有一个字母,同时 Rust 类型命名规范使用的是驼峰命名法(CamelCase),而 T 作为 “type” 的缩写变成为了首选。Rust 要求当在函数定义中使用一个泛型时,必须在使用它之前声明它。因此,为了定义泛型版本的 largest 函数,需要在函数名称与参数列表中间使用 <> 声明泛型:
fn largest<T>(list: &[T]) -> &T {
复制代码
我们可以这么理解上面的函数定义:函数 largest 有泛型 T,它的参数 list 的类型是一个类型为泛型 T 的切片。largest 函数将会返回一个类型为泛型 T 的值。
下面我们将展示一个较为完整的 largest 函数定义(目前还不能编译通过,稍后我们将修复其中的错误):
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);
}
复制代码
如果尝试编译这些代码,会出现类似如下错误:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/generic)
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 {
| ^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to previous error
For more information about this error, try `rustc --explain E0369`.
error: could not compile `generic`
To learn more, run the command again with --verbose.
复制代码
错误说明中提到了 std::cmp::PartialOrd,这是一个 trait。后面我们会讲到 trait。简单来说,这个错误意思是 largest 函数并不适用于所有的数据类型,因为我们需要对值的大小进行比较,因此只能用于知道如何排序的类型。而标准库中定义的 std::cmp::PartialOrd trait 可以实现比较功能,因此需要指定泛型为实现了比较功能的类型,我们将在后续的章节详细讨论。
在结构体定义中使用泛型
和函数类似,我们也可以使用 <> 语法来定义拥有泛型字段的结构体。譬如,一个可以存放任何类型 x 和 y 坐标值的结构体 Point:
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 };
}
复制代码
和在函数定义中使用泛型类似,首先,必须在结构体名称后面的尖括号中声明泛型的名称。接着我们就可以在结构体定义中用泛型取代具体类型的定义。
另外,需要注意的是,由于 Point<T> 的定义中只声明一个泛型,而且字段 x 和 y 是相同类型 T。因此,不管它具体是何类型,如果尝试创建一个有不同类型 x、y 值的 Point<T> 的实例,编译就会报错:
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
复制代码
在上面的例子中,当把整型值 5 赋值给 x 时,就已经告诉了编译器这个 Point<T> 实例中的泛型 T 是整型的。接着指定 y 为 4.0 就会得到一个类型不匹配错误,类似下面:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/generic)
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
error: aborting due to previous error
For more information about this error, try `rustc --explain E0308`.
error: could not compile `generic`
To learn more, run the command again with --verbose.
复制代码
如果想要定义一个 x 和 y 可以是不同类型的 Point 结构体,我们可以在开始声明多个泛型:
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 };
}
复制代码
上面例子中的 Point 实例都是正确的!我们可以在定义中使用任意多的泛型参数,不过太多的话将降低代码的可读性,并且表示我们可能需要对代码进行重构了。
在枚举定义中使用泛型
类似于结构体,枚举也可以在其成员中使用泛型。譬如,标准库提供的 Option<T> :
enum Option<T> {
Some(T),
None,
}
复制代码
现在我们应该可以很容易理解了, Option<T> 是一个拥有泛型 T 的枚举,它有两个成员:Some 和 None,其中 Some 存放了一个类型为 T 的值,由于 T 是泛型,因此,无论这个值是什么类型都可以使用。
同样,枚举也可以拥有多个泛型,譬如 Result 枚举:
enum Result<T, E> {
Ok(T),
Err(E),
}
复制代码
Result 枚举有两个泛型,T 和 E;有两个成员:Ok,用来存放类型 T 的值,Err,用来存放类型 E 的值。这个使得 Result 枚举能很方便的用来返回任何可能成功( T 类型的值)也可能失败( E 类型的值)的操作。譬如,前面介绍的打开文件的场景:当文件打开成功时 T 的类型为 std::fs::File ,而当打开文件出错时 E 的类型为 std::io::Error。
当我们发现代码中有多个只有类型有所不同的结构体或枚举时,就应该函数那样考虑引入泛型来减少重复。
在方法定义中使用泛型
我们也可以在结构体和枚举方法定义中使用泛型在,譬如,为 Point<T> 增加方法 x ,用来返回字段 x 的引用:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
复制代码
注意方法是在 impl 后面声明 T。
我们还可以为某个具体类型定义方法,譬如,我们为 Point<f32> 实例实现方法:
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
复制代码
上面的例子意味着 Point<f32> 类型会有一个方法 distance_from_origin,而其他不是 f32 类型的 Point<T> 实例则没有此方法。
另外,结构体定义中的泛型也可以与结构体方法定义中使用的泛型不同。譬如,我们在结构体 Point<T, U> 上定义一个方法 mixup,它使用另一个于当前 Point 字段类型可能不同的 Point 作为参数。这个方法用当前 Point 的 x 值(类型 T)和参数 Point 的 y 值(类型 W)来创建一个新 Point 实例:
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
复制代码
上面的例子最终会打印出 p3.x = 5, p3.y = c。在例子中,声明在 impl 之后的泛型参数 T 和 U 与结构体定义相对应;声明在 fn mixup 方法之后的泛型参数 V 和 W 只是相对于方法自身。
泛型的性能
泛型如此灵活,我们可能会担心是否会有运行时消耗。好消息是:在 Rust 中使用泛型代码相比使用具体类型的代码在运行时基本没有性能上的损失。这是由于 Rust 通过在编译时进行泛型代码的单态化(monomorphization,在编译时就将泛型转换为的具体的类型)来保证效率。编译器所做的工作正好与我们创建泛型的步骤相反。编译器会寻找所有调用了泛型代码的地方,并针对具体类型生成代码,让我们以标准库中的 Option<T>为例:
let integer = Some(5);
let float = Some(5.0);
复制代码
当 Rust 编译上面的代码的时候,它会进行单态化。编译器会读取传递给 Option<T> 的值并获得其对应的类型,在例子中即 i32 和 f64,并将泛型定义 Option<T> 展开为 Option_i32 和 Option_f64,然后将泛型定义替换为这两个具体类型的定义。看起来类似下面这样:
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 泛型在运行时极其高效的原因。
评论