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

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

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