泛型、Trait 和生命周期
每种编程语言都有一些工具,用来高效处理概念上的重复。在 Rust 中,这样的工具之一就是泛型(generics):它是具体类型或其他属性的抽象占位符。我们可以描述泛型的行为,或者它们与其他泛型之间的关系,而不需要在编译和运行代码时就提前知道它们具体会被什么替换。
函数可以像接收未知值那样,接收某种泛型类型的参数,而不是像 i32 或 String 这样的具体类型,从而让同一段代码可以作用于多种具体值。事实上,我们已经在第六章见过 Option<T>,在第八章见过 Vec<T> 和 HashMap<K, V>,在第九章见过 Result<T, E>。本章将继续探索如何使用泛型来定义我们自己的类型、函数和方法!
首先,我们会回顾如何通过提取函数来减少代码重复。然后,我们会使用同样的技巧,把两个只在参数类型上不同的函数变成一个泛型函数。我们也会解释如何在结构体和枚举定义中使用泛型类型。
然后,你会学到如何使用 trait 以泛型的方式定义行为。你可以把 trait 和泛型类型组合起来,把某个泛型类型限制为只接受具有特定行为的类型,而不是任意类型。
最后,我们会讨论 生命周期(lifetimes):它是一类向编译器提供“引用之间如何相互关联”信息的泛型。生命周期让我们能够向编译器提供足够的信息,使它在更多场景下也能确认引用是有效的。
提取函数来减少重复
泛型允许我们使用一个可以代表多种类型的占位符来替换特定类型,以此来减少代码冗余。在深入了解泛型的语法之前,我们首先来看一种没有使用泛型的减少冗余的方法,即提取一个函数。在这个函数中,我们用一个可以代表多种值的占位符来替换具体的值。接着我们使用相同的技术来提取一个泛型函数!通过学习如何识别并提取可以整合进一个函数的重复代码,你也会开始识别出可以使用泛型的重复代码。
让我们从下面这个寻找列表中最大值的小程序开始,如示例 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}"); assert_eq!(*largest, 100); }
示例 10-1:在一个数字列表中寻找最大值的函数
我们把一个整数列表存进变量 number_list,再把列表中第一个数字的引用放进名为 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-2 中两个列表里的最大值。将来如果我们还有别的 i32 值列表,也同样可以复用这个函数。
文件名: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}"); 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); }
示例 10-3:抽象后的寻找两个数字列表最大值的代码
largest 函数有一个名为 list 的参数,它表示任何传给该函数的 i32 值切片。因此,当我们调用这个函数时,代码会运行在我们传入的具体值上。
总的来说,我们把代码从示例 10-2 改成示例 10-3 时,经历了下面这些步骤:
- 找出重复代码。
- 将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。
- 将重复代码的两个实例,改为调用函数。
接下来,我们会用同样的步骤借助泛型来减少重复。就像函数体可以处理抽象的 list、而不是特定的值一样,泛型也允许代码处理抽象类型。
例如,假设我们有两个函数:一个用来找出 i32 切片中的最大项,另一个用来找出 char 切片中的最大项。我们该如何消除这种重复呢?继续往下看。