编程中常见的需求是:用同一功用的函数处理不同类型的数据。在不支持泛型的编程言语中,通常需求为每一种类型编写一个函数。而泛型的存在,就能够为开发者供给编程的便当,减少代码的臃肿,一起能够极大地丰厚言语自身的表达能力。即能够用一个函数,代替很多个完结相同功用但处理不同类型数据的函数。

例如,不运用泛型时,界说一个参数允许为u8、i8、u16、i16、u32、i32……等类型的double函数时:

fn double_u8(i: u8) -> u8 { i + i }
fn double_i8(i: i8) -> i8 { i + i }
fn double_u16(i: u16) -> u16 { i + i }
fn double_i16(i: i16) -> i16 { i + i }
fn double_u32(i: u32) -> u32 { i + i }
fn double_i32(i: i32) -> i32 { i + i }
fn double_u64(i: u64) -> u64 { i + i }
fn double_i64(i: i64) -> i64 { i + i }
fn main(){
  println!("{}",double_u8(3_u8));
  println!("{}",double_i16(3_i16));
}

上面界说了一堆double函数,函数的逻辑部分是彻底一致的,仅在于类型的不同。

泛型能够用于处理这样因类型而代码冗余的问题。运用泛型时:

use std::ops::Add;
fn double<T>(i: T) -> T
  where T: Add<Output=T> + Clone + Copy {
  i + i
}
fn main(){
  println!("{}",double(3_i16));
  println!("{}",double(3_i32));
}

上面的字母T便是泛型(和变量x的含义是相同的),它用来代表各种或许的数据类型。

在函数界说中运用泛型

当运用泛型界说函数时,本来在函数签名中指定参数和回来值的类型的地方,会改用泛型来表明,使得代码适应性更强,从而为函数的调用者供给更多的功用,一起也避免了代码的重复。

在 Rust 中,泛型参数的称号能够恣意起,处于惯例都是用T(type 的首字母)作为首选。

运用泛型参数,必须在运用前对其进行声明:

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

界说泛型版本函数类型参数声明位于函数称号与参数列表中心的尖括号<>中,比如
largest<T>首先对泛型参数T进行了声明,然后才有泛型参数list: &[T]和回来值T

参数部分list: &[T]表明参数list的类型是泛型&[T]

回来值部分-> T表明该函数的回来值类型是泛型T

因而这个函数的界说能够理解为:函数有泛型参数T,函数的参数是list,其类型是元素为T的数组切片,函数回来值类型也是T

综上,关于泛型函数:函数称号后边的<T>表明在函数效果域内界说一个泛型T,这个泛型只能在函数签名和函数体内运用,就跟在一个效果域内界说一个变量,这个变量只能在该效果域内运用是相同的。而且,泛型本便是代表各种数据类型的变量。

因而,上面这部分函数签名表达的含义是:传入某种数据类型的参数,也回来这种数据类型的回来值,且这种数据类型能够是恣意的类型。

结构体中运用泛型

结构体中的字段类型也能够用泛型来界说,如:

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

需求留意:在运用泛型参数之前必须进行声明Point<T>,然后才能够在结构体的字段类型中运用T来代替详细的类型,一起x 和 y 是相同的类型

假如想要让 x 和 y 具有不同的类型,需求运用不同的泛型参数:

struct Point<T, U> {
    x: T,
    y: U,
}
fn main() { 
  let p = Point{x: 1, y :1.1}; 
}

枚举中运用泛型

在枚举中运用泛型,Rust中最常见的枚举泛型类型是 Option<T>Result<T, E>

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

Option 和 Result 都常用于函数的回来值,Option 用于值的存在与否,Result 主要关注值的正确性。

假如函数正常运转,则Result终究回来一个Ok(T)T是函数详细的回来值类型,假如函数异常运转,则回来一个Err(E)E是错误类型。

办法中运用泛型

在办法上也能够运用泛型,在运用泛型参数前,仍然需求提早声明:impl<T>,只要提早声明晰,才能在Point<T>中运用,这样 Rust 就知道Point的尖括号中的类型是泛型而不是详细类型。

struct Point<T> {
    x: T,
    y: T,
}
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

需求留意:办法声明中的Point<T>不是泛型声明,而是一个完好的结构体类型,因为界说的结构体便是Point<T>而不是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,
        }
    }
}

该例子中,T, U是界说在结构体Point上的泛型参数,V, W是界说在办法上的泛型参数,它们并不冲突,能够理解为,一个是结构体泛型,一个是函数泛型。

也能够为泛型指定束缚(constraint)为详细的泛型类型完成办法,比如关于Point<T>类型,不仅能界说根据T的办法,还能针对特定的详细类型进行办法界说。这意味着该特定类型会有一个界说的办法,而其他的T不是该类型的Point<T>实例则没有界说此办法。这样就能针对特定的泛型类型完成某个特定的办法,关于其它泛型类型则没有界说该办法。

对泛型进行束缚

束缚泛型也叫做泛型束缚或许Trait绑定(Trait Bound),其语法有两种:

  • 在界说泛型类型T时,运用类似于T: Trait_Name这种语法进行束缚
  • 在回来值后边、大括号前面运用where关键字,如where T: Trait_Name

简而言之,要对泛型做束缚,一方面的原因是函数体内需求某种Trait供给的功用,另一方面的原因是要让泛型T所能代表的数据类型足够精确化(假如不做任何束缚,泛型将能代表恣意数据类型)。

const 泛型

之前的泛型中,能够笼统为一句话:针对类型完成的泛型,一切的泛型都是为了笼统不同的类型。

同一类型不同长度的数组也是不同的数组类型,如[i32, 2][i32, 3]便是不同的数组类型。能够运用数组切片(引用)和泛型来处理处理任何类型数组的问题,例如:

fn display_array<T: std::fmt::Debug>(arr: &[T]) {
    println!("{:?}", arr);
}

但是运用上面的办法,不适用于引用不好用或不能用的场景。这时就能够用const泛型,也便是针对值的泛型,来处理数组长度的问题:

fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
    println!("{:?}", arr);
}

代码中界说了一个类型为[T; N]的数组,其间T是一个根据类型的泛型参数,N是一个根据值的泛型参数,此处代替的是数组的长度。

N便是 const 泛型,界说的语法是const N: usize,表明 const 泛型N,它根据的值类型是usize。在泛型参数之前,Rust 彻底不适合杂乱矩阵的运算,自从有了 const 泛型,全部行将改变。

注:假设某段代码需求在内存很小的平台上作业,因而需求束缚函数参数占用的内存大小,此刻就能够运用 const 泛型表达式来完成。

泛型的功用

Rust 中的泛型是零本钱笼统,因而在运用泛型时,彻底不用担心功用上的问题。另一方面,咱们失去的是编译速度和增大了终究生成文件的大小,因为 Rust 在编译期为泛型对应的多个类型都生成了各自的代码。

Rust 经过在编译时进行泛型代码的单态化(monomorphization) 来确保功率。单态化是一个经过填充编译时运用的详细类型,将通用代码转换为特定代码的进程。编译器所做的作业正好与咱们创立泛型函数的步骤相反,编译器寻觅一切泛型代码被调用的方位并针对详细类型生成代码。因而运用泛型时没有运转时开支,单态化进程正是 Rust泛型在运转时及其高效的原因。

rustc在编译代码时,会将一切的泛型替换成它所代表的详细数据类型,就像编译期间会将变量名替换成它所代表数据的内存地址相同。因为编译期间,编译器会对泛型类型进行替换,这会导致泛型代码胀大(code bloat),从一个函数胀大为零个、一个或多个详细数据类型的函数。有时候这种胀大会导致编译后的程序文件变大很多。不过,大都情况下,代码胀大的问题都不是大问题。

另一方面,因为编译期间现已将泛型替换成了详细的数据类型,因而,在程序运转期间,直接调用对应类型的函数即可,不需求再消耗任何额定的资源去核算泛型所代表的详细类型。因而,Rust的泛型是零运转时开支的。

总结

Rust中能够运用泛型为像函数签名或结构体这样的项创立界说,这样它们就能够用于多种不同的详细数据类型。能够运用泛型界说函数结构体枚举办法,使得代码适应性更强,为调用者供给更多的功用,一起也避免了代码的重复。泛型的类型参数是运用尖括号大驼峰命名的称号:<A, B, ...>来指定的。
Rust经过在编译时进行泛型代码的单态化monomorphization)来确保功率。单态化是一个经过填充编译时运用的详细类型,将通用代码转换为特定代码的进程,这会导致泛型代码胀大

参阅

  • course.rs/basic/trait…
  • kaisery.github.io/trpl-zh-cn/…
  • rustwiki.org/zh-CN/rust-…
  • rustwiki.org/zh-CN/refer…
  • rust-book.junmajinlong.com/ch12/01_gen…