Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

泛型、trait 和生命周期

每种编程语言都有高效处理概念重复的工具。在 Rust 中,泛型(generics)就是这样一种工具:它是具体类型或其他属性的抽象替代。我们可以描述泛型的行为,或者泛型与其他泛型之间的关系,而无需在编写代码时知道它们在编译和运行时会被替换为什么具体类型。

函数的参数可以是某种泛型类型,而不是像 i32String 这样的具体类型,就像函数接受未知值的参数来对多个具体值运行相同的代码一样。事实上,我们在第六章的 Option<T>、第八章的 Vec<T>HashMap<K, V>,以及第九章的 Result<T, E> 中已经使用过泛型了。在本章中,你将学习如何用泛型定义自己的类型、函数和方法!

首先,我们将回顾如何通过提取函数来减少代码重复。然后,用同样的思路,从两个仅参数类型不同的函数中提取出一个泛型函数。我们还会讲解如何在结构体和枚举的定义中使用泛型。

接着,你将学习如何使用 trait 以泛型的方式定义行为。你可以将 trait 与泛型类型结合使用,将泛型约束为只接受具有特定行为的类型,而不是任意类型。

最后,我们将讨论_生命周期_(lifetime):一种特殊的泛型,它向编译器提供引用之间相互关系的信息。生命周期让我们能够向编译器提供足够的借用值信息,使得编译器能在更多场景下确保引用的有效性——这比没有生命周期标注时能覆盖的场景要多得多。

通过提取函数来消除重复

泛型允许我们用一个代表多种类型的占位符来替代具体类型,从而消除代码重复。在深入泛型语法之前,我们先来看看一种不涉及泛型的消除重复的方式——提取函数,用一个代表多个值的占位符替代具体的值。然后,我们将用同样的思路来提取泛型函数!通过学习如何识别可以提取为函数的重复代码,你也将开始学会识别可以使用泛型的重复代码。

我们从示例 10-1 中的小程序开始,它的功能是找出列表中的最大数字。

Filename: 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);
}
Listing 10-1: 找出列表中的最大数字

我们将一个整数列表存储在变量 number_list 中,并将列表中第一个数字的引用存入名为 largest 的变量。然后遍历列表中的所有数字,如果当前数字大于 largest 中存储的数字,就替换该变量中的引用。如果当前数字小于或等于目前为止见到的最大数字,变量不变,代码继续处理列表中的下一个数字。遍历完列表中的所有数字后,largest 应该指向最大的数字,在本例中就是 100。

现在我们的任务是在两个不同的数字列表中分别找出最大值。为此,我们可以选择复制示例 10-1 中的代码,在程序的两个不同位置使用相同的逻辑,如示例 10-2 所示。

Filename: 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}");
}
Listing 10-2: 在两个数字列表中分别找出最大值的代码

虽然这段代码能正常工作,但复制代码既繁琐又容易出错。而且当我们想修改逻辑时,还得记住在多个地方同时更新代码。

为了消除这种重复,我们可以定义一个函数,让它接受任意整数列表作为参数,从而创建一个抽象。这个方案使代码更加清晰,并让我们能够以抽象的方式表达“找出列表中最大数字“这一概念。

在示例 10-3 中,我们将找出最大数字的代码提取到一个名为 largest 的函数中。然后调用该函数来查找示例 10-2 中两个列表的最大值。我们也可以将这个函数用于将来可能遇到的任何其他 i32 值列表。

Filename: 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);
}
Listing 10-3: 将找出两个列表中最大值的代码抽象为一个函数

largest 函数有一个名为 list 的参数,它代表我们可能传入函数的任何具体的 i32 值切片。因此,当我们调用这个函数时,代码会在我们传入的具体值上运行。

总结一下,我们从示例 10-2 到示例 10-3 所经历的步骤如下:

  1. 识别重复的代码。
  2. 将重复的代码提取到函数体中,并在函数签名中指定代码的输入和返回值。
  3. 将两处重复代码替换为对该函数的调用。

接下来,我们将用同样的步骤来使用泛型以减少代码重复。就像函数体可以操作抽象的 list 而不是具体的值一样,泛型允许代码操作抽象的类型。

例如,假设我们有两个函数:一个在 i32 值的切片中找出最大项,另一个在 char 值的切片中找出最大项。我们该如何消除这种重复呢?让我们来一探究竟!