泛型类型、Trait 和生命周期(Generic Types, Traits, and Lifetimes)
每种编程语言都有有效处理概念重复的工具。在 Rust 中,其中一种工具是泛型(generics):具体类型或其他属性的抽象替代物。我们可以在编译和运行代码时不知道具体内容的情况下,表达泛型的行为或它们与其他泛型的关联方式。
函数可以接受某种泛型类型的参数,而不是像 i32 或 String 这样的具体类型,就像它们接受具有未知值的参数以在多个具体值上运行相同的代码一样。事实上,我们已经在第 6 章中使用过 Option<T>,在第 8 章中使用过 Vec<T> 和 HashMap<K, V>,在第 9 章中使用过 Result<T, E>。在本章中,你将探索如何使用泛型定义自己的类型、函数和方法!
首先,我们将回顾如何提取一个函数来减少代码重复。然后,我们将使用相同的技术,从两个仅在参数类型上不同的函数中制作一个泛型函数。我们还将解释如何在结构体(struct)和枚举(enum)定义中使用泛型类型。
然后,你将学习如何使用 trait(特质)以泛型方式定义行为。你可以将 trait 与泛型类型结合使用,以约束泛型类型仅接受具有特定行为的类型,而不是任何类型。
最后,我们将讨论生命周期(lifetimes):一种泛型变体,它为编译器提供有关引用之间如何相互关联的信息。生命周期允许我们给编译器提供关于借用值的足够信息,以便它能够确保引用在更多情况下比没有我们的帮助时更有效。
通过提取函数消除重复
泛型允许我们用代表多种类型的占位符替换特定类型,以消除代码重复。在深入泛型语法之前,让我们首先看看如何通过提取一个函数来消除重复,这种方式不涉及泛型类型,而是用代表多个值的占位符替换特定值。然后,我们将应用相同的技术来提取一个泛型函数!通过了解如何识别可以提取到函数中的重复代码,你将开始识别可以使用泛型的重复代码。
我们从示例 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 值的切片中查找最大项。我们如何消除这种重复?让我们来找出答案!