关联类型 (associated types):
- 是在 trait 定义中指定占位符类型,这样 trait 的方法签名中就可以使用这些占位符类型。
- trait 的实现者会针对特定的实现在这个类型的位置指定相应的具体类型。
- 如此可以定义一个使用多种类型的 trait,直到实现此 trait 时都无需知道这些类型具体是什么。
例子:标准库提供的 Iterator trait
pub trait Iterator {type Item; // 关联类型fn next(&mut self) -> Option<Self::Item>;}
关联类型看起来像一个类似泛型的概念,因为它允许定义一个函数而不指定其可以处理的类型。那么为什么要使用关联类型呢?
impl Iterator for Counter {type Item = u32;fn next(&mut self) -> Option<Self::Item> {// --snip--}
使用关联类型的定义,我们只能选择一次 Item 会是什么类型,因为只能有一个 impl Iterator for Counter。当调用 Counter 的 next 时不必每次指定我们需要 u32 值的迭代器。
默认泛型类型参数
Default Generic Type Parameters:为泛型指定一个默认的具体类型。语法:在声明泛型类型时使用 <PlaceholderType=ConcreteType>
如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。一个非常好的例子是用于 运算符重载 (Operator overloading ):在特定情况下自定义运算符(比如 +)行为的操作。
Rust 并不允许创建自定义运算符或重载任意运算符,不过
std::ops中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载。
Add trait 定义 中的默认泛型类型:
trait Add<RHS=Self> {type Output;// RHS 是一个泛型类型参数(“right hand side” 的缩写),它用于定义 add 方法中的 rhs 参数// 如果实现 Add trait 时不指定 RHS 的具体类型,RHS 的类型将是默认的 Self 类型,也就是在其上实现 Add 的类型fn add(self, rhs: RHS) -> Self::Output;}
重载 + 运算符:
use std::ops::Add;#[derive(Debug, Copy, Clone, PartialEq)]struct Point {x: i32,y: i32,}// 等价于 impl Add<Self> for Point {impl Add for Point { // 默认泛型参数可以不写,但必须和 add 方法的 rhs 类型一致type Output = Self;fn add(self, rhs: Self) -> Self { Self { x: self.x + rhs.x, y: self.y + rhs.y } }}fn main() {assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 });}
对泛型结构体重载 + 运算符:同时使用 trait 默认泛型参数与 trait 关联类型
use std::ops::Add;#[derive(Debug, Copy, Clone, PartialEq)]struct Point<T> {x: T,y: T,}// 等价于 impl<T: Add<Output = T>> Add<Self> for Point<T> {// 注意 Point 泛型 T 在 trait bound 里面使用的关联类型 `Output`impl<T: Add<Output = T>> Add for Point<T> {type Output = Self;fn add(self, other: Self) -> Self::Output { // 根据 Output 的值,Self::Output 等价于 SelfSelf { 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 });}
在 Add trait 定义中使用默认类型参数意味着大部分时候无需指定额外的参数(即不必写 Add<Self>)。换句话说,一小部分实现的样板代码是不必要的,这样使用 trait 就更容易了。
一个实际案例:微米单位和米单位的数据相加,得到微米单位
use std::ops::Add;// 这里是两个 newtype(元组结构体)#[derive(Debug, Clone)]struct Millimeters(u32);#[derive(Debug, Clone)]struct Meters(u32);impl Add<Meters> for Millimeters { // 默认泛型参数 RHS 不再是 Selftype Output = Millimeters;// 1 meter = 1000 millimeters (毫米)// 这里获取了两个参数的所有权fn add(self, other: Meters) -> Millimeters { Millimeters(self.0 + (other.0 * 1000)) }}fn main() {let a = Millimeters(1);let b = Meters(1);let res = a.clone() + b.clone();println!("{:?} + {:?} = {:?}", a, b, res);}
默认参数类型主要用于如下两个方面:
实例名.方法名()的方法在无同名的时候,可能来自于类型本身定义的方法,也可能来自于 trait 定义的方法,这时候没有歧义。
然而,如果类型和 trait 的方法重名,甚至不同 trait 定义了相同的名称,那么如何消除名称的歧义,让我们调用想调用的方法呢?
方法(method) 的特点是,第一个参数为 self 之类的参数,代表实例自身,所以,可以使用 类型/trait名::方法名(实例) 来明确指明这个方法来自于类型的实现,还是哪个 trait 的实现。在方法重名情况下,实例名.方法名() 与调用 类型名::方法名(实例) 是一致的:
trait Pilot {fn fly(&self);}trait Wizard {fn fly(&self);}struct Human;impl Pilot for Human {fn fly(&self) {println!("This is your captain speaking.");}}impl Wizard for Human {fn fly(&self) {println!("Up!");}}impl Human {fn fly(&self) {println!("*waving arms furiously*");}}fn main() {let person = Human;Pilot::fly(&person);Wizard::fly(&person);person.fly(); // 也可以选择写成 Human::fly(&person);}// 打印结果:// This is your captain speaking.// Up!// *waving arms furiously*
- 如果调用同名的关联函数呢?依然参照上面的语法吗?
并不是。关联函数的特点是,参数不含 self,也就是不含实例。当同一作用域的两个类型实现了同一 trait,Rust 就不能计算出我们期望的是哪一个类型:比如下面的 Wizard::fly() 到底是作用在 Human 结构体上,还是 Alien 结构体上。
trait Wizard {fn fly();}struct Human;impl Human {fn fly() { println!("lol"); }}impl Wizard for Human {fn fly() { println!("Up!"); }}struct Alien;impl Wizard for Alien {fn fly() { println!("Boom!") }}fn main() {// Ambiguous: Wizard trait for Human or Alien?// Wizard::fly();// Specific! Fully qualified syntax:<Alien as Wizard>::fly();// And below might be confusing too: from struct or trait?Human::fly(); // actually from struct Human// Specific! Fully qualified syntax:<Human as Wizard>::fly();}// 打印结果:// Boom!// lol// Up!
完全限定语法 (fully qualified syntax )解决了这种同名带来歧义:因为我们需要指明关联函数来自于哪个类型和哪个 trait,其完整语法为
<Type as Trait>::function(receiver_if_method, next_arg, ...);// 和 trait bound 语法 <Type: Trait> 一致
这里的 receiver_if_method 参数表明,method 也支持这个语法。这个语法完全反映了 类型+trait+方法/关联函数签名(除去返回值部分),不会存在任何歧义。
从而,我们可以把 同名方法的问题 与 同名关联函数的问题 得到统一的解决:
fn main() {let person = Human;Pilot::fly(&person);<Human as Pilot>::fly(&person);Wizard::fly(&person);<Human as Wizard>::fly(&person);person.fly();Human::fly(&person);<Human>::fly(&person);}
当然,只有当存在多个同名实现而 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)。
这两个场景的语法描述如下:
// trait bound: <T: trait>impl<T: Supertrait> Subtrait for T { }// where T: Supertraitimpl<T> Subtrait for T where T: Supertrait { }// Subtrait: Supertraittrait Subtrait: Supertrait { }// where Self: Supertraittrait Subtrait where Self: Supertrait { } /* 这里的 Self 指 Subtrait */
对泛型和对 trait 施加 trait bound 从语法上看如出一辙。
两个例子:
//! 例子1:构造:半径、面积 trait 和单位圆结构体//!例子来源:https://doc.rust-lang.org/nightly/reference/items/traits.html#supertraitsuse std::f64::consts::PI;trait Shape {fn area(&self) -> f64;}trait Circle: Shape {fn radius(&self) -> f64 { (self.area() / PI).sqrt() }}fn print_area_and_radius<C: Circle>(c: C) {// Here we call the area method from the supertrait `Shape` of `Circle`.println!("Area: {}", c.area());println!("Radius: {}", c.radius());}struct UnitCircle;impl Shape for UnitCircle {fn area(&self) -> f64 { PI }}impl Circle for UnitCircle {}fn main() {let circle = UnitCircle;// let circle = Box::new(circle) as Box<dyn Circle>;print_area_and_radius(UnitCircle);let nonsense = circle.radius() * circle.area();println!("nonsense: {}", nonsense);}// 打印结果:// Area: 3.141592653589793// Radius: 1// nonsense: 3.141592653589793
//! 例子2:打印带有星号框的值//!例子来源:https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#using-supertraits-to-require-one-traits-functionality-within-another-traituse std::fmt;#[derive(Debug)]struct Point {x: i32,y: i32,}impl fmt::Display for Point {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {write!(f, "({}, {})", self.x, self.y)}}trait OutlinePrint: fmt::Display {fn outline_print(&self) {let output = self.to_string();let len = output.len();println!("{}", "*".repeat(len + 4));println!("*{}*", " ".repeat(len + 2));println!("* {} *", output);println!("*{}*", " ".repeat(len + 2));println!("{}", "*".repeat(len + 4));}}impl OutlinePrint for Point {}fn main() { Point { x: 1, y: 3 }.outline_print(); }// 打印结果:// **********// * *// * (1, 3) *// * *// **********
newtype 模式:“绕开”孤儿原则
孤儿原则:只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait;或者说 不能为外部类型实现外部 trait。
newtype pattern:利用元组结构体,把外部类型放入这个元组结构体,从而获得一个本地创建的新类型。如此便符合孤儿原则,间接 达到给外部类型实现外部 trait 的目的。使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。
例子:在 Vec<T> 上实现自定义的 Display trait —— 孤儿规则阻止我们直接这么做,因为 Display trait 和 Vec<T> 都定义于我们的 crate 之外。
use std::fmt;#[derive(Debug)]struct Wrapper(Vec<String>); /* newtype pattern */impl fmt::Display for Wrapper {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {write!(f, "[{}]", self.0.join(", ")) // 注意字符串少了双引号}}fn main() {let w = Wrapper(vec![String::from("hello"), String::from("world")]);println!("w = {}", w);}// 打印结果:// w = [hello, world]
一些补充:
- 使用元组结构体的原因是它比较简洁,相比之下,完整的结构体需要字段来得到内部的类型,元组结构体只需要通过
.0便可直接获得第一个元素。 如果我们不希望
Wrapper的内部类型拥有的所有方法,那么可以自己实现我们想要的方法,不仅局限于给Wrapper实现内外部的 trait。// 比如构造我们想要的实例初始化方式,无需给 Wrapper 实现任何 traitimpl Wrapper {fn new() -> Self { Self(vec!["start".to_string()]) }}
Wrapper是一个新的类型,它内部虽然只有一个值,但是我们不能直接使用Vec<String>的方法,必须间接地通过Wrapper实例名.0来调用Vec的方法,从而完全像Vec<T>那样对待Wrapper。- 如果希望
Wrapper直接 拥有其内部类型的每一个方法,那么可以为Wrapper实现Dereftrait,比如:
还有其他场景很适合 newtype:”静态的确保某值(以及类型)不被混淆”、”轻量级封装”。use std::ops::Deref;#[derive(Debug)]struct Wrapper(Box<Vec<String>>);impl Deref for Wrapper {type Target = Vec<String>;fn deref(&self) -> &Self::Target { &self.0 }// 等价于以下有效 fully qualified syntax 写法:// fn deref(&self) -> &<Wrapper as Deref>::Target { &self.0 }}fn main() {let v = vec![String::from("hello"), String::from("world")];let w = Wrapper(Box::new(v));// indirectly call Vec's method (alse use automatic referencing):// let (left, right) = w.0.split_at(1);// automatic referencing + deref coercion:// let (left, right) = (&w).deref().split_at(1);let (left, right) = w.split_at(1);println!("left = {:?}, right = {:?}", left, right);}
