关联类型 (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 等价于 Self
Self { 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 不再是 Self
type 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: Supertrait
impl<T> Subtrait for T where T: Supertrait { }
// Subtrait: Supertrait
trait Subtrait: Supertrait { }
// where Self: Supertrait
trait Subtrait where Self: Supertrait { } /* 这里的 Self 指 Subtrait */
对泛型和对 trait 施加 trait bound 从语法上看如出一辙。
两个例子:
//! 例子1:构造:半径、面积 trait 和单位圆结构体
//!例子来源:https://doc.rust-lang.org/nightly/reference/items/traits.html#supertraits
use 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-trait
use 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 实现任何 trait
impl Wrapper {
fn new() -> Self { Self(vec!["start".to_string()]) }
}
Wrapper
是一个新的类型,它内部虽然只有一个值,但是我们不能直接使用Vec<String>
的方法,必须间接地通过Wrapper实例名.0
来调用Vec
的方法,从而完全像Vec<T>
那样对待Wrapper
。- 如果希望
Wrapper
直接 拥有其内部类型的每一个方法,那么可以为Wrapper
实现Deref
trait,比如:
还有其他场景很适合 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);
}