泛型数据类型

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

在函数定义中

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

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

文件名: 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 及其签名中的类型

largest_i32function 是我们在示例 10-3 中提取的那个,它找到 最大的i32在切片中。这largest_char函数查找最大的char在切片中。函数体具有相同的代码,因此让我们消除 通过在单个函数中引入泛型类型参数来实现重复。

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

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

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

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

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

文件名: 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:largest使用泛型类型的函数 参数;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

帮助文本提到std::cmp::PartialOrd,这是一个 trait,而我们是 将在下一节中讨论 traits。现在,请知道这个错误 声明largest不适用于所有可能的类型T可能是。因为我们想要比较T在体内,我们可以 仅使用其值可以排序的类型。为了启用比较,标准 library 具有std::cmp::PartialOrd你可以在类型上实现的 trait (有关此性状的更多信息,请参见附录 C)。按照帮助文本的 建议,我们会限制对T仅对那些实现PartialOrd并且此示例将编译,因为 standard 库 实现PartialOrd在两者上i32char.

在结构定义中

我们还可以定义结构体以在一个或多个 字段。示例 10-6 定义了一个<>Point<T>struct 来持有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>结构体,该xytype 的值T

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

请注意,因为我们只使用了一个泛型类型来定义Point<T>这 定义表示Point<T>struct 在某种类型上是泛型T和 字段xy都是同一类型,无论该类型是什么。如果 我们创建一个Point<T>,它的值不同,如 示例 10-7,我们的代码无法编译。

文件名: 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 的 intent 数据类型,因为两者具有相同的泛型数据类型T.

在此示例中,当我们分配整数值5x,我们让 编译器知道泛型类型T将是此实例的整数Point<T>.然后,当我们指定4.0y,我们将其定义为具有 与 相同类型x,我们将收到如下类型不匹配错误:

$ 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

要定义Pointstruct 中,其中xy都是泛型,但可能具有 不同的类型,我们可以使用多个泛型类型参数。例如,在 示例 10-8,我们将Point成为 type 上的泛型TU哪里x属于 类型Ty属于 类型U.

文件名: 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 定义中

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

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

现在,此定义对您来说应该更有意义。如您所见,Option<T>enum 是 type 上的泛型T,并且有两个变体:Some哪 包含一个 类型的值TNonevariant 中。 通过使用Option<T>enum 中,我们可以表示 optional 值,并且因为Option<T>是通用的,我们可以使用这个抽象 无论 Optional 值的类型是什么。

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

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

Resultenum 在两种类型上是泛型的,TE,并且有两个变体:Ok,它保存 type 为TErr,它保存 type 为E.此定义便于使用Resultenum 的任意位置 有一个可能成功的作(返回某种类型的值T) 或失败 (返回某种类型的错误E).事实上,这就是我们用来打开 file 中,其中T填充了类型std::fs::File什么时候 文件已成功打开,并且E填充了类型std::io::Error当打开文件时出现问题。

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

在方法定义中

我们可以在结构和枚举上实现方法(就像我们在第 5 章中所做的那样)并使用 泛型类型。示例 10-9 显示了Point<T>struct 中,我们在示例 10-6 中定义的 struct 中,使用名为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:实现一个名为xPoint<T>struct 中,它将返回对xtype 字段T

在这里,我们定义了一个名为xPoint<T>返回一个引用 到现场的数据x.

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

在 类型。例如,我们只能在Point<f32>实例 而不是打开Point<T>实例。在示例 10-10 中,我们 使用 concrete 类型f32,这意味着我们不会在impl.

文件名: 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:一个impl仅适用于 struct 替换为泛型类型参数的特定具体类型T

此代码表示类型Point<f32>将具有distance_from_origin方法;其他实例Point<T>哪里T不是类型f32不会 定义此方法。该方法测量我们的点与 指向坐标 (0.0, 0.0),并使用 仅适用于浮点类型。

结构体定义中的泛型类型参数并不总是相同的 你在同一结构体中使用 method signatures。示例 10-11 使用泛型 类型X1Y1对于Pointstruct 和X2 Y2对于mixup方法 签名以使示例更清晰。该方法会创建一个新的Point实例替换为xself Point(类型X1) 和y传入的值Point(类型Y2).

文件名: 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:使用不同泛型类型的方法 从其结构体的定义

main,我们定义了一个Point具有i32x(值5) 以及一个f64y(值10.4).这p2variable 是Point结构 它有一个字符串 slicex(值"Hello") 和chary(值c).叫mixupp1带有参数p2给我们p3, 它将具有i32x因为x来自p1.这p3变量 将具有chary因为y来自p2.这println!宏 call 将打印p3.x = 5, p3.y = c.

此示例的目的是演示一些泛型 参数使用impl有些是使用 定义。此处,泛型参数X1Y1impl因为它们与 struct 定义一起使用。泛型参数X2Y2fn mixup因为它们仅与 方法。

使用泛型的代码性能

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

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

让我们看看通过使用标准库的泛型Option<T>enum 的

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

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

代码的单态化版本类似于以下内容( 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);
}

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

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