10.1通用数据类型

通用数据类型

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

在函数定义中

当定义一个使用泛型的函数时,我们将泛型放在函数签名中,通常是在指定参数和返回值的数据类型的位置。这样做使我们的代码更灵活,并为调用者提供更多功能,同时避免代码重复。

继续之前最大的那个函数,清单10-4展示了两个都能找到切片中最大值的函数。接下来我们会把它们合并成一个使用泛型的单一函数。

文件名:src/main.rs

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

清单10-4:两个仅名称和签名中的类型不同的函数

largest_i32 函数是我们在清单10-3提取出来,用于查找切片中最大 i32 的那个;largest_char 函数则查找切片中的最大 char。这两个函数体内代码相同,因此让我们通过引入一个泛型类型参数,将它们合并成一个单一的通用版本以消除重复。

要给新建的单个函数参数化类型,我们需要像命名普通参数一样给这个类型参数命名。你可以用任何标识符作为类型参数名字,但按照惯例,Rust 中的类型参数名字通常很短,经常只有一个字母,而且遵循驼峰式大小写规范。T 是 type(类型)的缩写,是大多数 Rust 程序员默认选择。

当我们在函数体内使用某个参数时,需要先在签名里声明该名称,以便编译器知道它代表什么。同理,当在函数字面量里使用某个"类型"名称,也必须先声明该"类型"名称。在这里,为了定义这个通用版本的 largest 函数,我们把表示"类"的名字放到尖括号 < > 内,置于函数字面量与形参列表之间,如下所示:

rust 复制代码
fn largest<T>(list: &[T]) -> &T {

这句可读作:"largest 是针对某种 T 类型实现的一般性(generic)功能。"此函有且仅有一个叫 list 的形参,它是元素为 T 类型值组成切片引用;而返回值也是指向同样 T 类型元素之一的引用。

清单10-5 展示了结合后的 largest 泛型版本及其调用方式,可以传入 i32 或 char 切片。但注意,这段代码目前还不能编译通过:

文件名:src/main.rs

rust 复制代码
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 largets number is {result}");

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

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

清单10-5:带有泛型数据类型参数的大致版 largest 函数;尚未能成功编译

如果现在尝试编译,会得到如下错误:

复制代码
$ 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 > largeset{
 |            ---- ^ ------- &T 
 |            |
 |            &T 
 |
help: consider restricting type parameter `T` with trait `PartialOrd`
 |
1| fn largeset<T : std::cmp::PartialOrd> (list:& [ T ]) - >& T{
 |             ++++++++++++++++++++++
For more information about this error try rustc --explain E0369.
error could not compile chapter10(bin "chapter") due to previous error.

提示信息提到了 std::cmp::PartialOrd,这是 Rust 中一种特征(trait),下一节会详细介绍特征。目前只需知道,该错误说明对于所有可能被替代进来的任意 T 类型,上述比较操作不一定有效,因为不是所有数据都支持大小比较。而为了能够进行比较操作,标准库提供了 PartialOrd 特征,你可以为你的自定义数据实现该特征(详见附录C)。修正上述示例,只需按提示限制 T 必须实现 PartialOrd,即改写为:

rust 复制代码
fn largeset<T : std::cmp::PartialOrd>(list:& [ T ]) - >& T{
...
}

这样就能顺利编译,因为标准库已经为 i32 和 char 实现了 PartialOrd 特征。

方法定义中

我们可以在结构体和枚举上实现方法(正如我们在第5章所做的),并且也可以在它们的方法定义中使用泛型类型。清单10-9展示了我们在清单10-6中定义的Point<T>结构体,并为其实现了一个名为x的方法。

文件名:src/main.rs

rust 复制代码
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字段的引用

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

注意,我们必须紧跟impl后声明T,这样才能使用T来指定我们正在为类型Point<T>实现方法。通过在impl后声明泛型类型T,Rust能够识别出Point尖括号内的是泛型类型而非具体类型。虽然我们可以选择与结构体定义中的泛型参数不同的名称,但通常会使用相同名称。如果你在带有泛型声明的impl块内编写方法,那么该方法将被定义于该类型的任何实例,无论最终替换该泛型的是何种具体类型。

当给某个类型定义方法时,也可以对泛型进行约束。例如,我们可以只针对Point<f32>实例实现某些方法,而不是针对任意泛型参数T。在清单10-10中,我们使用具体类型f32,因此没有在impl后声明任何泛型参数。

文件名:src/main.rs

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

清单10-10:仅适用于具有特定具体类型作为泛型参数T的结构体的impl块

这段代码意味着只有Point<f32>这个具体化后的类型才拥有distance_from_origin这个方法;其他非f32具体化版本的Point<T>则不具备此方法。该方法计算点到坐标原点(0.0, 0.0) 的距离,并调用了仅浮点数可用的一些数学运算。

结构体定义中的泛型参数不一定与同一结构体的方法签名中使用的一致。清单10-11为了更明确示例,分别用了X1和Y1作为Point结构体上的两个泛型,以及X2和Y2作为mixup 方法签名里的两个不同范性。这种设计使得该方法能创建一个新的Point实例,其x值来自自身(Point<X1,Y1>),y值来自传入参数(Point<X2,Y2>)。

文件名:src/main.rs

rust 复制代码
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 是一个点结构体,它的 x 是字符串切片类型(值为 "Hello"),y 是字符类型(值为 c)。调用 p1 的 mixup 方法并传入参数 p2 会得到变量 p3,p3 的 x 将是 i32,因为来自于 p1。p3 的 y 将是 char,因为来自于 p2。println! 宏调用将打印出 "p3.x = 5,p3.y = c"。

这个例子的目的是演示一种情况,其中一些泛型参数是在 impl 后声明的,而另一些则是在方法定义后声明的。在这里,泛型参数 X1 和 Y1 在 impl 后声明,因为它们属于结构体定义。而泛型参数 X2 和 Y2 则在 fn mixup 后声明,因为它们只与该方法相关。

使用泛型的代码性能

你可能会想,使用泛型类型参数是否会带来运行时开销。好消息是,使用泛型类型不会让你的程序比使用具体类型运行得更慢。

Rust 通过在编译时对使用泛型的代码进行单态化(monomorphization)来实现这一点。单态化是将泛型代码转换为特定代码的过程,即在编译时填充所用的具体类型。在这个过程中,编译器执行了与我们创建第10-5节中泛型函数相反的步骤:它查看所有调用泛型代码的位置,并为调用时所用的具体类型生成对应代码。

让我们通过标准库中的泛型枚举 Option 来看看这是如何工作的:

rust 复制代码
let integer = Some(5);
let float = Some(5.0);

当 Rust 编译这段代码时,会执行单态化。在此过程中,编译器读取 Option<T> 实例中所用到的值,并识别出两种 Option<T>:一种是 i32 类型,一种是 f64 类型。因此,它将 Option<T> 的泛型定义扩展成两个针对 i32 和 f64 专门化的定义,从而用具体定义替代了原有的泛型定义。

单态化后的代码大致如下(编译器实际使用的是不同于这里示例名称):

文件名: src/main.rs

rust 复制代码
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 的泛型在运行时极其高效。

相关推荐
维维酱23 分钟前
三、目标规范与交叉编译
rust
维维酱1 小时前
二、相关编程基础
rust
寻月隐君2 小时前
Rust Scoped Threads 实战:更安全、更简洁的并发编程
后端·rust·github
a cool fish(无名)8 小时前
9.1无法恢复的错误与 panic!
rust
勇敢牛牛_1 天前
【OneAPI】网页搜索API和网页正文提取API
rust·oneapi
受之以蒙1 天前
Rust & WebAssembly:探索js-sys的奇妙世界
笔记·rust·webassembly
dzj20211 天前
VS Code中配置使用slint(Rust)的一个小例子
ui·rust·slint
大卫小东(Sheldon)1 天前
智能生成git提交消息工具 GIM 发布 1.7 版本了
git·ai·rust
寻月隐君1 天前
Rust 泛型 Trait:关联类型与泛型参数的核心区别
后端·rust·github