Rust 学习笔记:泛型

发布于:2025-05-22 ⋅ 阅读:(22) ⋅ 点赞:(0)

Rust 学习笔记:泛型

泛型是 Rust 用于消除重复的工具之一。

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

在函数定义中使用泛型

在定义使用泛型的函数时,我们将泛型放在函数的签名中,通常在签名中指定参数和返回值的数据类型。

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}");

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

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

函数 largest_i32 和 largest_char 都在切片中查找最大值。

我们将这些组合成一个使用泛型的函数。为了定义泛型的最大函数,我们将类型名称声明放在函数名和形参列表之间的尖括号 <> 内,如下所示:

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

我们将这个定义理解为:函数最大是某种类型 T 上的泛型函数。这个函数有一个名为 list 的形参,它是 T 类型值的切片。

下面给出完整的泛型函数 largest:

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}");
}

注意,这段代码还不能通过编译。

在这里插入图片描述

帮助文本提到了 std::cmp::PartialOrd,这是一个 trait。这个错误表明,对于 T 可能是的所有类型,largest 函数体并不适用。因为要比较主体中类型 T 的值,所以只能使用值可以排序的类型。为了启用比较,标准库有 std::cmp::PartialOrd trait。要修复上面的示例代码,我们需要遵循帮助文本的建议,并将 T 的有效类型限制为仅实现 PartialOrd 的类型。然后这个例子就可以编译了,因为标准库在 i32 和 char 上都实现了 PartialOrd trait。

在结构体定义中使用泛型

还可以使用 <> 语法定义在一个或多个字段中使用泛型类型参数的结构。

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 };
}

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

注意,因为我们只使用了一个泛型来定义 Point<T>,所以这个定义表明 Point<T> 结构体在某种类型 T 上是泛型的,并且字段 x 和 y 都是相同的类型,无论该类型是什么。如果创建具有不同类型值的 Point<T> 实例,比如下面这段代码,代码将出现类型不匹配错误,无法编译。

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

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

要定义一个 Point 结构体,其中 x 和 y 都是泛型,但可以具有不同的类型,可以使用多个泛型类型参数。

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 };
}

我们将 Point 的定义更改为类型 T 和 U 的泛型,其中 x 属于类型 T, y 属于类型 U。

使用过多泛型类型会使代码难以阅读。

在枚举定义中使用泛型

与处理结构体时一样,可以定义枚举以在其变体中保存泛型数据类型。

让我们再看一下标准库提供的枚举 Option<T>:

enum Option<T> {
    Some(T),
    None,
}

因为 Option<T> 是泛型的,所以无论可选值的类型是什么,我们都可以使用这个抽象。

枚举也可以使用多个泛型类型。枚举 Result 的定义就是一个例子:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Result 枚举在 T 和 E 两种类型上是泛型的,并且有两种变体:Ok 保存类型为 T 的值,Err 保存类型为 E 的值。

在方法定义中使用泛型

我们可以在结构体和枚举上实现方法,也可以在它们的定义中使用泛型类型。

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());
}

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

注意,我们必须在 impl 之后声明 T,这样我们就可以使用 T 来指定我们在类型 Point<T> 上实现方法。通过在 impl 之后将T声明为泛型类型,Rust 可以识别 Point 中尖括号中的类型是泛型类型,而不是具体类型。

如果在声明泛型类型的 impl 中编写方法,则该方法将在该类型的任何实例上定义,而不管用什么具体类型替代泛型类型。

在泛型类型上定义方法时,还可以指定对该类型的约束。例如,我们可以只在 Point<f32> 实例上实现方法,而不是在任何泛型类型的 Point<T> 实例上实现方法。

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

这段代码意味着类型 Point<f32> 将有一个 distance_from_origin 方法。Point<T> 的其他实例,如果 T 不是 f32 类型,则不会定义此方法。

结构定义中的泛型类型参数并不总是与您在同一结构的方法签名中使用的参数相同。

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);
}

将 X1 和 Y1 用于 Point 结构,将 X2、Y2 用于 mixup 方法签名,以使示例更清晰。

泛型参数 X1 和 Y1 在 impl 之后声明,因为它们与结构定义一致。泛型参数 X2 和 Y2 在 fn mixup 之后声明,因为它们只与方法相关。

使用泛型的代码性能

使用泛型类型不会使程序的运行速度比使用具体类型慢。

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

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

let integer = Some(5);
let float = Some(5.0);

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

代码的单态版本看起来类似于下面(编译器使用的名称与我们在这里使用的名称不同):

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);
}

泛型选项 <T> 被编译器创建的特定定义取代。因为 Rust 将泛型代码编译成在每个实例中指定类型的代码,所以我们不用为使用泛型支付运行时成本。

单态化的过程使得 Rust 的泛型在运行时非常高效。