Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

10.2 泛型

10.2.1. 什么是泛型

泛型的主要作用是提高代码的复用性。它们适合处理重复代码问题,也可以看成是把数据与算法分离

泛型是具体类型或其他属性的抽象替代。 换句话说,你写的泛型代码并不是最终代码,而更像是一种带有一些占位符模板

编译器会在编译时把这些占位符替换成具体类型。看个例子:

#![allow(unused)]
fn main() {
fn largest<T>(list:&[T]) -> T {
//......
}
}

这个函数定义使用了泛型类型参数T 就是所谓的“占位符”。写代码时,T 可以表示任意类型,但在编译时,编译器会根据实际用法把 T 替换成具体类型。这个过程叫做单态化

T 是泛型类型参数。实际上,你可以使用任意合法标识符作为类型参数名,但按惯例通常使用大写字母 T(代表 Type)。选择泛型类型参数名时,通常会非常短,一个字母往往就够了。如果确实想写长一点,使用驼峰命名即可。

10.2.2. 函数定义中的泛型

用泛型定义函数时,需要把泛型类型参数放在函数签名里。泛型类型参数通常用于指定参数类型和返回类型。

10.1 提取函数以消除重复代码 的代码为例,这里做一点泛型修改:

#![allow(unused)]
fn main() {
fn largest<T>(list: &[T]) -> T{
    let mut largest = list[0];
    for &item in list{
        if item > largest{
            largest = item;
        }
    }
    largest
}
}

可以把整个函数定义理解成:函数 largest 有一个泛型类型参数 T,它接收一个切片作为参数,切片中的元素类型是 T,返回值类型也是 T

尝试编译一下,输出是:

error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src/main.rs:4:17
  |
4 |         if item > largest{
  |            ---- ^ ------- T
  |            |
  |            T
  |
help: consider restricting type parameter `T` with trait `PartialOrd`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T{
  |             ++++++++++++++++++++++

这里先不讨论原因和修复方法。你只需要知道,泛型参数大概就是这样写的。后面的文章会讲解如何指定某个特定的 trait。

10.2.3. struct 定义中的泛型

结构体中定义的泛型类型参数,主要用在其字段上。例如:

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

在结构体名称后面加上 <>,并在其中写入泛型参数名,这个泛型类型就可以应用到结构体的各个字段上。

main 中实例化了这个结构体。integer 中的两个字段都是 i32float 中的两个字段都是 f64。因为 xy 都被声明为 T,所以实例化后的 xy 也必须是同一类型。这两个类型必须保持一致。

如果我想让 xy 是两种不同的类型呢?很简单:声明两个泛型类型参数。

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

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

此时,实例化后的 xy 可以是不同类型,当然也可以是相同类型

需要注意的是,虽然允许多个泛型类型参数,但太多泛型会降低可读性。通常这意味着代码应该被重组为更多、更小的单元。

10.2.4. enum 定义中的泛型

与结构体很像,枚举中的泛型类型参数主要用在其变体中,让枚举变体可以持有泛型数据类型。最常见的例子是 Option<T>Result<T, E>

例如:

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

enum Result<T, E> {
    Ok(T),
    Err(E),
}
}
  • Option 枚举中,Some(T) 是持有类型为 T 的值的变体,而 None 变体表示不持有任何值。正因为 Option 枚举使用了泛型,无论这个可能存在的值是什么类型,都可以用 Option<T> 来表示
  • 同样,枚举也可以使用多个泛型类型参数。例如,Result 枚举使用了 TEOk 变体存储 TErr 变体存储 E

10.2.5. 方法定义中的泛型

方法可以附加在枚举或结构体上。既然枚举和结构体可以使用泛型参数,方法当然也可以,如下所示:

#![allow(unused)]
fn main() {
struct Point<T> {
    x: T,
    y: T,
}

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

x 方法本质上是一个 getter。为 Point<T> 实现方法时,需要在 impl 关键字后面加上 <T>。这表示该实现是针对泛型 T 的,而不是针对某个具体类型。

当然,如果是为某个具体类型实现方法,就不需要那样写:

#![allow(unused)]
fn main() {
impl Point<i32> {
    fn x1(&self) -> &i32 {
        &self.x
    }
}
}

x1 方法只存在于具体类型 Point<i32> 上,其他 Point<T> 类型没有这个方法,类似于 C++ 中的特化和偏特化。

还有一点很重要:结构体中的泛型类型参数可以与方法中的泛型类型参数不同。例如:

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

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        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);
}

Point<T, U> 实现了方法 mixup。它有两个泛型类型参数 VW。方法中的这两个类型参数与 Point 中的两个类型参数不同,尽管实际类型最终也可能相同。mixup 的第二个参数是 other,它的类型也是 Point,但这个 Point 不一定与 self 所指向的 Point 使用相同的数据类型,因此需要两个新的泛型类型参数。再看返回类型,它是 Point<T, W>T 来自 Point<T, U>W 来自 Point<V, W>

现在看 main:先声明 p1,它的两个字段分别是 i32f64;再声明 p2,它的两个字段分别是 &str(字符串切片)和 char(用 '' 表示的单个字符)。接着使用 mixupp1 对应 Point<T, U>p2 对应 Point<V, W>。根据它们的字段类型可以推断:Ti32Uf64V&strWcharmixup 的返回类型是 Point<T, W>,在这个例子中就是 Point<i32, char>

输出:

p3.x = 5, p3.y = c

10.2.6. 泛型代码的性能

使用泛型编写的代码,运行速度与使用具体类型编写的代码一样快。Rust 会在编译时执行单态化,把泛型类型替换成具体类型,因此执行时不存在类型替换过程。

例如:

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

这里 integerOption<i32>floatOption<f64>。在编译时,编译器会把 Option<T> 展开为 Option_i32Option_f64

#![allow(unused)]
fn main() {
enum Option_i32 {
	Some(i32),
	None,
}

enum Option_f64 {
	Some(f64),
	None,
}
}

也就是说,泛型定义 Option<T> 被替换成了两个具体类型定义。

单态化后的 main 函数也会变成这样:

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