泛型数据类型

我们使用泛型为函数签名或 结构体,然后我们可以将其与许多不同的具体数据类型一起使用。让我们 首先看看如何使用 泛 型。然后,我们将讨论泛型如何影响代码性能。

在函数定义中

在定义使用泛型的函数时,我们将泛型放在 签名,我们通常会指定 parameters 和 return value 的 API 中。这样做使我们的代码更加灵活,并提供 为函数的调用者提供更多功能,同时防止代码重复。

继续我们的函数,示例 10-4 展示了两个函数,它们 两者都能找到 slice 中的最大值。然后,我们将这些组合成一个 使用泛型的函数。largest

文件名: src/main.rs

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> &char {
    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_i32(&number_list);
    println!("The largest number is {result}");
    assert_eq!(*result, 100);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest_char(&char_list);
    println!("The largest char is {result}");
    assert_eq!(*result, 'y');
}

示例 10-4:两个函数仅在其 names 及其签名中的类型

该函数是我们在示例 10-3 中提取的函数,它找到 切片中最大的。该函数在切片中查找最大的。函数体具有相同的代码,因此让我们消除 通过在单个函数中引入泛型类型参数来进行复制。largest_i32i32largest_charchar

要在新的单个函数中参数化类型,我们需要将类型命名为 parameter 的值,就像我们对函数的值 parameters 所做的那样。您可以使用 any identifier 作为类型参数名称。但是我们将使用 because,通过 约定,Rust 中的类型参数名称很短,通常只有一个字母,而 Rust 的类型命名约定是 UpperCamelCase。type 的缩写是 大多数 Rust 程序员的默认选择。TT

当我们在函数体中使用参数时,我们必须声明 parameter name 的签名,以便编译器知道该名称的含义。 同样,当我们在函数签名中使用类型参数名称时,我们有 来声明类型参数名称。为了定义泛型函数,我们将类型名称声明放在函数名称和参数列表之间的尖括号 , 内,如下所示:largest<>

fn largest<T>(list: &[T]) -> &T {

我们将这个定义理解为:函数在某种类型上是泛型的。此函数有一个名为 的参数,它是一个值切片 的类型 。该函数将返回对 相同类型 .largestTlistTlargestT

示例 10-5 显示了使用泛型 数据类型。清单还显示了我们如何调用函数 替换为 values 或 values 切片。请注意,此代码不会 编译,但我们将在本章后面修复它。largesti32char

文件名: src/main.rs

fn largest<T>(list: &[T]) -> &T {
    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}");

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {result}");
}

示例 10-5:使用泛型类型的最大函数 参数;this 尚未编译

如果我们现在编译这段代码,我们将收到这个错误:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  |             ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

帮助文本提到了 ,这是一个特征,我们是 将在下一节中讨论 traits。现在,请知道这个错误 表示 body of 不适用于所有可能的类型。因为我们想要比较 body 中 type 的值,所以我们可以 仅使用其值可以排序的类型。为了启用比较,标准 library 具有 Trait that you can implement on types (有关此性状的更多信息,请参见附录 C)。按照帮助文本的 建议中,我们将对 set for 有效的类型限制为仅实现的类型,此示例将编译,因为标准库 在 和 上实施。std::cmp::PartialOrdlargestTTstd::cmp::PartialOrdTPartialOrdPartialOrdi32char

在结构定义中

我们还可以定义结构体以在一个或多个 字段。示例 10-6 定义了一个结构体来保存和协调任何类型的值。<>Point<T>xy

文件名: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

示例 10-6:一个 Point<T> 结构体,其中包含 T 类型的 xy

在结构体定义中使用泛型的语法类似于 函数定义。首先,我们在 结构名称后面的尖括号。然后我们使用泛型 type 在 struct 定义中,否则我们将指定具体数据 类型。

请注意,因为我们只使用了一个泛型类型来定义 ,所以这个 definition 表示该结构在某种类型上是泛型的,并且 字段 和 都是相同的类型,无论该类型是什么。如果 我们创建一个具有不同类型值的 a 实例,如 示例 10-7,我们的代码无法编译。Point<T>Point<T>TxyPoint<T>

文件名: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}

示例 10-7:字段 xy 必须相同 type 的 ID 数据类型,因为两者具有相同的泛型数据类型 T

在这个例子中,当我们将整数值赋值给 时,我们让 编译器知道泛型类型将是此实例的整数。然后,当我们为 指定 时,我们将其定义为具有 与 相同类型,我们将收到如下所示的类型不匹配错误:5xTPoint<T>4.0yx

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
 --> src/main.rs:7:38
  |
7 |     let wont_work = Point { x: 5, y: 4.0 };
  |                                      ^^^ expected integer, found floating-point number

For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

定义一个结构,其中 和 都是泛型,但可以具有 不同的类型,我们可以使用多个泛型类型参数。例如,在 示例 10-8,我们将 的定义更改为 泛型 ,其中 is 为 type 和 is 为 type 。PointxyPointTUxTyU

文件名: src/main.rs

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

示例 10-8:一个 Point<T、U> 泛型在两种类型上,所以 xy 可以是不同类型的值

现在允许显示的所有实例!您可以使用任意数量的通用 根据需要在定义中键入参数,但使用多个参数会使 你的代码很难阅读。如果你发现你需要大量的泛型类型 您的代码,它可能表明您的代码需要重组为更小的代码 件。Point

在 Enum 定义中

就像我们对结构体所做的那样,我们可以定义枚举来保存其 变种。让我们再看一下标准 library 提供,我们在第 6 章中使用了它:Option<T>

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}
}

现在,此定义对您来说应该更有意义。如你所见,枚举是 type 上的泛型,并且有两个变体:,其中 包含一个 type 为 的值 ,以及一个不包含任何值的 variant。 通过使用枚举,我们可以表达 optional 值,并且因为是 generic 的,所以我们可以使用这个抽象 无论 Optional 值的类型是什么。Option<T>TSomeTNoneOption<T>Option<T>

枚举也可以使用多个泛型类型。我们在第 9 章中使用的枚举定义就是一个例子:Result

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

枚举在两种类型上是泛型的,并且具有两个变体: ,它包含类型为 的值 ,以及 ,它包含类型为值。这个定义使得在任何地方使用 enum 都很方便。 有一个作可能会成功(返回某种类型的值)或失败 (返回某种类型的错误)。事实上,这就是我们用来打开 file 中,其中 文件已成功打开,并在打开文件时填写了类型。ResultTEOkTErrEResultTETstd::fs::FileEstd::io::Error

当你识别到代码中具有多个 struct 或 enum 的情况时 定义仅在它们所持有的值的类型上有所不同,您可以 通过使用泛型类型来避免重复。

在方法定义中

我们可以在结构和枚举上实现方法(就像我们在第 5 章中所做的那样)并使用 泛型类型。示例 10-9 显示了我们在示例 10-6 中定义的结构体,上面有一个名为 implemented 的方法。Point<T>x

文件名: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

示例 10-9:在 Point<T> 结构体上实现一个名为 x 的方法,该方法将返回对 T 类型的 x 字段的引用

在这里,我们定义了一个名为 on 的方法,它返回一个引用 添加到字段中的数据 。xPoint<T>x

请注意,我们必须紧接着声明,以便我们可以用来指定 我们正在 .通过声明为 泛型类型后,Rust 可以识别出该类型在 Brackets in 是泛型类型,而不是具体类型。我们可以 为此泛型参数选择的名称与泛型 parameter 的 Parameter 中声明的,但使用相同的名称是 协定的。在声明泛型类型的 an 中编写的方法 将在该类型的任何实例上定义,无论具体类型以何种结尾 up 替换泛型类型。TimplTPoint<T>TimplPointimpl

在 类型。例如,我们只能在实例上实现方法 而不是在具有任何泛型类型的实例上。在示例 10-10 中,我们 使用具体类型 ,这意味着我们不会在 .Point<f32>Point<T>f32impl

文件名: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

示例 10-10:一个仅适用于 struct 替换为泛型类型参数 T 的特定具体类型

此代码意味着该类型将具有一个方法;where is not 类型的其他实例不会 定义此方法。该方法测量我们的点与 指向坐标 (0.0, 0.0),并使用 仅适用于浮点类型。Point<f32>distance_from_originPoint<T>Tf32

结构体定义中的泛型类型参数并不总是相同的 你在同一结构体中使用 method signatures。示例 10-11 使用泛型 types,以及 struct 和 method 签名以使示例更清晰。该方法使用(类型 )中的值和传入(类型 )中的值创建一个新实例。X1Y1PointX2Y2mixupPointxselfPointX1yPointY2

文件名: src/main.rs

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

示例 10-11:使用不同泛型类型的方法 从其结构体的定义

在 中,我们定义了一个 for (值为 ) 和一个 for (值为 )。变量是一个结构 它有一个字符串 slice for (with value ) 和一个 for (with value )。用参数调用 on 得到我们 , 它将具有一个 for because 来自 。变量 将有一个 for 因为来自 。宏 call 将打印 。mainPointi32x5f64y10.4p2Pointx"Hello"charycmixupp1p2p3i32xxp1p3charyyp2println!p3.x = 5, p3.y = c

此示例的目的是演示一些泛型 参数是使用 定义。在这里,泛型参数 and 是在 之后声明的,因为它们与 struct 定义一起。泛型参数 和 在 之后声明,因为它们仅与 方法。implX1Y1implX2Y2fn mixup

使用泛型的代码性能

您可能想知道使用泛型类型时是否有运行时成本 参数。好消息是,使用泛型类型不会使您的程序 运行速度比使用 Concrete 类型慢。

Rust 通过使用 generics 的 generics 进行编译。Monomorphization 是转为泛型 code 转换为特定代码,方法是填写 编译。在这个过程中,编译器执行与我们使用的步骤相反的作 创建示例 10-5 中的泛型函数:编译器会查看所有 调用泛型代码并为具体类型生成代码的位置 泛型代码被调用 with。

让我们通过使用标准库的通用枚举来看看它是如何工作的:Option<T>

#![allow(unused)]
fn main() {
let integer = Some(5);
let float = Some(5.0);
}

当 Rust 编译此代码时,它会执行单态化。在此期间 进程中,编译器会读取实例中已使用的值,并识别两种值:一种是 IS 和另一种 是。因此,它将 的通用定义扩展为两个 专门用于 和 的定义,从而替换泛型 定义与特定 Ones 一起使用。Option<T>Option<T>i32f64Option<T>i32f64

代码的单态化版本类似于以下内容( compiler 使用的名称与我们在此处使用的名称不同):

文件名: src/main.rs

enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

泛型将替换为由 编译器。因为 Rust 将泛型代码编译成指定 type 时,我们无需为使用泛型支付运行时成本。当代码 运行,它的性能就像我们通过 手。单态化过程使 Rust 的泛型非常高效 在运行时。Option<T>

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准