泛型数据类型


我们使用泛型为函数签名或结构体等项目创建定义,然后我们可以将其与许多不同的具体数据类型一起使用。让我们首先看看如何使用泛型定义函数、结构体、枚举和方法。然后,我们将讨论泛型如何影响代码性能。


在函数定义中


在定义使用泛型的函数时,我们将泛型放在函数的签名中,我们通常会在其中指定参数和返回值的数据类型。这样做可以使我们的代码更加灵活,并为函数的调用者提供更多功能,同时防止代码重复。


继续我们最大的函数,示例 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:两个函数仅在名称和签名中的类型上有所不同


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


要在新的单个函数中参数化类型,我们需要命名类型参数,就像我们为函数的 value 参数命名一样。您可以使用任何标识符作为类型参数名称。但是我们将使用 T,因为按照约定,Rust 中的类型参数名称很短,通常只有一个字母,而 Rust 的类型命名约定是 UpperCamelCase。Ttype 的缩写,是大多数 Rust 程序员的默认选择。


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

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


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


示例 10-5 显示了在其签名中使用 generic 数据类型的组合 largest 函数定义。该清单还显示了如何使用 i32 值或 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:使用泛型类型参数的最大函数;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::P artialOrd,这是一个特征,我们将在下一节中讨论特征。现在,要知道这个错误表明 largest 的主体不适用于 T 可能是。因为我们想要比较 body 中类型 T 的值,所以我们只能使用其值可以排序的类型。为了启用比较,标准库具有 std::cmp::P artialOrd trait,您可以在类型上实现该 trait(有关此 trait 的更多信息,请参阅附录 C)。按照帮助文本的建议,我们将对 T 有效的类型限制为仅实现 PartialOrd 和此示例将编译,因为标准库在 i32char 上都实现了 PartialOrd


在结构定义中


我们还可以使用 <> 语法定义结构体,以便在一个或多个字段中使用泛型类型参数。示例 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:一个包含 xyPoint<T> 结构体 类型 T 的值


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


请注意,因为我们只使用了一个泛型类型来定义 Point<T>,所以这个定义说 Point<T> 结构体在某个类型 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 必须是相同的类型,因为它们具有相同的通用数据类型 T


在此示例中,当我们将整数值 5 分配给 x 时,我们让编译器知道泛型类型 T 将是此实例的整数 点<T>.然后,当我们为 y 指定 4.0 时,我们将其定义为与 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


要定义一个 Point 结构体,其中 xy 都是泛型,但可以具有不同的类型,我们可以使用多个泛型类型参数。例如,在示例 10-8 中,我们将 Point 的定义更改为类型 T 上的泛型 和 U,其中 x 的类型为 T,y 的类型为 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:A Point<T、U> 泛型两种类型,因此 xy 可以是不同类型的值


现在允许显示的所有 Point 实例!可以在定义中使用任意数量的泛型类型参数,但使用多个泛型类型参数会使代码难以阅读。如果你发现代码中需要大量的泛型类型,这可能表明你的代码需要重构为更小的部分。


在 Enum 定义中


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

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


现在,此定义对您来说应该更有意义。如您所见, Option<T> 枚举在类型 T 上是泛型的,并且有两个变体:Some,它包含一个类型 T 的值,以及一个不保存任何值的 None 变体。通过使用 Option<T> 枚举,我们可以表达可选值的抽象概念,并且由于 Option<T> 是泛型的,因此无论可选值的类型是什么,我们都可以使用此抽象。


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

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


Result 枚举在两种类型(TE)上是泛型的,并且有两种变体: Ok (包含类型 T 的值) 和 Err (包含类型为 E. 通过此定义,可以方便地在作可能成功(返回某种类型的 T)或失败(返回某种类型的错误)的任何位置使用 Result 枚举。事实上,这就是我们在示例 9-3 中打开文件的方式,当文件成功打开时,T 填充了 std::fs::File 类型,E 填充了 type std::io::Error 打开文件时出错。


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


在方法定义中


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


在这里,我们在 Point<T> 上定义了一个名为 x 的方法,该方法返回对字段 x 中数据的引用。


请注意,我们必须在 impl 之后声明 T,这样我们就可以使用 T 来指定我们在 Point<T> 类型上实现方法。通过在 impl 之后将 T 声明为泛型类型,Rust 可以识别 Point 中尖括号中的类型是泛型类型,而不是具体类型。我们可以为此泛型参数选择与结构定义中声明的泛型参数不同的名称,但使用相同的名称是约定俗成的。在 impl 中编写的声明泛型类型的方法将在该类型的任何实例上定义,无论最终用哪种具体类型替换泛型类型。


在类型上定义方法时,我们还可以指定泛型类型的约束。例如,我们只能在 Point<f32> 实例上实现方法,而不能在具有任何泛型类型的 Point<T> 实例上实现方法。在示例 10-10 中,我们使用了具体的类型 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 块,仅适用于泛型类型参数 T 的特定具体类型的结构体


此代码表示类型 Point<f32> 将具有distance_from_origin 方法;如果 T 不是 f32 类型的 Point<T> 的其他实例将不会定义此方法。该方法测量我们的点与坐标 (0.0, 0.0) 处的点的距离,并使用仅适用于浮点类型的数学运算。


结构定义中的泛型类型参数并不总是与在同一结构的方法签名中使用的类型参数相同。示例 10-11 使用泛型类型 X1Y1 作为 Point 结构体,使用 X2Y2 作为 mixup 方法签名,以使示例更清晰。该方法会创建一个新的 Point 实例,其 x 值来自 selfPoint(类型 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,它的 x 有一个 i32(值为 5),y 有一个 f64(值为 10.4)。p2 变量是一个 Point 结构体,它有一个 x 的字符串切片(值为 “Hello”),一个 ychar (值为 c)。使用参数 p2p1 调用 mixup 会得到 p3,它将有一个 xi32,因为 x 来自 p1p3 变量将具有 ychar,因为 y 来自 p2println! 宏调用将打印 p3.x = 5, p3.y = c


此示例的目的是演示一些泛型参数使用 impl 声明,而一些泛型参数使用方法定义声明的情况。此处,泛型参数 X1Y1impl 的 API 中,因为它们与 struct 定义一起使用。泛型参数 X2Y2fn mixup 之后声明,因为它们仅与方法相关。


使用泛型的代码性能


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


Rust 通过在编译时使用泛型执行代码的单态化来实现这一点。Monomorphization 是通过填充编译时使用的具体类型,将泛型代码转换为特定代码的过程。在这个过程中,编译器执行与示例 10-5 中创建泛型函数的步骤相反的作:编译器查看调用泛型代码的所有位置,并为调用泛型代码的具体类型生成代码。


让我们看看通过使用标准库的泛型 选项<T> 枚举:

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


当 Rust 编译此代码时,它会执行单态化。在此过程中,编译器读取 Option<T> 中使用的值 实例并标识两种 Option<T>:一种是 i32,另一种是 f64。因此,它将 Option<T> 的通用定义扩展为两个专门用于 i32f64 的定义,从而用特定的定义替换通用定义。


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


文件名: 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 将泛型代码编译成指定每个实例中的类型的代码,所以我们无需为使用泛型支付运行时成本。当代码运行时,它的执行方式与我们手动复制每个定义时一样。单态化过程使 Rust 的泛型在运行时非常高效。