• rust泛型


    泛型,英文是generic。
    泛型是一种参数化多态。就是把类型作为参数,使用时才指定具体类型。
    这样一套代码可以应用于多种类型。比如Vec,可以是整型向量Vec,也可以是浮点型向量Vec

    Rust中的泛型属于静多态,它是一种编译期多态。
    在编译期,会根据指定的具体类型,生成一套特化代码,这叫单态化。比如将一个泛型函数生成具体类型对应的函数。
    单态化是编译器进行静态分发的一种策略。
    单态化静态分发的好处就是性能好,没有运行时开销;缺点就是造成编译后生成的二进制文件膨胀。

    一、定义泛型

    语法格式

    T就是泛型参数。

    (一)在函数中使用泛型
    使用了泛型的函数就叫泛型函数
    1.语法格式
    泛型函数的定义语法如下

    fn function_name(param1:T, [other_params]) {
         // 函数实现代码
    }
    
    • 1
    • 2
    • 3

    实例

    fn max(array: &[T]) -> T {
        let mut max_index = 0;
        let mut i = 1;
        while i < array.len() {
            if array[i] > array[max_index] {
                max_index = i;
            }
            i += 1;
        }
        array[max_index]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    (二)在结构体中使用泛型
    使用了泛型的结构体叫做泛型结构体
    1.语法格式
    泛型结构体的定义语法如下

    struct struct_name {
         field:T
    }
    
    • 1
    • 2
    • 3

    例子

    struct Point {
        x: T,
        y: T
    }
    let p1 = Point {x: 1, y: 2};
    let p2 = Point {x: 1.0, y: 2.0};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用时并没有声明类型,这里使用的是自动推断机制,但不允许出现类型不匹配的情况如下:

    let p = Point {x: 1, y: 2.0};
    
    • 1

    2.在结构体的实现块中使用泛型
    语法格式

    impl Point {
    }
    
    • 1
    • 2

    注意,impl关键字的后方必须有 ,因为Point要以它为实参。

    实例

    struct Point {
        x: T,
        y: T,
    }
    impl Point {
        fn x(&self) -> &T {
            &self.x
        }
    }
    fn main() {
        let p = Point { x: 1, y: 2 };
        println!("p.x = {}", p.x());
    }
    运行结果:
    p.x = 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    也可以直接特化

    impl Point {
         fn x(&self) -> f64 {
             self.x
         }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在成员方法中使用泛型
    impl块本身的泛型不影响成员方法的泛型:

    impl Point {
         fn mixup(self, other: Point) -> Point {
             Point {
                 x: self.x,
                 y: other.y,
             }
         }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    方法mixup将一个Point点的x与Point点的y融合成一个类型为Point的新点。

    (三)在枚举中使用泛型
    使用了泛型的枚举叫泛型枚举
    诸如Option和Result

    enum Option {
         Some(T),
         None
    }
    
    enum Result {
         Ok(T),
         Err(E)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    (四)在trait中使用泛型

    struct AS {}
    trait AT {
        fn foo(&self, para: T);
    }
    
    impl AT for AS{
         fn foo(&self, para: String){}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    使用泛型与使用关联类型的区别
    例子
    标准库提供的Iterator trait有一个关联类型Item

    pub trait Iterator {
         type Item;
         fn next(&mut self) -> Option;
    }
    impl Iterator for Counter {
         type Item = u32;
         fn next(&mut self) -> Option {
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Item是一个类型占位符,next方法返回Option 类型的值。这个trait的实现者会指定Item的具体类型。

    关联类型看起来像泛型,都是在定义时不指定具体类型。那么为什么不直接使用泛型呢?比如下面这样

    pub trait Iterator {
         fn next(&mut self) -> Option;
    }
    
    • 1
    • 2
    • 3

    区别在于当trait有泛型参数时,一个类型可以多次实现这个trait,每次为泛型参数指定不同的具体类型。比如Iterator for CounterIterator for CounterIterator for Counter。Counter有多个Iterator的实现。
    接着当使用Counter的next方法时,必须标明使用哪一个Iterator实现。比如Counter.next()Counter.next()Counter.next()
    通过关联类型,则无需标注类型,因为只能实现一次这个trait。只能有一个impl Iterator for Counter。当调用Counter的next时不必每次指定我们需要u32值的迭代器。

    (五)在别名中使用泛型
    比如

    type IoResult=Result;
    
    • 1

    二、泛型参数约束

    1.trait约束
    限制参数为必须实现了某个特性

    范例
    传递的参数必须是实现了Display特性的类型。

    use std::fmt::Display;
    fn main(){
         print_pro(10 as u8);
         print_pro(20 as u16);
         print_pro("Hello TutorialsPoint");
    }
    fn print_pro(para:T){
         println!("Inside print_pro generic function:");
         println!("{}",para);
    }
    编译运行结果如下
    Inside print_pro generic function:
    10
    Inside print_pro generic function:
    20
    Inside print_pro generic function:
    Hello TutorialsPoint
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2.多重约束
    使用+
    比如

    fn notify(item: T){}
    
    • 1

    实例

    struct StructA {}
    impl StructA {
         fn d(&self) {}
    }
    
    • 1
    • 2
    • 3
    • 4

    StructA类型必须在T已经实现B和C特性的前提下才能实现此impl块。

    3.使用where关键字简写约束
    约束也可以使用where分句来表达,它放在 { 的前面。

    例如:

    fn some_function(t: T, u: U){
    }
    
    • 1
    • 2

    可以简化成:

    fn some_function(t: &T, u: &U) -> i32
    where
    T: Display + Clone,
    U: Clone + Debug,{
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    下面的impl如果不用where从句,就无法直接表达。

    use std::fmt::Debug;
    trait PrintInOption {
         fn print_in_option(self);
    }
    // 这里需要一个 `where` 从句,否则就要表达成 `T: Debug`(这样意思就变了),
    // 或者改用另一种间接的方法。
    impl PrintInOption for T
    where
    Option: Debug {
         // 我们要将 `Option: Debug` 作为约束,因为那是要打印的内容。
         // 否则我们会给出错误的约束。
         fn print_in_option(self) {
              println!("{:?}", Some(self));
         }
    }
    fn main() {
         let vec = vec![1, 2, 3];
         vec.print_in_option();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    4.泛型参数约束与impl Trait的区别
    (1)注意,下面两种形式几乎是等价的:

    trait Trait {}
    fn with_generic_type(arg: T) { }
    fn with_impl_trait(arg: impl Trait) { }
    
    • 1
    • 2
    • 3

    (2)注意下面两种有差异:

    fn foo() -> T {}
    允许调用方确定返回类型。
    fn foo() -> impl Trait {}
    不允许调用方确定返回类型。
    
    • 1
    • 2
    • 3
    • 4

    5.泛型参数约束与trait对象的区别
    泛型参数一次只能替代一个具体类型,而trait对象则允许在运行时替代多种具体类型。
    例如

    pub struct Screen {
         pub shapes: Vec,
    }
    impl Screen
    where T: Draw {
         pub fn run(&self) {
              for shape in self.shapes.iter() {
                   shape.draw();
             }
         }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    shapes是一个全是Button类型或者全是TextField类型的列表。总之,所有元素类型是相同的。
    而使用trait对象,shapes能同时包含Button和TextField,各个元素类型可以不同。

    三、泛型参数默认值

    1.当使用泛型时,可以为泛型参数指定一个默认的具体类型。
    语法

     
    
    • 1

    这种情况的一个非常好的例子是运算符重载。运算符重载是指自定义运算符(比如 +)的行为。
    Rust并不允许创建自定义运算符或重载任意运算符,不过std::ops中所列出的运算符和相应的trait可以通过实现运算符相关trait来重载。
    例如,下例展示了如何在Point结构体上实现Add trait来重载 + 运算符,这样就可以将两个Point实例相加了

    use std::ops::Add;
    #[derive(Debug, PartialEq)]
    struct Point {
         x: i32,
         y: i32,
    }
    impl Add for Point {
         type Output = Point;
         fn add(self, other: Point) -> Point {
             Point {
                 x: self.x + other.x,
                 y: self.y + other.y,
             }
         }
    }
    fn main() {
         assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
         Point { x: 3, y: 3 });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    add方法将两个Point实例的x值和y值分别相加来创建一个新的Point。

    Add定义如下

    trait Add {
         type Output;
         fn add(self, rhs: RHS) -> Self::Output;
    }
    
    • 1
    • 2
    • 3
    • 4

    RHS=Self,Self就是泛型参数默认值,它表示实现Add的类型,在这里就是Point类型。如果实现Add时不指定RHS的具体类型,RHS的类型将是Self类型。

    2.不使用默认类型的例子。
    我们希望能够将毫米值与米值相加,并让Add的实现正确处理转换。可以为Millimeters实现Add并以Meters作为RHS

    use std::ops::Add;
    struct Millimeters(u32);
    struct Meters(u32);
    impl Add for Millimeters {
         type Output = Millimeters;
         fn add(self, other: Meters) -> Millimeters {
              Millimeters(self.0 + (other.0 * 1000))
         }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    为了使Millimeters和Meters能够相加,我们使用Add而不是使用默认的Add。

    四、泛型代码的性能

    Rust通过在编译时单态化来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
    编译器寻找所有泛型代码被调用的位置并针对具体类型生成特化代码。
    例子

    let integer = Some(5);
    let float = Some(5.0);
    
    • 1
    • 2

    当Rust编译这些代码的时候,它会单态化。编译器会读取传递给Option的值并发现有两种Option:一个对应i32,另一个对应f64。为此,它会将泛型定义Option展开为两个针对i32和f64的定义,接着将泛型定义替换为这两个具体的定义。
    编译器生成的单态化代码看起来像这样:

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Option被编译器替换为了具体的定义。因为Rust会将每种情况下的泛型代码编译为具体类型,使用泛型没有运行时开销。当代码运行时,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是Rust泛型在运行时极其高效的原因。

  • 相关阅读:
    2341. 数组能形成多少数对-数据统计
    Python网络爬虫的实践与应用
    maven打包可执行jar含依赖lib
    「二叉树与递归的一些框架思维」「1464. 数组中两元素的最大乘积」(每日刷题打卡Day33)[C++]
    Java EE企业级开发学习 -- day1
    数据库系统原理与应用教程(076)—— MySQL 练习题:操作题 160-167(二十):综合练习
    idea 配置ssm项目后配置文件的简要解析及功能类之间的联系
    【前端】学习前端vue框架MVVM模式
    VMware Workstation Pro详解
    Linux项目自动化构建工具-make/Makefile
  • 原文地址:https://blog.csdn.net/inxunxun/article/details/133244897