关联类型 (associated types):

  • 是在 trait 定义中指定占位符类型,这样 trait 的方法签名中就可以使用这些占位符类型。
  • trait 的实现者会针对特定的实现在这个类型的位置指定相应的具体类型。
  • 如此可以定义一个使用多种类型的 trait,直到实现此 trait 时都无需知道这些类型具体是什么。

例子:标准库提供的 Iterator trait

  1. pub trait Iterator {
  2. type Item; // 关联类型
  3. fn next(&mut self) -> Option<Self::Item>;
  4. }

关联类型看起来像一个类似泛型的概念,因为它允许定义一个函数而不指定其可以处理的类型。那么为什么要使用关联类型呢?

  1. impl Iterator for Counter {
  2. type Item = u32;
  3. fn next(&mut self) -> Option<Self::Item> {
  4. // --snip--
  5. }

使用关联类型的定义,我们只能选择一次 Item 会是什么类型,因为只能有一个 impl Iterator for Counter。当调用 Counternext 时不必每次指定我们需要 u32 值的迭代器。

默认泛型类型参数

Default Generic Type Parameters:为泛型指定一个默认的具体类型。语法:在声明泛型类型时使用 <PlaceholderType=ConcreteType>
如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。一个非常好的例子是用于 运算符重载Operator overloading ):在特定情况下自定义运算符(比如 +)行为的操作。

Rust 并不允许创建自定义运算符或重载任意运算符,不过 std::ops 中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载。

Add trait 定义 中的默认泛型类型:

  1. trait Add<RHS=Self> {
  2. type Output;
  3. // RHS 是一个泛型类型参数(“right hand side” 的缩写),它用于定义 add 方法中的 rhs 参数
  4. // 如果实现 Add trait 时不指定 RHS 的具体类型,RHS 的类型将是默认的 Self 类型,也就是在其上实现 Add 的类型
  5. fn add(self, rhs: RHS) -> Self::Output;
  6. }

重载 + 运算符:

  1. use std::ops::Add;
  2. #[derive(Debug, Copy, Clone, PartialEq)]
  3. struct Point {
  4. x: i32,
  5. y: i32,
  6. }
  7. // 等价于 impl Add<Self> for Point {
  8. impl Add for Point { // 默认泛型参数可以不写,但必须和 add 方法的 rhs 类型一致
  9. type Output = Self;
  10. fn add(self, rhs: Self) -> Self { Self { x: self.x + rhs.x, y: self.y + rhs.y } }
  11. }
  12. fn main() {
  13. assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 });
  14. }

对泛型结构体重载 + 运算符:同时使用 trait 默认泛型参数与 trait 关联类型

  1. use std::ops::Add;
  2. #[derive(Debug, Copy, Clone, PartialEq)]
  3. struct Point<T> {
  4. x: T,
  5. y: T,
  6. }
  7. // 等价于 impl<T: Add<Output = T>> Add<Self> for Point<T> {
  8. // 注意 Point 泛型 T 在 trait bound 里面使用的关联类型 `Output`
  9. impl<T: Add<Output = T>> Add for Point<T> {
  10. type Output = Self;
  11. fn add(self, other: Self) -> Self::Output { // 根据 Output 的值,Self::Output 等价于 Self
  12. Self { x: self.x + other.x, y: self.y + other.y }
  13. }
  14. }
  15. fn main() {
  16. assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 });
  17. }

Add trait 定义中使用默认类型参数意味着大部分时候无需指定额外的参数(即不必写 Add<Self>)。换句话说,一小部分实现的样板代码是不必要的,这样使用 trait 就更容易了。
一个实际案例:微米单位和米单位的数据相加,得到微米单位

  1. use std::ops::Add;
  2. // 这里是两个 newtype(元组结构体)
  3. #[derive(Debug, Clone)]
  4. struct Millimeters(u32);
  5. #[derive(Debug, Clone)]
  6. struct Meters(u32);
  7. impl Add<Meters> for Millimeters { // 默认泛型参数 RHS 不再是 Self
  8. type Output = Millimeters;
  9. // 1 meter = 1000 millimeters (毫米)
  10. // 这里获取了两个参数的所有权
  11. fn add(self, other: Meters) -> Millimeters { Millimeters(self.0 + (other.0 * 1000)) }
  12. }
  13. fn main() {
  14. let a = Millimeters(1);
  15. let b = Meters(1);
  16. let res = a.clone() + b.clone();
  17. println!("{:?} + {:?} = {:?}", a, b, res);
  18. }

默认参数类型主要用于如下两个方面:

  • 扩展类型而不破坏现有代码。
  • 在大部分用户都不需要的特定情况进行自定义。比如将两个相似的类型相加,从而提供了自定义额外行为的能力。

    调用同名的 方法、关联函数

  1. 实例名.方法名() 的方法在无同名的时候,可能来自于类型本身定义的方法,也可能来自于 trait 定义的方法,这时候没有歧义。
    然而,如果类型和 trait 的方法重名,甚至不同 trait 定义了相同的名称,那么如何消除名称的歧义,让我们调用想调用的方法呢?

方法(method) 的特点是,第一个参数为 self 之类的参数,代表实例自身,所以,可以使用 类型/trait名::方法名(实例) 来明确指明这个方法来自于类型的实现,还是哪个 trait 的实现。在方法重名情况下,实例名.方法名() 与调用 类型名::方法名(实例) 是一致的:

  1. trait Pilot {
  2. fn fly(&self);
  3. }
  4. trait Wizard {
  5. fn fly(&self);
  6. }
  7. struct Human;
  8. impl Pilot for Human {
  9. fn fly(&self) {
  10. println!("This is your captain speaking.");
  11. }
  12. }
  13. impl Wizard for Human {
  14. fn fly(&self) {
  15. println!("Up!");
  16. }
  17. }
  18. impl Human {
  19. fn fly(&self) {
  20. println!("*waving arms furiously*");
  21. }
  22. }
  23. fn main() {
  24. let person = Human;
  25. Pilot::fly(&person);
  26. Wizard::fly(&person);
  27. person.fly(); // 也可以选择写成 Human::fly(&person);
  28. }
  29. // 打印结果:
  30. // This is your captain speaking.
  31. // Up!
  32. // *waving arms furiously*
  1. 如果调用同名的关联函数呢?依然参照上面的语法吗?

并不是。关联函数的特点是,参数不含 self,也就是不含实例。当同一作用域的两个类型实现了同一 trait,Rust 就不能计算出我们期望的是哪一个类型:比如下面的 Wizard::fly() 到底是作用在 Human 结构体上,还是 Alien 结构体上。

  1. trait Wizard {
  2. fn fly();
  3. }
  4. struct Human;
  5. impl Human {
  6. fn fly() { println!("lol"); }
  7. }
  8. impl Wizard for Human {
  9. fn fly() { println!("Up!"); }
  10. }
  11. struct Alien;
  12. impl Wizard for Alien {
  13. fn fly() { println!("Boom!") }
  14. }
  15. fn main() {
  16. // Ambiguous: Wizard trait for Human or Alien?
  17. // Wizard::fly();
  18. // Specific! Fully qualified syntax:
  19. <Alien as Wizard>::fly();
  20. // And below might be confusing too: from struct or trait?
  21. Human::fly(); // actually from struct Human
  22. // Specific! Fully qualified syntax:
  23. <Human as Wizard>::fly();
  24. }
  25. // 打印结果:
  26. // Boom!
  27. // lol
  28. // Up!

完全限定语法fully qualified syntax )解决了这种同名带来歧义:因为我们需要指明关联函数来自于哪个类型和哪个 trait,其完整语法为

  1. <Type as Trait>::function(receiver_if_method, next_arg, ...);
  2. // 和 trait bound 语法 <Type: Trait> 一致

这里的 receiver_if_method 参数表明,method 也支持这个语法。这个语法完全反映了 类型+trait+方法/关联函数签名(除去返回值部分),不会存在任何歧义。
从而,我们可以把 同名方法的问题 与 同名关联函数的问题 得到统一的解决:

  1. fn main() {
  2. let person = Human;
  3. Pilot::fly(&person);
  4. <Human as Pilot>::fly(&person);
  5. Wizard::fly(&person);
  6. <Human as Wizard>::fly(&person);
  7. person.fly();
  8. Human::fly(&person);
  9. <Human>::fly(&person);
  10. }

当然,只有当存在多个同名实现而 Rust 需要帮助以便知道我们希望调用哪个实现时,才需要使用这个较为冗长的 完全限定语法

使用 supertrait 的功能

在 trait bound 中的,我们见过 “对泛型施加 trait bound” ,目的是限制泛型具有某种 trait 的功能,或者说让泛型使用某种 trait 功能。
把这个场景拓展至 trait 上——对 A trait 施加 B trait bound,也就是让 A trait 使用 B trait 的功能,那么 A trait 被成为 subtrait (子 trait),B trait 被称为 supertrait (父 trait)。
这两个场景的语法描述如下:

  1. // trait bound: <T: trait>
  2. impl<T: Supertrait> Subtrait for T { }
  3. // where T: Supertrait
  4. impl<T> Subtrait for T where T: Supertrait { }
  5. // Subtrait: Supertrait
  6. trait Subtrait: Supertrait { }
  7. // where Self: Supertrait
  8. trait Subtrait where Self: Supertrait { } /* 这里的 Self 指 Subtrait */

对泛型和对 trait 施加 trait bound 从语法上看如出一辙。
两个例子:

  1. //! 例子1:构造:半径、面积 trait 和单位圆结构体
  2. //!例子来源:https://doc.rust-lang.org/nightly/reference/items/traits.html#supertraits
  3. use std::f64::consts::PI;
  4. trait Shape {
  5. fn area(&self) -> f64;
  6. }
  7. trait Circle: Shape {
  8. fn radius(&self) -> f64 { (self.area() / PI).sqrt() }
  9. }
  10. fn print_area_and_radius<C: Circle>(c: C) {
  11. // Here we call the area method from the supertrait `Shape` of `Circle`.
  12. println!("Area: {}", c.area());
  13. println!("Radius: {}", c.radius());
  14. }
  15. struct UnitCircle;
  16. impl Shape for UnitCircle {
  17. fn area(&self) -> f64 { PI }
  18. }
  19. impl Circle for UnitCircle {}
  20. fn main() {
  21. let circle = UnitCircle;
  22. // let circle = Box::new(circle) as Box<dyn Circle>;
  23. print_area_and_radius(UnitCircle);
  24. let nonsense = circle.radius() * circle.area();
  25. println!("nonsense: {}", nonsense);
  26. }
  27. // 打印结果:
  28. // Area: 3.141592653589793
  29. // Radius: 1
  30. // nonsense: 3.141592653589793
  1. //! 例子2:打印带有星号框的值
  2. //!例子来源:https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#using-supertraits-to-require-one-traits-functionality-within-another-trait
  3. use std::fmt;
  4. #[derive(Debug)]
  5. struct Point {
  6. x: i32,
  7. y: i32,
  8. }
  9. impl fmt::Display for Point {
  10. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  11. write!(f, "({}, {})", self.x, self.y)
  12. }
  13. }
  14. trait OutlinePrint: fmt::Display {
  15. fn outline_print(&self) {
  16. let output = self.to_string();
  17. let len = output.len();
  18. println!("{}", "*".repeat(len + 4));
  19. println!("*{}*", " ".repeat(len + 2));
  20. println!("* {} *", output);
  21. println!("*{}*", " ".repeat(len + 2));
  22. println!("{}", "*".repeat(len + 4));
  23. }
  24. }
  25. impl OutlinePrint for Point {}
  26. fn main() { Point { x: 1, y: 3 }.outline_print(); }
  27. // 打印结果:
  28. // **********
  29. // * *
  30. // * (1, 3) *
  31. // * *
  32. // **********

newtype 模式:“绕开”孤儿原则

孤儿原则:只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait;或者说 不能为外部类型实现外部 trait

newtype pattern:利用元组结构体,把外部类型放入这个元组结构体,从而获得一个本地创建的新类型。如此便符合孤儿原则,间接 达到给外部类型实现外部 trait 的目的。使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。
例子:在 Vec<T> 上实现自定义的 Display trait —— 孤儿规则阻止我们直接这么做,因为 Display trait 和 Vec<T> 都定义于我们的 crate 之外。

  1. use std::fmt;
  2. #[derive(Debug)]
  3. struct Wrapper(Vec<String>); /* newtype pattern */
  4. impl fmt::Display for Wrapper {
  5. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  6. write!(f, "[{}]", self.0.join(", ")) // 注意字符串少了双引号
  7. }
  8. }
  9. fn main() {
  10. let w = Wrapper(vec![String::from("hello"), String::from("world")]);
  11. println!("w = {}", w);
  12. }
  13. // 打印结果:
  14. // w = [hello, world]

一些补充:

  1. 使用元组结构体的原因是它比较简洁,相比之下,完整的结构体需要字段来得到内部的类型,元组结构体只需要通过 .0 便可直接获得第一个元素。
  2. 如果我们不希望 Wrapper 的内部类型拥有的所有方法,那么可以自己实现我们想要的方法,不仅局限于给 Wrapper 实现内外部的 trait。

    1. // 比如构造我们想要的实例初始化方式,无需给 Wrapper 实现任何 trait
    2. impl Wrapper {
    3. fn new() -> Self { Self(vec!["start".to_string()]) }
    4. }
  3. Wrapper 是一个新的类型,它内部虽然只有一个值,但是我们不能直接使用 Vec<String> 的方法,必须间接地通过 Wrapper实例名.0 来调用 Vec 的方法,从而完全像 Vec<T> 那样对待 Wrapper

  4. 如果希望 Wrapper 直接 拥有其内部类型的每一个方法,那么可以为 Wrapper 实现 Deref trait,比如:
    1. use std::ops::Deref;
    2. #[derive(Debug)]
    3. struct Wrapper(Box<Vec<String>>);
    4. impl Deref for Wrapper {
    5. type Target = Vec<String>;
    6. fn deref(&self) -> &Self::Target { &self.0 }
    7. // 等价于以下有效 fully qualified syntax 写法:
    8. // fn deref(&self) -> &<Wrapper as Deref>::Target { &self.0 }
    9. }
    10. fn main() {
    11. let v = vec![String::from("hello"), String::from("world")];
    12. let w = Wrapper(Box::new(v));
    13. // indirectly call Vec's method (alse use automatic referencing):
    14. // let (left, right) = w.0.split_at(1);
    15. // automatic referencing + deref coercion:
    16. // let (left, right) = (&w).deref().split_at(1);
    17. let (left, right) = w.split_at(1);
    18. println!("left = {:?}, right = {:?}", left, right);
    19. }
    还有其他场景很适合 newtype:”静态的确保某值(以及类型)不被混淆”、”轻量级封装”。