当前位置: 首页 > 工具软件 > rust-jsonnet > 使用案例 >

【Rust 笔记】09-特型与泛型

喻子航
2023-12-01

09 - 特型与泛型

  • Rust 的多态性:

    • 特型(trait)
    • 泛型(generic)
  • 特型:Rust 对接口或抽象基类的实现。

    • 如下写字节的特型 std::io::Write

      trait Write {
        fn write(&mut self, buf: &[u8]) -> Result<usize>;
        fn flus(&mut self) -> Result<()>;
        fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... }
        ...
      }
      
    • 内置特型是 Rust 语言为操作符重载及其其他特性提供的钩子(hook)。

  • 泛型:泛型函数或类型可以搭配很多种不同类型的值使用。

    /// 给定两个值,取出较小的值
    fn min<T: Ord>(value1: T, value2: T) -> T {
      if value1 <= value2 {
        value1
      } else {
        value2
      }
    }
    
    • <T: Ord> 代表 min 可以使用实现 Ord 特型的任意类型参数 T;
    • 声明 T: Ord 叫做绑定。

9.1 - 使用特型

  • 特型是一种任何类型都可以选择支持或不支持的特性。常见的特型:
    • 实现 std::io::Write 的值可以执行写字节操作;
    • 实现 std::iter::Iterator 的值可以产生值的序列;
    • 实现 std::clone::Clone 的值可以在内存中克隆自身;
    • 实现 std::fmt::Debug 的值可以使用 println!(){:?} 格式说明符打印出来。
  • 常见的标准库实现了上述特型:
    • std::fs::File 实现了 Write 特型,可以将字节写入本地文件。std::net::TcpStream 则是将字节写入网络连接。Vec<u8> 同样实现了 Write。在字节向量上每次调用.write(),都可以在向量末尾追加一些数据。
    • Range<i32> 实现了 Iterator 特型,与切片、散列表等相关的其他爹大气类型也实现了这个特型。
    • 除了 TcpStream 这种不仅仅表示内存中数据的类型外,大多数标准库类型都实现了 Clone 特型。
    • 大多数标准库都支持 Debug 特型。
  • 特型本身必须在作用域中。
  • 可以使用特型给任何类型添加新方法。
  • CloneIterator 方法,默认一直在作用域中。它们是标准前置模块的一部分,Rust 会自动将其导入到所有模块中。
  • 标准前置模块,本质是一个特型集合。

9.1.1 - 特型目标

  • Rust 有两种方式使用特型编写多态化代码:特型目标与泛型。

  • Rust 不允许声明 Write 类型的变量:因为变量的大小必须在编译时就知道,而实现 Write 的类型可以是任意大小。

  • 特型目标:指向一个特型类型(如下面的 writer)的引用,具有引用的共同性:指向某个值,有生命期,可以是 mut 或共享引用。

    let mut buf: Vec<u8> = vec![];
    let writer: &mut Write = &mut buf;
    
  • 特型目标的不同之处:Rust 在编译时通常不知道引用目标的类型。

9.1.2 - 特型目标的内存布局

  • 特型目标是一个胖指针,包含指向值的指针和指向表示该值类型的表的指针。每个特型目标都占用两个机器字。

  • Rust 在必要时会自动将普通引用转换为特型目标。

    let mut local_file = File::create("hello.txt")?;  // &mut local_file的类型是&mut File
    say_hello(&mut local_file)?;   // say_hello的参数类型是&mut Write
    

9.1.3 - 泛型函数

  • 如下 say_hello() 泛型函数,接收一个特型目标作为参数。

    fn say_hello<W: Write>(out: &mut W) -> std::io::Result<()> {
      out.write_all(b"hello world\n")?;
      out.flush()
    }
    
    • <W: Write> 是类型参数(type parameter),表示实现了 Write 特型的某种类型。
    • 类型参数通常是一个大写字母。
  • 泛型函数可以有多个类型参数:

    fn run_query<M: Mapper + Serialize, R: Reducer + Serialize>(data: &DataSet, map: M, reduce: R) -> Results { ... }
    
    • 上述代码的绑定显得比较长,可以使用 where 关键字,将上述代码结构清晰的表述出来:

      fn run_query<M, R>(data: &DataSet, map: M, reduce: R) -> Results where M: Mapper + Serialize, R: Reducer + Serialize { ... }
      
    • 这样的类型参数 MR 仍然是提前声明,但是绑定转移到了后面。

    • where 子句也适用于泛型结构体、枚举、类型别名和方法等,任何允许使用绑定的地方。

  • 泛型函数可以同时拥有生命期参数和类型参数。生命期参数写在前面:

    fn nearest<'t, 'c, P>(target: &'t P, candidates: &'c [P]) -> &'c P where P: MeasureDistance {
      ...
    }
    
  • 除了函数外,其他特性也支持泛型:

    • 泛型结构体

    • 泛型枚举

    • 泛型方法,不管定义该方法的类型是不是泛型的。

      impl PancakeStack {
        fn push<T: Topping>(&mut self, goop: T) -> PancakeResult<()> {
          ...
        }
      }
      
    • 泛型类型别名

      type PancakeResult<T> = Result<T, PancakeError>;
      
    • 泛型特型

  • 绑定、where 子句、生命期参数等,可以适用于上述所有泛型特性项。

9.1.4 - 使用场景

  • 定义一个混合类型的值,使之全部集合在一起,推荐使用特性目标。
  • 使用特型目标可以减少编译后的总代码量,防止出现代码膨胀(code bloat),适用于制作微控制器。
  • 泛型相比特型目标的优势:
    • 速度:消除了动态查找的时间;
    • 并非所有特型都支持特型目标。如静态方法只对泛型有效,完全没有考虑特型对象。

9.2 - 定义和实现特型

  • 定义特型:给它命名并列出特型方法的类型签名:

    trait Visible {
      fn draw(&self, canvas: &mut Canvas);
      fn hit_test(&self, x: i32, y: i32) -> bool;
    }
    
  • 实现特型,使用 impl TraitName for Type 语法:

    impl Visible for Broom {
      fn draw(&self, canvas: &mut Canvas) {
        for y in self.y - self.height - 1 .. self.y {
          canvas.write_at(self.x, y, '|');
        }
        canvas.write_at(self.x, self.y, 'M');
      }
    
      fn hit_test(&self, x: i32, y: i32) -> bool {
        self.x == x
        && self.y - self.height - 1 <= y
        && y <= self.y
      }
    }
    
    • 特型 impl 中定义的,都是特型的特性。

    • 如果要添加一个支持 Broom::draw() 的辅助方法,那么就在单独的 impl 块中定义它:

      impl Broom {
        fn broomstick_range(&self) -> Range<i32> {
          self.y - self.height - 1 .. self.y
        }
      }
      
      impl Visible for Broom {
        fn draw(&self, canvas: &mut Canvas) {
          for y in self.broomstick_range() {
            ...
          }
          ...
        }
        ...
      }
      

9.2.1 - 默认方法

  • 标准库的 Write 特型的定义包含一个 write_all 的默认实现:

    trait Write {
      fn write(&mut self, buf: &[u8]) -> Result<usize>;
      fn flush(&mut self) -> Result<()>;
      fn write_all(&mut self, buf: &[u8]) -> Result<()> {
        let mut bytes_written = 0;
        while bytes_written < buf.len() {
          bytes_written += self.write(&buf[bytes_written..])?;
        }
        Ok(())
      }
      ...
    }
    
    • writeflush 方法是每个书写器必须实现的基本方法;
    • 书写器也可以实现 write_all,如果没有,那就会使用上面的默认实现。
  • 标准库中默认方法最多的是 Iterator 特型,它有一个必须方法(.next())和其他很多默认方法。

9.2.2 - 特型与其他人的类型

  • Rust 允许在任意类型上实现任意特型,只要当前包中导入了相关特性或类型即可。

  • 每当想给任何类型添加方法时,都可以使用特型来完成:

    trait IsEmoji {
      fn is_emoji(&self) -> bool;
    }
    
    /// 为内置字符类型实现IsEmoji方法,这种特型称为扩展特型
    impl IsEmoji for char {
      fn is_emoji(&self) -> bool {
        ...
      }
    }
    
    assert_eq!('$'.is_emoji(), false);
    
  • 可以使用泛型 impl 块,让一个类型家族一次性全部实现一个扩展特型。

    use std::io::{self, Write};
    
    /// 为可以发送HTML的值定义扩展特型
    trait WriteHtml {
      fn write_html(&mut self, html: &HtmlDocument) -> io::Result<()>;
    }
    
    /// 这样就可以向任意std::io::writer写入HTML了
    // 对每个实现了Write的类型W,在这里为W再实现特型WriteHtml
    impl<W: Write> WriteHtml for W {
      fn write_html(&mut self, html: &HtmlDocument) -> io::Result<()> {
        ...
      }
    }
    
  • serde 为所有相关类型都添加了一个.serialize() 方法:

    use serde::Serialize;
    use serde_json;
    
    pub fn save_configuration(config: &HashMap<String, String>) -> std::io::Result<()> {
      // 创建一个JSON序列化处理程序,将数据写入文件
      let writer = File::create(config_filename())?;
      let mut serializer = serde_json::Serializer::new(writer);
    
      // 剩下的任务交给serde的.serialize()方法处理
      config.serialize(&mut serializer)?;
      Ok(())
    }
    
  • 连贯规则(coherence rule):在实现特型时,相关的特型或类型必须都一个在当前包中是新的。确保特型实现的唯一性。

9.2.3 - 特型中的 Self

  • 特型可以使用 Self 关键字作为类型。

    pub trait Clone {
      fn clone(&self) -> Self;
      ...
    }
    
    • Self 作为返回类型:x.clone() 的类型就是 x 的类型。
  • 下述特型有两个实现:

    pub trait Spliceable {
      fn splice(&self, other: &Self) -> Self
    }
    
    // self和other的类型必须完全一样
    
    impl Spliceable for CherryTree {
      fn splice(&self, other: &Self) -> Self {    // 此处Self是CherryTree的别名
        ...
      }
    }
    
    impl Spliceable for Mammoth {
      fn splice(&self, other: &Self) -> Self {    // 此处Self是Mammoth的别名
        ...
      }
    }
    
  • 使用 Self 类型的特型于特型目标不能共存:

    // 错误:特型Spliceable不能成为引用目标
    fn splice_anything(left: &Spliceable, right: &Spliceable) {
      let combo = left.splice(right);
      ...
    }
    
    • 使用特型目标,类型在运行时才能确定;
    • 在编译时,Rust 不能判断 leftright 是不是同一种类型
  • 特型目标只能针对最简单的特型实现。这种特型可以使用 Java 中的接口,或者 C++ 中的抽象基类实现。

  • 特型的高级特型,不能与特型目标共存。

  • 如何设计一个目标友好的特型:

    pub trait MegaSpliceable {
      fn splice(&self, other: &MegaSpliceable) -> Box<MegaSpliceable>;
    }
    // 调用.splice()方法时,other参数的类型不必跟self的类型完全一样
    // 只要两者的类型都是MegaSpliceable就可以通过类型检查
    

9.2.4 - 子特型

  • 可以将一个特型声明为另一个特型的扩展

    // 所有与角色Creature有关的代码,也可以使用来自Visible特型的方法
    trait Creature: Visible {
      fn position(&self) -> (i32, i32);
      fn facing(&self) -> Direction;
      ...
    }
    
  • 所有实现 Creature 的类型,必须也实现 Visible 特型:

    impl Visible for Broom {
      ...
    }
    impl Creature for Broom {
      ...
    }
    
  • 子类型类似 Java 或 C# 的字接口,是一个特型要用另外几个扩展已有特型的表达式。

9.2.5 - 静态方法

  • 不同于其他面向对象语言,Rust 特型可以包含静态方法和构造函数。

    trait StringSet {
      /// 返回一个新的空集合
      fn new() -> Self;
    
      /// 返回一个包含strings中所有字符串的集合
      fn from_slice(strings: &[&str]) -> Self;
    
      /// 确定当前集合是否包含特定的value
      fn contains(&self, string: &str) -> bool;
    
      /// 向当前集合中添加一个字符串
      fn add(&mut self, string: &str);
    }
    
    • 所有实现 StringSet 特型的类型,都必须实现这 4 个关联函数

    • 前两个函数不接收 self 参数,它们充当构造函数。在非泛型代码中,这些函数可以使用:: 语法调用,就跟调用其他静态方法一样:

      // 创建两个实现了StringSet的假设类型的集合
      let set1 = SortedStringSet::new();
      let set2 = HashedStringSet::new();
      
    • 在泛型代码中,类型是可变的:

      /// 返回document中不在wordlis中的词的集合
      fn unknown_words<S: StringSet>(document: &Vec<String>, wordlist: &S) -> S {
        let mut unknowns = S::new();
        for word in document {
          if !wordlist.contains(word) {
            unknowns.add(word);
          }
        }
        unknowns
      }
      
  • 特型目标不支持静态方法:如果要使用特型目标 &StringSet,就必须修改特型,给每个静态方法都添加 where Self: Sized 绑定。

    trait StringSet {
      fn new() -> Self where Self: Sized;
      fn from_slice(strings: &[&str]) -> Self where Self: Sized;
      fn contains(&self, string: &str) -> bool;
      fn add(&mut self, string: &str);
    }
    
    • 告诉 Rust,特型目标将免于支持这个静态方法。
    • 这样使用 &StringSet 特型目标,可以调用.contains().add() 方法。

9.3 - 完全限定方法调用

  • 方法其实是一种特殊的函数:

    "hello".to_string()
    
    // 等价于
    str::to_string("hello")
    
  • to_string 是标准 ToString 特型的方法,上述代码等价于:

    ToString::to_string("hello")
    
    <str as ToString>::to_string("hello")
    
  • 上述 4 种方法调用,完成相同的操作:

    • 一般情况下,value.method() 这种形式比较常用;
    • 其他形式称为限定方法调用:
      • 需要指定方法关联的类型或特型
    • 最后一种带尖括号的形式,同时指定了关联的类型,和特型,因此称为完全限定方法调用。
  • 完全限定方法调用,要确切指定使用的方法,应用场景有:

    • 两个方法同名

      outlaw.draw();  // 错误,.draw()方法中包含outlaw方法
      Visible::draw(&outlaw);
      HasPistol::draw(&outlaw);
      
    • 无法推断 self 参数的类型

      let zero = 0;   // 类型未指定
      zeor.abs();     // 错误,没有找到abs方法
      i64::abs(zero); // 可以
      
    • 将函数本身作为值

      let words: Vec<String> = 
          line.split_whitespace()        // 产生&str值的迭代器
              .map(<str as ToString>::to_string)  // 可以
              .collect();
      
    • 在宏里调用特型方法。

  • 完全限定语法也适用于静态方法。

9.4 - 定义类型关系的特型

  • 特型是类型可以实现的一组方法。
  • 特型可以用来描述类型之间的关系。有以下 3 种方式:

9.4.1 - 关联类型:迭代器的工作原理

  • std::iter::Iterator 特型通过自身产生值的类型,将不同迭代器类型关联起来。

  • 迭代器:能够通过它遍历一系列值的对象。

    pub trait Iterator {
      type Item;    // 关联类型(associated type)
      fn next(&mut self) -> Option<Self::Item>;
      ...
    }
    
    • 所有实现 Iterator 的类型,都必须指定自己产生的项(item)的类型。

    • 实现一个 Iterator 的类型:

      // std::env标准库模块的部分代码
      impl Iterator for Args {
        type Item = String;   // 类型声明
      
        fn next(&mut self) -> Option<String> {
          ...
        }
        ...
      }
      
  • 泛型代码可以使用关联类型:

    /// 遍历一个迭代器,将它们的值存储到一个新变量中
    fn collect_into_vector<I: Iterator>(iter: I) -> Vec<I::Item> {
      let mut results = Vec::new();
      for value in iter {
        results.push(value);
      }
      results
    }
    
  • 关联类型在特型需要覆盖除一个方法之外的定义时也很常用:

    • 一个线程池的库中的 Task 特型(表示工作单元),可以包含一个关联的 Output 特型;

    • 一个 Pattern 特型(表示搜索字符串的一种方式),可以包含一个关联的 Match 类型,表示模式与字符串匹配之后收集到的所有信息:

      trait Patern {
        type Match;
      
        fn search(&self, string: &str) -> Option<Self::Match>;
      }
      
      /// 可以在字符串中搜索特定的字符
      impl Pattern for char {
        /// Match(匹配)表示的时发现字符的位置
        type Match = usize;
      
        fn search(&self, string: &str) -> Option<usize> {
          ...
        }
      }
      
    • 一个操作关系数据库的库可以有一个 DatabaseConnection 特型,可以有表示事务、指针、初始化语句等的关联函数。

9.4.2 - 泛型特型:操作符重载的原理

  • std::ops::Mul 特型关联可以参与乘法运算的类型。

    /// 支持*操作符的类型实现的特型
    pub trait Mul<RHS> {    // RHS:Right Hand Side右手边
      /// 应用*操作符之后返回的类型结果
      type Output;
    
      /// *操作符对应的方法
      fn mul(self, rhs: RHS) -> Self::Output;
    }
    
    • 表达式 lhs * rhsMul::mul(lhs, rhs) 的简写

9.4.3 - 伴型特型:rand::random 工作原理

  • rand 中既包含一个跟随机数生成器有关的特型 rand::Rng,还包含一个能够被随机生成的类型有关的特型 rand::Rand
  • 伴型特型:用来协同工作的特型。

9.5 - 逆向工程绑定

  • 如下面的代码:

    use std::ops::{Add, Mul};
    
    fn dot<N>(v1: &[N], v2: &[N]) -> N 
        where N: Add<Output=N> + Mul<Output=N> + Default + Copy  // 对N的绑定
    {
      let mut total = N::default();
      for i in 0..v1.len() {
        total = total + v1[i] * v2[i];
      }
      total
    }
    
    #[test]
    fn test_dot() {
      assert_eq!(dot(&[1, 2, 3, 4], &[1, 1, 1, 1]), 10);
      assert_eq!(dot(&[53.0, 7.0], &[1.0, 5.0]), 88.0);
    }
    
  • N 的绑定进行了逆向工程,让编译器当指导,反复检查自己的工作

  • Number 特型包含所有操作符和方法。

  • 逆向工程绑定的优点:

    • 可以让泛型代码具有向前兼容的能力。
    • 能通过编译器报错指导要解决的麻烦在哪里。
    • 在代码和文档里都存在。

详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第十一章
原文地址

 类似资料: