泛型类型、Trait 与生命周期
每种编程语言都有有效处理概念重复的工具。在 Rust 中,这类工具之一就是泛型:它是具体类型或其他属性的抽象占位符。即使在编译和运行代码时还不知道这些占位符具体代表什么,我们也可以表达泛型的行为,或说明它们之间的关系。
函数可以接受某种泛型类型的参数,而不是像 i32 或 String 这样的具体类型;这与函数接受未知值作为参数、从而对多个具体值运行同一段代码的方式类似。实际上,我们已经在第 6 章使用了 Option<T>,在第 8 章使用了 Vec<T> 和 HashMap<K, V>,并在第 9 章使用了 Result<T, E>。本章将介绍如何用泛型定义自己的类型、函数和方法!
首先,我们会回顾如何提取函数以减少代码重复。然后,对两个仅参数类型不同的函数使用相同技巧,将它们改写为一个泛型函数。我们还会说明如何在结构体和枚举定义中使用泛型类型。
接下来,你将学习如何使用 trait 以泛型方式定义行为。你可以将 trait 与泛型类型结合起来,限制泛型类型只接受具有特定行为的类型,而不是任意类型。
最后,我们会讨论生命周期:这是一类泛型,它向编译器提供引用之间关系的信息。生命周期让我们能够向编译器充分说明借用值的情况,使它能够在比没有这些信息时更多的情形下确保引用有效。
通过提取函数消除重复
泛型允许我们用一个代表多种类型的占位符替换具体类型,从而消除代码重复。在深入了解泛型语法之前,我们先看看不涉及泛型类型的消除重复方法:提取一个函数,用代表多个值的占位符替换具体值。然后,我们会使用同样的技巧提取泛型函数!了解如何识别可提取到函数中的重复代码后,你也会开始发现哪些重复代码可以使用泛型。
我们先看清单 10-1 中的简短程序,它会找出列表中的最大值。
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}");
assert_eq!(*largest, 100);
}
我们将一个整数列表存入变量 number_list,并在名为 largest 的变量中保存对列表第一个数字的引用。接着遍历列表中的所有数字;如果当前数字大于 largest 中保存的数字,就替换该变量中的引用。如果当前数字小于或等于目前遇到的最大值,变量就保持不变,代码继续处理列表中的下一个数字。检查完列表中的所有数字后,largest 应该指向最大值,本例中即为 100。
现在,我们需要找出两个不同数字列表中的最大值。为此,可以复制清单 10-1 中的代码,并在程序的两个位置使用相同逻辑,如清单 10-2 所示。
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-3 中,我们将找最大值的代码提取到名为 largest 的函数中。然后调用该函数,找出清单 10-2 中两个列表的最大值。将来遇到其他 i32 值列表时,也可以使用这个函数。
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}");
assert_eq!(*result, 100);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {result}");
assert_eq!(*result, 6000);
}
largest 函数有一个名为 list 的参数,它代表我们可能传入函数的任意具体 i32 切片。因此,调用函数时,代码会针对我们传入的特定值运行。
总结一下,从清单 10-2 改写为清单 10-3 的步骤如下:
- 找出重复代码。
- 将重复代码提取到函数体中,并在函数签名中指定这段代码的输入和返回值。
- 将两处重复代码改为调用该函数。
接下来,我们会用泛型执行同样的步骤来减少代码重复。正如函数体可以处理抽象的 list 而非特定值一样,泛型也允许代码处理抽象类型。
例如,假设我们有两个函数:一个找出 i32 切片中的最大项,另一个找出 char 切片中的最大项。如何消除这两者之间的重复呢?让我们继续探索!