5分钟速读之Rust权威指南(四十)高级trait

513 阅读5分钟

前面章节我们有介绍过trait像“接口”一样,讲过了简单的使用方式,这一节我们看下trait的一些相关高级特性

在trait的定义中使用关联类型指定占位类型

关联类型(associated type)是trait中的类型占位符,它可以被用于trait的方法签名中,例如Iterator trait中就含有关联类型Item

trait Iterator {
  type Item;
  fn next(&mut self) -> Option<Self::Item>;
}

使用这个关联类型的Iterator trait

struct Counter {
  value: u32
}

impl Iterator for Counter {
  type Item = u32; // 定义Item类型
  fn next(&mut self) -> Option<Self::Item> { // 返回值为Option<Self::Item>
    if self.value < 3 {
      self.value += 1;
      Some(self.value)
    } else {
      None
    }
  }
}

let mut counter = Counter {
  value: 0
};

println!("{:?}", counter.next()); // Some(1)
println!("{:?}", counter.next()); // Some(2)
println!("{:?}", counter.next()); // Some(3)
println!("{:?}", counter.next()); // None

有同学可能在想,为什么不用泛型来实现呢,比如这样使用泛型实现Iterator

trait Iterator<T> { // 定义泛型参数
  fn next(&mut self) -> Option<T>;
}

// 使用u32类型
impl Iterator<u32> for Counter {
  fn next(&mut self) -> Option<u32> {
    if self.value < 3 {
      self.value += 1;
      Some(self.value)
    } else {
      None
    }
  }
}
println!("{:?}", counter.next()); // Some(1)
println!("{:?}", counter.next()); // Some(2)
println!("{:?}", counter.next()); // Some(3)
println!("{:?}", counter.next()); // None

上面使用关联类型和泛型都可以实现Iterator trait,但是为什么要设计一种关联类型,而不用泛型呢?

试想,既然利用泛型可以实现实现u32类型的迭代器:impl Iterator<u32> for Counter,那么就可以也实现其他类型的迭代impl Iterator<String> for Counter,那么next方法中每次返回的就是一个String,如果我们为Counter同时实现了这两种类型的迭代器,那么在for循环的时候,应该使用哪个类型的next方法呢?为了避免这种冲突,所以只能使用关联类型来实现Iterator trait,进而阻止单个类型多次实现同一个trait的行为。

运算符重载

rust中的很多运算符是可以重载的,这里以+为例,我们可以自定义实现+的行为,下面为结构体上实现Add trait来重载+运算符,实现结构体相加的功能:

use std::ops::Add;
#[derive(Debug)]
struct Point {
  x: i32,
  y: i32,
}
// 为Point重载 + 运算符的行为
impl Add for Point {
  // 定义关联类型Output为add的返回值
  type Output = Point;
  fn add(self, other: Point) -> Point {
    Point {
      x: self.x + other.x,
      y: self.y + other.y,
    }
  }
}
println!("{:?}", Point { x: 1, y: 0 } + Point { x: 2, y: 3 });
// Point { x: 3, y: 3 }

根据上面的例子我们可以顺便看下Add trait

// 注意定义了默认的RHS类型是自身,也就是上面实现了Add的类型
// 比如:1 + 2 RHS就是1的类型,也就是i32
trait Add<RHS = Self> {
  type Output;
  fn add(self, rhs: RHS) -> Self::Output;
}

当然我们可以手动指定类型:

#[derive(Debug)]
struct Millimeters(u32);
#[derive(Debug)]
struct Meters(u32);

// 指定Millimeters在使用 + 运算符时,+ 右边的类型是Meters
impl Add<Meters> for Millimeters {
  type Output = Millimeters;
  fn add(self, other: Meters) -> Millimeters {
    Millimeters(self.0 + (other.0 * 1000))
  }
}
println!("{:?}", Millimeters(1) + Meters(2));
// Millimeters(2001)

用于消除歧义的完全限定语法:

当我们为一个结构体实现多个trait时,难保这些trait中有重名的方法,而且结构体中也可能已经存在相同的方法:

struct Human;

impl Human {
  // Human结构体自身的方法
  fn fly(&self) {
    println!("挥舞着手臂");
  }
}

trait Pilot {
  // Pilot trait也具有fly方法
  fn fly(&self);
}
trait Wizard {
  // Wizard trait也具有fly方法
  fn fly(&self);
}
// 为Human实现Pilot
impl Pilot for Human {
  fn fly(&self) {
    println!("这是你的队长在讲话");
  }
}
// 为Human实现Wizard
impl Wizard for Human {
  fn fly(&self) {
    println!("起飞!");
  }
}

let person = Human;

// 默认会调用Human自身实现的方法
person.fly(); // 挥舞着手臂

手动指定明确调用的方法,有点像JS中函数的call方法:

Human::fly(&person); // 挥舞着手臂
Pilot::fly(&person); // 这是你的队长在讲话
Wizard::fly(&person); // 起飞!

关联函数是trait的一部分,但没有self参数,可以理解为JS中class的静态方法(也就是类上的方法,而非prototype上的方法,比如Object.create())。当同一作用域的两个类型实现了同一trait,rust 就不能计算出我们期望使用的是哪一个,除非使用 完全限定语法(fully qualified syntax):

trait Animal {
  fn baby_name() -> String;
}

struct Dog;

impl Dog {
  fn baby_name() -> String {
    String::from("Spot")
  }
}

impl Animal for Dog {
  fn baby_name() -> String {
    String::from("puppy")
  }
}

// 默认仍然会调用Dog自身实现的baby_name方法
println!("小狗的名字叫 {}", Dog::baby_name());
// 小狗的名字叫 Spot

println!("小狗的名字叫 {}", Animal::baby_name());
// 报错, 由于Animal::baby_name是一个没有self参数的关联函数而不是方法,
// 所以Rust无法推断出我们想要调用哪一个Animal::baby_name的实现

// 使用完全限定语法解决
println!("小狗的名字叫 {}", <Dog as Animal>::baby_name());
// 小狗的名字叫 puppy

用于在trait中附带另外一个trait功能的超trait

有时在一个A trait使用B trait的功能。在这种情况下,我们需要使当前A trait的功能依赖于B trait同时被实现,这个被依赖的B trait也就是当前A trait的超trait(supertrait):

#[derive(Debug)]
struct Position(u32, u32);
trait Logger: std::fmt::Debug { // Logger的实现依赖于Debug的实现
  fn log(&self) {
    println!("position: {:?}", self);
  }
}
impl Logger for Position {} // 报错,Logger没有实现Debug

简单的为Logger实现Debug,这里利用派生:

#[derive(Debug)]
struct Position(u32, u32);
trait Logger: std::fmt::Debug {
  fn log(&self) {
    println!("position: {:?}", self);
  }
}
impl Logger for Position {}
let position = Position(1, 2);
position.log(); // position: Position(1, 2)

使用newtype模式在外部类型上实现外部trait

前面有说过trait实现的限制:只有当类型和对应trait中的任意一个定义在本地包内时(孤儿规则),我们才能够为该类型实现这一trait,实际上可以使用newtype模式来巧妙地绕过这个限制,下面尝试为Vec<String>实现Display trait

impl std::fmt::Display for Vec<String> {};
// 报错,因为Display和Vec都是外部定义的

创建Wrapper类型封装Vec<String>以便能够实现Display trait

impl std::fmt::Display for Wrapper {
  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
    write!(f, "[{}]", self.0.join(", ")) // 访问self.0,也就是Vec<String>来使用Display
  }
}
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w); // w = [hello, world]

封面图:跟着Tina画美国

关注「码生笔谈」公众号,阅读更多最新章节