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

在函数定义中
举个例子,如果我们定义两个函数,分别是求最大值和最大字符串且要求传入的参数都是一个数组,我们可能这样去实现:

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!("最大数是:{}", result); //100

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

    let result = largest_char(&char_list);
    println!("最大字符是:{}", result); //y
}

通过上例我们可以看到虽然两个函数方法传入的参数类型不一致,但是其行为是一样或者类似的。所以我们是不是提取一个公共函数类出来,若能提取,此方法就可以称之为泛型:
fn largest<T>(list: &[T]) -> T {

我们将这个定义读为:最大的函数在某种类型T上是泛型的。此函数有一个名为list的参数,该参数是类型T的值的一部分。最大的函数将返回相同类型T的值。

下例显示了在签名中使用通用数据类型的最大组合函数定义。清单还显示了如何使用i32值切片或char值调用函数。 请注意,该代码尚未编译。

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

如果我们现在编译该代码:

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
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 {
  |             ^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0369`.
error: could not compile `cargo_learn`.

To learn more, run the command again with --verbose.

注释中提到了std::cmp::PartialOrd,这是一个特征。 目前,此错误表明,最大的正文不适用于T可能适用的所有类型。 因为我们要比较主体中T类型的值,所以我们只能使用其值可以排序的类型。为了进行比较,标准库具有std::cmp::PartialOrd特性,我们可以在类型上实现。

在结构定义中
我们还可以使用<>语法将结构定义为在一个或多个字段中使用通用类型参数。下例显示了如何定义Point <T>结构来保存任何类型的x和y坐标值:

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

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

请注意,因为我们仅使用一种通用类型来定义Point <T>,所以此定义表示Point <T>结构在某些类型T上是通用的,并且字段x和y都是同一类型,无论是哪种类型。 如果我们创建一个Point <T>的实例,该实例具有不同类型的值,如下例所示,我们的代码将无法编译:

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

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

由于传入的x与y不是同一个类型,因此在编译的时候:

$ cargo run
 --> src/main.rs:7:38
  |
7 |     let wont_work = Point { x: 5, y: 4.0 };
  |                                      ^^^ expected integer, found floating-point number

error: aborting due to previous error

要定义Point结构,其中x和y都是泛型但可以具有不同的类型,我们可以使用多个泛型类型参数。例如,在下例中,我们可以将Point的定义更改为在类型T和U上通用,其中x是T类型,而y是U类型:

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实例! 您可以根据需要在定义中使用任意多个泛型类型参数,但是使用多个泛型类型参数会使代码难以阅读。 当代码中需要大量泛型类型时,这可能表明代码需要重组为较小的部分。

在枚举定义中
先来看一个枚举类型的实例:


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

使用该枚举可以定义任意类型的Some,比如:let y: Option<i8> = Some(5);,那么与struc类似,我们可以相应的定义枚举类型的泛型:


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

Result枚举对T和E这两种类型具有通用性,并且具有两个变体:Ok(具有T类型的值)和Err(具有E类型的值)。此定义使在任何地方使用Result枚举都很方便。 我们执行的操作可能会成功(返回某个T类型的值)或失败(返回某个E类型的错误)。 实际上,这就是之前错误处理中打开文件所使用的文件,其中,当文件成功打开时,T填充为std::fs::File类型,而E填充为std::io::Error打开文件时出现问题。

当在代码中识别出具有多个结构或枚举定义的情况时,这些情况仅在它们所持有的值的类型上有所不同,因此可以通过使用泛型类型来避免重复。

在方法定义中
我们可以在结构和枚举上实现方法(就像我们在第5章中所做的那样),并且也可以在它们的定义中使用泛型类型。 如下例所示:

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

D:\learn\rust_test>cargo run
   Compiling rust_test v0.1.0 (D:\learn\rust_test)
warning: field is never read: `y`
 --> src\main.rs:3:5
  |
3 |     y: T,
  |     ^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: 1 warning emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.54s
     Running `target\debug\rust_test.exe`
p.x = 5

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

例如,我们可以仅在Point <f32>实例上实现方法,而不能在具有任何泛型类型的Point <T>实例上实现方法。 在下例中,我们使用具体类型f32,这意味着我们在impl之后不声明任何类型。

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类型)将没有定义此方法。 该方法测量我们的点与坐标(0.0, 0.0)处的点的距离,并使用仅适用于浮点类型的数学运算。

结构定义中的泛型类型参数并不总是与我们在该结构的方法签名中使用的参数相同。比如下面例子中的Point <T,U>结构上定义了方法混合。 该方法将另一个Point作为参数,该类型可能与我们调用混合的自Point的类型不同。 该方法使用自点(类型T)的x值和传入点(类型W)的y值创建一个新的Point实例:

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);
}
D:\learn\rust_test>cargo run
   Compiling rust_test v0.1.0 (D:\learn\rust_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target\debug\rust_test.exe`
p3.x = 5, p3.y = c

本示例的目的是演示一种情况,其中一些通用参数用impl声明,而另一些则用方法定义声明。 在这里,通用参数T和U在impl之后声明,因为它们与struct定义一起使用。 通用参数V和W在fn混合后声明,因为它们仅与方法有关。

使用泛型的代码性能

我们可能想知道在使用通用类型参数时是否会产生运行时成本。 好消息是,Rust以这样的方式实现泛型,即使用泛型类型的代码不会比使用具体类型的代码运行慢。

Rust通过在编译时对使用泛型的代码进行单态化来实现这一点。 单态化是通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

在此过程中,编译器执行与下例中用于创建泛型函数的步骤相反的操作:编译器查看调用泛型代码的所有位置,并为调用泛型代码的具体类型生成代码。

我们来看一个使用标准库的Option <T>枚举的示例的工作方式:

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

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

代码的单体化版本如下所示。 通用Option <T>替换为编译器创建的特定定义:

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

因为Rust将通用代码编译成在每个实例中指定类型的代码,所以我们无需为使用通用代码付出任何运行时成本。代码运行时,其性能与我们手工复制每个定义时的性能相同。 单一化的过程使Rust的泛型在运行时非常高效。


子康
10 声望0 粉丝