rust 快速入门——10 泛型

82 阅读5分钟

[!|center] 普若哥们儿

github.com/wu-hongbing…

gitee.com/wuhongbing/…

泛型

可以使用泛型为函数签名或定义结构体和枚举,这样它们就可以用于多种不同的具体数据类型。

在函数定义中使用泛型

下面的 largest 是一个泛型函数,类型参数声明位于函数名称与参数列表中间的尖括号 <> 中:

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

泛型函数 largest 有一个泛型类型 T,它有个参数 list,其类型是元素为 T 的 slice。largest 函数会返回一个与 T 相同类型的引用。

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

如果现在就编译这个代码,会出现如下错误:

...
help: consider restricting type parameter `T`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  |             ++++++++++++++++++++++

编译输出中提到了 std::cmp::PartialOrd,这是一个 trait,trait 的概念类似其它语言中的接口 interface,是一组预定义的函数接口规范,一个类型实现了某个 trati 表明该类型实现了一组函数,具有特定的功能,后面的章节会详细介绍 trait。简单来说,这个错误表明 largest 的函数体不能适用于 T所有可能的类型。因为在函数体需要比较 T 类型的值,不过它只能用于我们知道如何排序的类型。标准库中定义的 std::cmp::PartialOrd trait 实现了类型实例的比较功能,依照帮助说明中的建议,我们限制 T 只对实现了 PartialOrd 的类型有效,这样代码就可以编译了,因为标准库为 i32char 实现了 PartialOrd trait。

函数定义应当改为:

fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {...}

<T: std::cmp::PartialOrd> 表示对泛型参数 T 的约束,要求类型 T 必须实现了 std::cmp::PartialOrd trait。

需要注意:如果不加约束,Rust 要求泛型函数中的语句适应所有类型,因此 Rust 中的泛型函数通常需要对类型参数进行约束。

结构体定义中的泛型

同样也可以用 <> 语法来定义结构体,它包含一个或多个泛型参数类型字段。示例定义了一个可以存放任何类型的 xy 坐标值的结构体 Point

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 是泛型的,而且字段 xy 都是相同类型的,无论它具体是何类型。

如果想要定义一个 xy 可以有不同类型,可以使用多个泛型类型参数:

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

枚举定义中的泛型

和结构体类似,枚举也可以在成员中存放泛型数据类型。标准库提供的 Option<T> 枚举就是一个泛型:

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

Option<T> 是一个拥有泛型 T 的枚举,它有两个成员:Some,它存放了一个类型 T 的值,和不存在任何值的 None。通过 Option<T> 枚举可以表达有一个可能的值的抽象概念,同时因为 Option<T> 是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。

枚举也可以拥有多个泛型类型,标准库提供的Result 枚举定义就是一个这样的例子:

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

Result 枚举有两个泛型类型,TEResult 有两个成员:Ok,它存放一个类型 T 的值,而 Err 则存放一个类型 E 的值。这个定义使得 Result 枚举能很方便的表达任何可能成功(返回 T 类型的值)也可能失败(返回 E 类型的值)的操作。

使用标准库打开文件时,当成功打开文件,T 对应的是 std::fs::File 类型;而当打开文件出现问题时,E 的值则是 std::io::Error 类型。

泛型的类型推断

前面的例子中编译器自动推断出了泛型的类型,实际上也可以显式指定泛型参数类型,比如:

fn my_function<T>(value: T) -> T {  
    value
}  
  
fn main() {  
    let x = my_function::<i32>(5);  
    let y = my_function(5);  
}

第 6 行通过显式指定泛型类型来调用函数。注意显式指定泛型参数类型的语法:函数名::<类型名>(函数参数)

再比如在结构体中:

struct Point<T, U> {  
    x: T,  
    y: U,  
}  
  
fn main() {  
    let both_integer = Point::<i32,i32> { x: 5, y: 10 };  
    let integer_and_float = Point::<i32,f32>  { x: 5, y: 10.0 };  
}

注意显式指定泛型参数类型的语法:结构体名::<类型名>{指定结构体成员的值}

由于 Rust 能够自动推断类型,因此显式指定泛型参数类型是不必要的。

默认泛型参数

在结构体中可以指定默认泛型类型,其意义是,当不显式指定泛型类型时使用默认泛型类型。比如:

struct Point<T = i32, U = f32> {  // 指定默认泛型类型
    x: T,  
    y: U,  
}  
  
fn main() {  
    let integer_and_float = Point { x: 5, y: 10.0 }; // 使用默认泛型类型  
    let both_integer = Point::<i32, i32> { x: 5, y: 10 }; // 显式指定泛型类型  
    let both_float = Point { x: 1.0, y: 4.0 }; // 通过类型推断获取泛型类型  
}

第 7 行没有显式指定泛型类型,因此使用默认泛型。

第 8 行显式指定了泛型类型,因此不使用默认泛型类型。

第 9 行通过类型推断获取泛型类型,也不使用默认泛型类型。

同样,由于 Rust 能够自动推断类型,因此指定默认泛型类型通常也是不必要的。

注意,不能为函数参数指定默认泛型类型。

默认泛型类型在 trait 定义中有些情况下是有意义的,这将在关于 trait 的章节中介绍。