
18 高级trait
关联类型
trait设计的时候,就需要保证trait中的方法满足trait所要实现的功能。这个时候为了防止trait实现者胡乱实现,就可以用关联类型来限制trait的胡乱实现。要求实现者满足关联类型的限制要求。
在trait 内部,通过type 关键字 接 关联类型的占位符命名即可。
Self 是指“实现了当前这个 trait 的那个具体类型”。它可以是结构体、枚举、联合体、基本类型(如 i32),甚至是引用或元组。
在具体的实现中,需要指明关联类型的具体类型。
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
}
}
[!faq] 为什么不像泛型那样对每个不同关联类型的trait通过<具体类型>来区别?
因为每个类型只能实现1次trait,所以对于这个类型来说,只有1种trait,所以根本不需要区分不同的trait实现,只有我自己的一种实现。
泛型+trait
泛型在rust中是单态化的,也就是编译器会将泛型最终转换成具体类型。所以泛型帮助我们使用类型参数生成不同类型参数相似的类型实现。
诸如此类还有很多,不如直接 厘米运算<类型参数> trait,我们想要哪个trait填参数。
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
默认类型参数
有时候,某个类型参数使用的特别多,我们将其设置为默认类型参数,这样实现者完全可以忽略泛型。
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
默认参数类型主要用于如下两个方面:
- 扩展类型而不破坏现有代码。
- 在大部分用户都不需要的特定情况进行自定义。
运算符重载
Rust 并不允许创建自定义运算符或重载任意运算符,但可以通过实现std::ops 中列出的运算符相关 trait 来重载它们。(因为本质上运算符就是通过寻找类型的运算符trait来实现功能的)
- 运算符类型,比如
+运算符就是 std::ops::Add; - 参数类型,比如
Self + Self是默认情况,Self + 另外的类型就std::ops::Add<另外的类型>;
各trait中的同名方法
- 可以通过
trait::method_name(传入变量)来调用方法,trait能通过传入self的类型来判断具体要调用trait为哪个结构体的实现。(但是如果不是方法,就没有办法判断了) - 使用完全限定语法,
<Dog as Animal>::baby_name()这里从语法上非常像将结构体转型成了trait object类型,但是这里发生的是静态的,不会转换成trait object这样会有性能消耗。
我们在尖括号中向 Rust 提供了类型注解,这表明我们希望在此次函数调用中将 Dog 类型视为 Animal,从而调用在 Dog 上实现的 Animal trait 中的 baby_name 方法。
通常,完全限定语法定义为如下:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
超trait
语法就是在trait定义添加trait约束。
use std::fmt;
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));
}
}
使用 newtype 模式在外部类型上实现外部 trait
如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 Deref trait并返回其内部类型是一种解决方案。
