泛型、特质和生命周期

每种编程语言都有有效地处理概念重复的工具。在 Rust 中,这样一个工具是 泛型:具体类型或其他属性的抽象替代品。我们可以在编译和运行代码时不知道它们的位置是什么的情况下,表达泛型的行为或它们与其他泛型的关系。

函数可以接受某些泛型类型的参数,而不是像 i32String 这样的具体类型,以同样的方式处理具有未知值的参数,以便在多个具体值上运行相同的代码。事实上,我们在第 6 章中已经使用了泛型 Option<T>,在第 8 章中使用了 Vec<T>HashMap<K, V>,在第 9 章中使用了 Result<T, E>。在本章中,你将探索如何定义自己的带有泛型的类型、函数和方法!

首先,我们将回顾如何提取函数以减少代码重复。然后,我们将使用相同的技术从两个仅在参数类型上不同的函数中创建一个泛型函数。我们还将解释如何在结构体和枚举定义中使用泛型类型。

然后你将学习如何使用特质以通用方式定义行为。你可以将特质与泛型类型结合使用,以限制泛型类型只接受具有特定行为的类型,而不仅仅是任何类型。

最后,我们将讨论生命周期:一种泛型,可以向编译器提供关于引用之间关系的信息。生命周期使我们能够向编译器提供足够的关于借用值的信息,以便它可以在没有我们帮助的情况下确保引用在更多情况下有效。

通过提取函数来消除重复

泛型允许我们用一个占位符替换特定类型,该占位符代表多种类型,以消除代码重复。在深入泛型语法之前,我们先来看看如何通过提取一个函数来消除不涉及泛型类型的重复代码,该函数用一个代表多个值的占位符替换特定值。然后我们将应用相同的技术来提取一个泛型函数!通过了解如何识别可以提取到函数中的重复代码,你将开始识别可以使用泛型的重复代码。

我们将从列表 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: Finding the largest number in a list of numbers

我们将一个整数列表存储在变量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: Code to find the largest number in two lists of numbers

虽然这段代码可以工作,但复制代码既繁琐又容易出错。我们还必须记住,当我们想要更改代码时,需要在多个地方更新代码。

为了消除这种重复,我们将通过定义一个函数来创建一个抽象,该函数可以操作作为参数传递的任何整数列表。这种解决方案使我们的代码更清晰,并让我们能够抽象地表达在列表中查找最大数字的概念。

在清单 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: Abstracted code to find the largest number in two lists

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

总之,我们采取了以下步骤将代码从清单 10-2 更改为清单 10-3:

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

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

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