关于rust:rust学习泛型generics-Data-Types

8次阅读

共计 5496 个字符,预计需要花费 14 分钟才能阅读完成。

咱们能够应用泛型为诸如函数签名或构造之类的项创立定义,而后能够将其用于许多不同的具体数据类型。首先让咱们看一下如何应用泛型定义函数,构造,枚举和办法。而后,咱们将探讨泛型如何影响代码性能。

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

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 的泛型在运行时十分高效。

正文完
 0