详解 Rust 的 trait,通过接口让你的类型实现多态
楔子
本篇文章来聊一聊 trait,准确的说是复习一下 trait,并补充一些之前遗漏的内容。
我们说过 Rust 一切皆类型,由 trait 定义类型的行为逻辑。trait 非常重要,如果把所有权比作 Rust 的心脏,那么类型+trait就是 Rust 的大脑。那么问题来了,什么是 trait 呢?
什么是 trait
trait 就是 Rust 中的接口,它定义了类型使用这个接口的行为,Rust 的 trait 就类似 Go 的 interface。很多文章会把 trait 翻译成特征,但我觉得没啥必要,直接就说 trait 即可。因为 trait 说白了就是一个标记,只不过这个标记专门用在泛型参数的后面,用来限定泛型参数所能表示的类型范围。
光用文字描述的话有些抽象,我们举个例子:
// 获取 a、b 中较大的那个数
fn max(a: i32, b: i32) -> i32 {
if a > b {a} else {b}
}
Rust 要求所有变量都有类型,因为类型是对变量值空间的约束。这里给参数 a、b 指定为 i32 类型,就相当于给 a、b 的取值范围添加了约束,它们的值空间被限定在了 i32 范围内。对于简单的应用,这种限定是没有问题的,但对于抽象程度比较高的应用,这种限定就显得太过死板。
比如这里我们将函数的参数都限定为 i32,就不太合适,因为不光 i32 类型的整数可以比较大小,任何类型的整数都可以比较,甚至浮点数和字符串也可以。那应该怎么办呢?一种比较笨的办法是为每一种可比较大小的类型都单独定义一个函数。
fn max_i32(a: i32, b: i32) -> i32 {
if a > b {a} else {b}
}
fn max_i64(a: i64, b: i64) -> i64 {
if a > b {a} else {b}
}
fn max_u32(a: u32, b: u32) -> u32 {
if a > b {a} else {b}
}
// .........
// .........
显然这种做法就太笨了,不过你还别说,Go 在没有引入泛型之前就是这么做的。但 Rust 在设计之初就包含了泛型,所以这种情况完全可以使用泛型进行处理。
fn max<T>(a: T, b: T) -> T {
if a > b {a} else {b}
}
泛型 T 只是一个占位符,它可以代表任意类型,具体代表哪种,则取决于你在调用时给参数 a、b 传的值是什么类型。从表面上来看,问题似乎已经解决了,但如果你真的这么定义函数的话,是会报出编译错误的。
因为 T 是泛型,它可以代表任意类型,但不是所有类型的值都可以比较大小。因此可以考虑给泛型 T 施加一个约束(也可以是多个),让 T 不再代表任意类型,而是代表满足指定约束的任意类型。
use std::cmp::PartialOrd;
// 这里的 PartialOrd 便是 trait,它要求泛型必须是能够比较大小的
// 所以此时调用 max 函数时,Rust 会先检测传给参数 a、b 的值是否能比较大小
// 如果无法比较,那么直接就报错了
fn max<T: PartialOrd>(a: T, b: T) -> T {
if a > b {a} else {b}
}
梳理一下整个过程不难发现,如果给参数设置一个固定的类型,那么在一些抽象程度较高的场景中,能够表达的值空间就太小了。于是便引入了类型参数(泛型)机制,用一个泛型 T 即可代表不同的类型,这样之前为一些特定类型开发的函数,就可以在不改变逻辑的情况下,扩展到支持多种类型,从而变得更加灵活。
但因为泛型参数 T 可以是任意类型,在很多场景中,参数所能表达的值空间又太大了。所以光引入泛型参数还不够,还得配套地引入一个对泛型进行约束的机制,于是便有了 trait。
use std::fmt::Display;
struct Point<T> {
x: T,
y: T,
}
fn print<K: Display>(p: Point<K>) {
println!("{} {}", p.x, p.y)
}
fn main() {
let p1 = Point{x: 123, y: 234};
let p2 = Point{x: Some(123), y: Some(234)};
print(p1); // 123 234
// print(p2); 不合法
}
结构体 Point 带了一个泛型参数,它没有施加任何参数,所以 T 可以代表任意类型,但不是所有的 Point<T> 都可以传给 print 函数。print 的参数 p 是 Point<K> 类型,其中 K 也是泛型,但是在定义 K 的时候,我们给 K 施加了一个约束,就是它必须实现 Display 这个 trait。
所以只有当 Point 实例的泛型 T 代表的类型实现了 Display 才可以作为参数传给 print 函数,因此 print(p1) 是合法的,但 print(p2) 不合法,因为 Option<i32> 没有实现 Display。另外需要注意:函数里的泛型参数我们刻意起了一个别的名字(K),为了和结构体实例的泛型参数(T)进行区分,但其实这两个泛型之间没有任何关系,都叫 T 也是没问题的。
当然,关于打印的 trait 除了 Display 之外,还有 Debug。
use std::fmt::{Debug, Display};
// 实现了 Display trait 的类型,可以通过 {} 进行打印
fn print_display<T: Display>(x: T) {
println!("{}", x)
}
// 实现了 Debug trait 的类型,可以通过 {:?} 进行打印
fn print_debug<T: Debug>(x: T) {
println!("{:?}", x)
}
// 实现了 Display 的类型,也一定实现了 Debug
// 换言之,可以用 {} 打印的,一定也可以用 {:?} 打印,但反过来不行
fn main() {
// i32 实现了 Display 和 Debug,因此两个函数都可以调用
let x = 123;
print_display(x); // 123
print_debug(x); // 123
// Vec 只实现了 Debug,没有实现 Display
let x = vec![1, 2, 3];
print_debug(x); // [1, 2, 3]
// print_display(vec![1, 2, 3]); 报错,因为 Vec 没有实现 Display
}
所以这就是 trait,它往往跟泛型结合起来使用,比如 T: SomeTrait,用来对泛型 T 进行约束。它表示只有实现了 SomeTrait 的类型,才能被泛型 T 表示,也就是限定了 T 能表示的类型范围。
以上便是 trait 的作用,用一张图来描述它的诞生过程:
那么问题来了,Rust 的原生类型实现了哪些 trait 都是已经固定了的,但对于自定义的类型,怎么让它实现某些 trait 呢?
use std::fmt::{Debug, Formatter};
// 必须实现 Debug trait
fn print_debug<T: Debug>(x: T) {
println!("{:?}", x)
}
// 默认情况下,像结构体等自定义类型是没有实现 Debug 的
// 那我们怎么让 Girl 实现 Debug trait 呢?
struct Girl {
name: String,
age: u8,
}
// trait 类似 Go 的接口,内部可以定义一系列方法
// 在 Go 里面如果实现某个接口的所有方法,那么就代表实现了这个接口
// 而在 Rust 里面,你不仅要实现 trait 的所有方法,还要显式地指定实现的 trait
impl Debug for Girl {
// 语法:impl SomeTrait for SomeType,表示为某个类型实现指定 trait
// 在 Rust 里面要显式地指定实现的 trait,然后实现它内部定义的所有方法
// Debug 里面只定义了一个 fmt 方法,我们实现它即可
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let res = format!("姓名: {}, 年龄: {}", self.name, self.age);
f.write_str(&res)
}
}
fn main() {
let girl = Girl{name: "古明地觉".to_string(), age: 17};
println!("{:?}", girl); // 姓名: 古明地觉, 年龄: 17
}
在通过 {:?} 打印的时候,会自动调用实现的 fmt 方法。当然啦,让结构体实现某个 trait 还有其它,也就是通过 derive 派生。
use std::fmt::Debug;
fn print_debug<T: Debug>(x: T) {
println!("{:?}", x)
}
// 让结构体实现 Debug
#[derive(Debug)]
struct Girl {
name: String,
age: u8,
}
fn main() {
let girl = Girl{name: "古明地觉".to_string(), age: 17};
println!("{:?}", girl); // Girl { name: "古明地觉", age: 17 }
}
但此时的打印结果是 Rust 提前内置好的,我们就无法自定义了。
如果你熟悉 Python 的话,应该知道 Python 一切皆对象,类型对象定义了哪些方法,决定了实例对象具有哪些行为。而 Rust 的 trait 就类似 Python 类型对象里的一些魔法函数,比如 Drop trait 对应 __del__,对象被释放时会自动调用 Drop 里的 drop 方法,这里的 Debug trait 对应 __str__。当然啦,Python 的类如果实现了 __add__,那么实例对象是可以相加的,那么 Rust 可不可以通过 trait 实现呢?显然是可以的。
use std::ops::{Add, Div, Mul, Sub};
#[derive(Copy, Clone)] // 表示结构体是可 Copy 的,在栈上分配
struct Point {
x: i32,
y: i32
}
impl Add for Point{
type Output = (i32, i32);
// Add trait 里面要求必须给返回值类型起一个别名叫 Output
// 这里的返回值类型 Self::Output 写成 (i32, i32) 也可以,但上面的类型别名逻辑不能省略
fn add(self, rhs: Self) -> Self::Output {
(self.x + rhs.x, self.y + rhs.y)
}
}
impl Sub for Point{
type Output = (i32, i32);
fn sub(self, rhs: Self) -> Self::Output {
(self.x - rhs.x, self.y - rhs.y)
}
}
impl Mul for Point{
type Output = (i32, i32);
fn mul(self, rhs: Self) -> Self::Output {
(self.x * rhs.x, self.y * rhs.y)
}
}
impl Div for Point{
type Output = (i32, i32);
fn div(self, rhs: Self) -> Self::Output {
(self.x / rhs.x, self.y / rhs.y)
}
}
fn main() {
let p1 = Point{x: 10, y: 20};
let p2 = Point{x: 1, y: 2};
println!("{:?}", p1 + p2); // (11, 22)
println!("{:?}", p1 - p2); // (9, 18)
println!("{:?}", p1 * p2); // (10, 40)
println!("{:?}", p1 / p2); // (10, 10)
}
以上我们就重载了 + - * / 四个操作符,运算时会自动执行相应的方法,比如:add、sub、mul、div 等等。当然其它操作符也可以重载,有兴趣可以自己试一下。
然后我们上面在实现 trait 的时候,频繁看到两个特殊的关键字:Self 和 self,它们有什么区别呢?
Self 代表当前的类型,因为我们是为 Point 类型实现方法,那么实现过程中使用到的 Self 就指代 Point
self 是方法的第一个参数,实际上它是 self: Self 的简写,所以 &self 是 self: &Self, 而 &mut self 是 self: &mut Self
举个例子:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
impl Point {
fn new1() -> Point {
Point { x: 3, y: 4 }
}
// 我们是为 Point 实现方法,那么 Self 指的就是 Point
fn new2() -> Point {
Self { x: 3, y: 4 }
}
fn new3() -> Self {
Point { x: 3, y: 4 }
}
fn new4() -> Self {
Self { x: 3, y: 4 }
}
}
fn main() {
let p1 = Point::new1();
let p2 = Point::new2();
let p3 = Point::new3();
let p4 = Point::new4();
println!("{:?}", p1); // Point { x: 3, y: 4 }
println!("{:?}", p2); // Point { x: 3, y: 4 }
println!("{:?}", p3); // Point { x: 3, y: 4 }
println!("{:?}", p4); // Point { x: 3, y: 4 }
}
所以后续在使用的时候,建议使用 Self,从而实现代码的统一。注意:不论是 impl SomeTrait for Point 还是 impl Point,方法里面的 Self 都是 Point,因为这两者都是在为 Point 实现方法。只不过第一种方式也叫为 Point 实现 trait,但实现 trait 依旧要实现它内部的方法。
然后就是一个 trait 在一个类型上只能被实现一次,这个很好理解, 比如给 Point 实现了两次 Add,那么做加法的时候调用哪一个呢?
自定义 trait
了解完 trait 之后,再来看看如何自定义 trait,目前使用的 trait 都是 Rust 内置的,我们也可以自己定义。
// 通过 trait 关键即可定义一个 trait,它类似 Go 里面的接口,里面只需要定义一系列方法即可
// 如果要实现这个 trait,那么必须实现里面所有的方法,少一个都不行
trait Animal {
// 只需要定义方法的参数和返回值签名即可,具体细节交给相应的类型实现
fn eat(&self);
fn drink(&self);
}
struct Dog {
name: String,
category: &'static str
}
struct Cat {
name: String,
category: &'static str
}
// 在 Go 里面只需要给 Dog 实现方法即可
// 只要实现了某个接口里的所有方法,那么就自动实现了该接口
// 但 Rust 则不同,它还要求你必须显式地指定要实现的 trait
impl Animal for Dog {
fn eat(&self) {
println!("{} 在吃东西,它是一只 {}", self.name, self.category);
}
fn drink(&self) {
println!("{} 在喝饮料,它是一只 {}", self.name, self.category);
}
}
impl Animal for Cat {
fn eat(&self) {
println!("{} 在吃东西,它是一只 {}", self.name, self.category);
}
fn drink(&self) {
println!("{} 在喝饮料,它是一只 {}", self.name, self.category);
}
}
// 一个 eat 函数,接收一个泛型 T,但 T 必须是实现了 Animal trait 的类型
// 如果实现了 Animal trait,那么它一定能够调用 eat 方法
// 要是没有 eat 方法,那么它就不叫实现 Animal trait,那么当前的 eat 函数也不可能被调用
fn eat<T: Animal>(animal: &T) {
animal.eat();
}
fn drink<T: Animal>(animal: &T) {
animal.drink();
}
fn main() {
let dog = Dog{name: "旺财".to_string(), category: "小狗"};
let cat = Cat{name: "翠花".to_string(), category: "小猫"};
eat(&dog); // 旺财 在吃东西,它是一只 小狗
eat(&cat); // 翠花 在吃东西,它是一只 小猫
drink(&dog); // 旺财 在喝饮料,它是一只 小狗
drink(&cat); // 翠花 在喝饮料,它是一只 小猫
}
通过以上方式我们就实现了多态,一个接口多种实现,eat 函数和 drink 函数均接收一个值的引用,其中值的类型是 T。至于 T 则是实现了 Animal trait 的任意类型,由于这里的 Dog 和 Cat 都实现了 Animal trait,所以它们的实例都可以作为参数传递给 eat 函数和 drink 函数。
在 eat 函数和 drink 里面,我们分别调用了对象的 eat 方法和 drink 方法,因为 Animal trait 里面定义了这两个方法。如果某个类型想实现该 trait,那么必须要实现里面的方法,否则它就没有实现该 trait。
因此 Rust 基于 trait 实现了动态的效果,比如 eat 函数,它不在乎对象是什么类型,只要里面有 eat 方法即可,drink 函数也是同理。所以通过 trait 进行制约,只要实现了 Animal trait 的对象(的引用)都可以作为参数调用该函数。如果是 Python 的话,函数定义也是相似的:
def eat(animal):
animal.eat()
def drink(animal):
animal.drink()
但是 Python 在编译时不会对参数 animal 做检测,它有没有 eat 和 drink 方法压根不知道,所以不管什么类型都可以调用这两个函数。在执行 animal.eat() 和 animal.drink() 的时候,如果发现对象没有指定方法时,再抛出 AttributeError,所以 Python 的这些检测都在运行时,因此它是动态语言。
如果是 Rust 则不允许这么做,比如下面代码就是不合法的:
fn eat<T>(animal: &T) {
animal.eat();
}
fn drink<T>(animal: &T) {
animal.drink();
}
这里没有对泛型进行制约,那么 Rust 就不知道 T 是否包含 eat 和 drink 这两个方法,因为 T 可以代表任意类型,所以这两个函数本身就不合法。因此要限制 T 的表达范围,让它只能表达那些实现了 eat 方法和 drink 方法的类型。
相信你现在对 trait 已经有了很深刻的了解了,但还没有结束。我们说 Rust 类似 Go 里面的接口,但 Go interface 里面只能存放方法的定义,不可以有实现,而 Rust 的 trait 里面除了定义还可以有默认实现。
// 定义了两个方法,但 eat 方法有默认的实现
trait Animal {
// 有声明,有实现(函数体)
fn eat(&self) {
println!("Animal 在吃东西")
}
// 只有声明,没有实现(不存在函数体)
fn drink(&self);
}
struct Dog {
name: String,
category: &'static str
}
struct Cat {
name: String,
category: &'static str
}
impl Animal for Dog {
fn eat(&self) {
println!("{} 在吃东西,它是一只 {}", self.name, self.category);
}
fn drink(&self) {
println!("{} 在喝饮料,它是一只 {}", self.name, self.category);
}
}
// 我们没有为 Cat 实现 eat 方法,但由于 eat 方法有默认实现,不实现也没关系
// 因此一个类型如果要实现某个 trait,那么必须实现该 trait 里面所有没有默认实现的方法
impl Animal for Cat {
fn drink(&self) {
println!("{} 在喝饮料,它是一只 {}", self.name, self.category);
}
}
fn eat<T: Animal>(animal: &T) {
animal.eat();
}
fn drink<T: Animal>(animal: &T) {
animal.drink();
}
fn main() {
let dog = Dog{name: "旺财".to_string(), category: "小狗"};
let cat = Cat{name: "翠花".to_string(), category: "小猫"};
eat(&dog); // 旺财 在吃东西,它是一只 小狗
// Cat 没有实现 eat 方法,此时调用的是 trait 的默认实现
eat(&cat); // Animal 在吃东西
drink(&dog); // 旺财 在喝饮料,它是一只 小狗
drink(&cat); // 翠花 在喝饮料,它是一只 小猫
}
比较简单,如果一个 trait 没有定义任何方法,那么就是空 trait。
trait Animal {}
struct Dog {
name: String,
}
// 尽管 Animal 没有定义任何方法,也必须要显式地指定实现某个 trait
// 需要注意:如果想为 Dog 定义方法,那么需要单独写一个 impl 块
// 比如为 Dog 实现 eat 方法,那么 eat 方法就不能写在当前的块里面
// 因为这是为 Dog 实现 Animal trait,它只能包含 Animal 里面已经定义的方法
impl Animal for Dog {
}
// 单独开启一个 impl 块,定义方法
impl Dog {
fn eat(&self) {}
}
fn main() {
}
当然啦,一个类型也可以实现多个 trait,比如我们希望给泛型参数施加多个约束。
trait People {
fn get_name(&self) -> &str;
fn get_gender(&self) -> &str;
}
trait Female {
fn get_name(&self) -> &str;
}
struct Girl {
name: String,
}
impl Female for Girl {
fn get_name(&self) -> &str {
&self.name
}
}
impl People for Girl {
fn get_name(&self) -> &str {
&self.name
}
fn get_gender(&self) -> &str {
"Female"
}
}
// 将两个 trait 加起来,表示泛型 T 要同时实现这两个 trait
fn print_info<T: People + Female>(obj: &T) {
// 获取 name,由于 People 和 Female 都实现了 get_name
// 所以 obj.get_name() 会出现歧义,因此需要通过类型去调用
let name = People::get_name(obj);
println!("name = {}", name);
let name = Female::get_name(obj);
println!("name = {}", name);
// 由于只有 Female 定义了 get_gender,所以此时不会出现歧义
let gender = obj.get_gender();
println!("gender = {}", gender);
}
fn main() {
let girl = Girl {name: "古明地觉".to_string()};
print_info(&girl);
/*
name = 古明地觉
name = 古明地觉
gender = Female
*/
}
不难理解,而且通过这段代码我们似乎还看到了 Python 的影子,结构体类型(或者 trait)可以看作是 Python 的类对象,而结构体实例可以看做 Python 的实例对象。方法的第一个参数是 self,如果是实例调用会自动传递(并根据参数是 self、&self 还是 &mut self 而自动获取引用或者解引用),如果是类调用则需要手动传递第一个参数(并且参数类型要匹配,而这个过程需要开发者负责)。
trait 里面都可以包含什么?
trait 负责定义方法,这些方法可以有默认的实现,也可以没有。然后类型在实现 trait 的时候,要实现 trait 里面所有的没有默认实现的方法,而对于那些有默认实现的方法,类型可以选择实现(会覆盖),也可以选择不实现(使用默认实现)。
trait People {
fn get_name(&self) -> &str; // 这里直接以分号结尾,表示函数签名
fn set_name(&mut self);
fn transfer_ownership(self);
fn new(name: String) -> Self;
}
struct Girl {
name: String,
}
impl People for Girl {
fn get_name(&self) -> &str {
&self.name
}
fn set_name(&mut self) {
self.name = self.name.to_uppercase();
}
fn transfer_ownership(self) {
}
fn new(name: String) -> Self {
Self { name }
}
}
trait 里面的方法也可以叫做关联函数,但 trait 里面除了方法之外,还可以有什么呢?
关联类型
记得在实现 Add、Sub 这些 trait 的时候,我们通过 type 起了个类型别名,没错,trait 里面还可以有关联类型。
trait Season {
// 这里不需要指定具体的类型,Months 究竟是谁的类型别名
// 由具体类型在实现时指定
type Months;
fn get_months(&self) -> Self::Months;
}
struct Summer {
months: [u8; 3],
}
impl Season for Summer {
// trait Season 里面存在一个 type Months
// 那么类型在实现时要负责指定 Months 是谁的类型别名
type Months = [u8; 3];
// 这里便可以通过 Self::Months 替换掉 [u8; 3]
// 当然返回值签名写成 [u8; 3] 也可以的,只不过 Months 就没用了
fn get_months(&self) -> Self::Months {
self.months
}
}
fn get_months<T: Season<Months = [u8; 3]>>(season: &T) -> [u8; 3]{
season.get_months()
}
fn main() {
let summer = Summer { months: [4, 5, 6] };
println!("{:?}", get_months(&summer)); // [4, 5, 6]
}
整个过程不难理解,无非就是指定了一个类型别名,由于它在类型里面,所以需要通过 Self:: 才能拿到。但有一个需要注意的点,就是在给泛型 T 施加约束的时候,我们不仅要指定 Season 这个 trait,还要指定它内部的关联类型,比如我们重新定义一下函数。
fn get_months<T: Season<Months = [u8; 4]>>(season: &T) -> [u8; 3]{
season.get_months()
}
此时就无法调用了,因为函数要求实现了 Season trait 的类型里面的 Months 是 [u8; 4] 的类型别名,我们当前是 [u8; 3]。
迭代器也是类似的,所有的迭代器都实现了 Iterator 这个 trait。
Iterator 也包含一个类型别名 Item,它表示迭代出的元素的类型,由迭代器在实现该 trait 的时候指定。
use std::ops::RangeInclusive;
// T 必须实现 Iterator trait,并且里面的 item 是 i32 类型
fn get_sum<T: Iterator<Item = i32>>(mut iter: T) -> i32{
let mut sum = 0;
while let Some(v) = iter.next() {
sum += v;
}
sum
}
fn main() {
let vec: Vec<i32> = vec![1, 2, 3];
// vec 是 Vec<i32>,所以调用 iter 方法创建迭代器的时候,内部的 Item 也是 i32
println!("{}", get_sum(vec.iter().cloned())); // 6
let iter: RangeInclusive<i32> = 1..=100;
println!("{}", get_sum(iter)); // 5050
let iter: RangeInclusive<u32> = 1..=100;
// 但这里就报错了,因为 get_sum 的参数是一个实现了 Iterator<Item = i32> 的类型
// 而这里的 iter 实现的是 Iterator<Item = u32>,所以不可以调用
println!("{}", get_sum(iter));
/*
| println!("{}", get_sum(iter)); // 5050
| ------- ^^^^ expected `i32`, found `u32`
| |
| required by a bound introduced by this call
*/
}
以上就是关联类型在约束中的具体化。
当然我们也可以直接使用关联类型,还是看 Iterator 这个 trait。
这里直接使用了 Item,如果 Item 是 i32 类型,那么这里的返回值签名就完全可以替换成 i32。但关联类型不仅可以用在返回值签名中,参数签名也是可以的。
trait People {
type Length;
}
struct Girl;
struct Boy;
impl People for Girl {
type Length = u8;
}
impl People for Boy {
type Length = u16;
}
// 重点来了,泛型 T 代表实现了 People 的任意类型,而参数却不是 T,而是 T::Length
// 这是没问题,只要泛型都出现在函数参数中即可,然后这里使用的是 T::Length
// 因为 T 要求类型实现了 People,而如果要实现 People,那么内部一定给某个具化类型起了个别名叫 Length
// 因此 T::Length 一定是存在的,否则 Rust 编译器不认为实现了 People,也就不会允许调用该函数
fn get_info<T: People>(v: T::Length) {
}
fn main() {
// get_info(153); 报错
}
但是问题来了,我怎么调用 get_info 呢?如果参数 v 的类型是 T,不是 T::Length,那么很简单,直接调用就行了,只要实现了 People trait。但现在参数是 T::Length,而 T 可以表示很多类型,那么到底是哪一个类型的 Length 呢?所以 Rust 此时要求我们给予更多的信息:
fn main() {
get_info::<Girl>(123u8);
get_info::<Boy>(123u16);
}
这种语法也可以用在参数没有使用关联类型的函数当中:
use std::fmt::Debug;
// T 代表实现了 Debug 的任意类型,而 v 的类型是 T,而不是某个关联类型
fn get_info<T: Debug>(v: T) {
println!("{:?}", v);
}
fn main() {
// Rust 会执行单态化,泛型 T 会被替换为具体的类型,不影响运行时效率
get_info(('a', 'b', 'c')); // ('a', 'b', 'c')
get_info(Some("你好")); // Some("你好")
// 而 T 会被替换成什么类型,则取决于我们传递的参数是什么类型(只要实现了 Debug)
// 总之在调用函数的那一刻,T 就已经确定为具体的类型(由编译器基于参数推断)
// 比如这里的 123,编译器会推断为 i32,那么就等价于调用了这么一个函数:fn get_info_i32(v: i32)
get_info(123); // 123
// 这里的 123 是 u8 类型,那么等价于调用的是:fn get_info_u8(v: u8)
get_info(123u8); // 123
// 在每个函数调用点上,编译器会根据传递给函数的参数的具体类型生成一个对应的函数实例
// 也就是说,以上的单态化,编译器要先推断出参数的类型,然后才能确定泛型 T,生成对应的函数实例
// 但我们也可以显式指定,比如 get_info::<i64>,由于明确指定了泛型为 i64
// 编译器就会根据 i64 进行单态化,生成一个特定于 i64 类型的函数实例,类似于 fn get_info_i64(v: i64)
// 但此时的单态化不再依据参数的类型,而是我们显式指定的类型,由于指定了泛型为 i64,那么参数一定也要是 i64
get_info::<i64>(123i64); // 123
}
以上是泛型的一些基础知识,我们算是回顾了一下。单态化生成的函数实例所作用的具体类型,可以由开发者通过 ::<类型> 手动指定,也可以让编译器基于传递的值进行推断。因为参数的类型是 T,那么在传参之后是可以基于参数值的类型确定 T 的。但如果是使用了关联类型,即参数的类型不是 T,而是 T 内部的关联类型,那么调用时就必须手动指定了。
因为关联类型与具体的实现(我们定义的结构体)是相关的,而不同实现的关联类型可以一致,也可以不一致,比如 T 可以代表 Boy 和 Girl,它们里面的 Length 可以相同也可以不同。所以 Rust 无法基于关联类型推断出相应的具体实现,换言之就是基于上面的 T::Length 无法推断出 T 是 Boy 还是 Girl,因此必须由我们告诉 Rust 怎么执行单态化。
除了给泛型施加约束,还可以给关联类型施加约束,举个例子。
use std::fmt::Display;
trait People {
// 这里给关联类型 Length 施加了一个 Display 约束
// 那么结构体在实现 People trait 时,必须将实现了 Display trait 的类型赋给 Length
type Length: Display;
}
struct Girl;
impl People for Girl {
// type Length = Option<i32>; 不合法,因为 Option<i32> 没有实现 Display
type Length = i32;
}
fn main() {
}
在函数里面,也可以对关联类型施加更强的约束,比如:
use std::fmt::{Debug, Display};
trait People {
type Length: Display;
}
struct Girl;
impl People for Girl {
type Length = String;
}
fn get_info1<T>(v: T::Length)
where
T: People,
T::Length: Copy
{}
fn get_info2<T>(v: T::Length)
where
T: People,
T::Length: Clone
{}
fn main() {
// get_info1::<Girl>(String::from("你好")); 调用不合法
get_info2::<Girl>(String::from("你好"));
}
代码稍微有点复杂,我们一点点来分析。首先 Girl 如果想实现 People trait,那么内部必须要给某个实现了 Display 的类型起一个别名叫 Length,因为 People 里面定义了 type Length,并且还给关联类型 Length 添加了一个 Display 约束,这个应该没啥问题。
然后我们定义了 Girl 结构体,并实现了 People trait,因为 String 实现了 Display trait,所以 get_info1::<Girl> 和 get_info2::<Girl> 都是合法的。而且这种方式表示由开发者告诉编译器怎么执行单态化,生成针对哪一种特定类型的函数,当然啦,这里单态化的前提是函数的泛型参数 T 可以代表 Girl,否则就不合法了。
但是在函数中,我们给关联类型又施加了一个约束,如果你想调用函数,那么关联类型还要满足 Copy、Clone。对于 People trait 来说,不管 Girl 的关联类型有没有实现 Copy、Clone,都是不影响的。因为 People 里面只给 Length 施加了 Display 约束,所以只要关联类型 Length 实现了 Display,那么就算实现了 People trait。
而 get_info1 和 get_info2 两个函数,则要求泛型 T 单态化后的类型不仅实现了 People,还要求内部的关联类型实现了 Copy(get_info1)和 Clone(get_info2)。而 Girl 对的关联类型是 String,它没有实现 Copy,所以它无法调用 get_info1 函数。
你甚至都不需要调用,写上 get_info1::<Girl> 就会报错,因为 Rust 是先执行单态化,然后才调用的。由于我们指定了 ::<Girl>,那么 Rust 编译器就会生成特定于 Girl 类型的函数实例。但问题是,编译器发现 Girl 虽然实现了 People trait,但它内部的关联类型没有实现 Copy。因为我们给 T 施加了两个约束,不仅要满足 T: People,还要满足 T:: Length: Copy(对关联类型的约束也可以看作是对 T 的约束),而 Girl 只满足了第一个。因此 T 无法代表 Girl,那么自然也就是无法生成基于 Girl 的单态化实现。
但是 get_info2 则没有问题,因为 Girl 的关联类型实现了 Clone,因此可以放心调用。当然啦,如果把 Girl 里面的关联类型从 String 换成 i32,那么两个函数就都可以调用了,因为 i32 同时实现了 Copy 和 Clone(如果实现了 Copy,那么必须实现 Clone)。
其实整个过程还是不复杂的,只是为了描述的详细一些,而用了一定的笔墨。
关联常量
说完了方法(关联函数)、关联类型,再来看看关联常量。
同样的,trait 里也可以携带一些常量信息,表示这个 trait 的一些内在信息(挂载在 trait 上的信息)。和关联类型不同的是,关联常量可以在 trait 定义的时候指定,也可以在给具体类型实现的时候指定。
trait SomeTrait {
const LEN: u32 = 123;
}
struct A;
struct B;
impl SomeTrait for A {
const LEN: u32 = 666;
}
impl SomeTrait for B {
}
fn main() {
println!("{}", A::LEN); // 666
println!("{}", B::LEN); // 123
// 注意:trait 是一个接口,相当于提供了一个抽象,但 trait 本身是无法使用的
// 像 SomeTrait::LEN 是不合法的,trait 里面的一切条目,都应该由实现它的具体类型去操作
}
关联常量和方法一样,如果 trait 已经实现,那么具体类型就可以不实现了。如果 trait 没有实现,也就是只有定义、没有赋值(比如 const LEN: u32),那么具体类型就必须实现。
trait SomeTrait {
// 要实现此 trait,内部必须定义常量 LEN
const LEN: u32;
}
struct A;
struct B;
impl SomeTrait for A {
const LEN: u32 = 666;
}
impl SomeTrait for B {
const LEN: u32 = 777;
}
fn main() {
println!("{}", A::LEN); // 666
println!("{}", B::LEN); // 777
}
还有一个地方需要注意:关联类型和关联常量只能通过结构体类型本身去访问,它的实例是无法访问的。
trait SomeTrait {
const LEN: u32;
type SomeType;
}
struct A;
impl SomeTrait for A {
const LEN: u32 = 666;
type SomeType = Option<i32>;
}
fn main() {
println!("{}", A::LEN); // 666
let x: <A as SomeTrait>::SomeType = Some(123);
println!("{:?}", x); // Some(123)
}
所以关联类型和关联常量,只能通过结构体类型本身通过符号 :: 去访问。然后我们看到在访问关联类型的时候,对 A 强制转化了一下,这是为什么呢?很简单,因为 A 可以实现不同的 trait,每个 trait 都可以有自己的 SomeType,那么在获取的时候要获取哪一个呢?所以必须要先转一下,并且逻辑要放在 <> 里面,注意是尖括号、不是小括号,这是在获取关联类型和关联常量时的语法。
那么问题来了,为啥获取 LEN 的时候不需要转呢?因为当前 A 只实现了一个 trait,因此不会出现歧义。如果 A 再实现一个 trait,里面也定义了 LEN 常量,那么在获取的时候就需要显式转换了。但关联类型不同,即使不会出现歧义,也要显式指定,告诉编译器你要获取为实现哪一个 trait 而定义的关联类型。
trait 作为一种协议
我们已经看到,trait 里有可选的关联函数、关联类型、关联常量这三项内容。一旦 trait 定义好,它就相当于一条法律或协议,在实现它的各个类型之间,在团队协作中不同的开发者之间,都必须按照它定义的规范实施。这是强制性的,而且这种强制性是由 Rust 编译器来执行的。也就是说,如果你不想按这套协议来实施,那么你注定无法编译通过。
这个对于团队开发来说非常重要。它相当于在团队中协调的接口协议,强制不同成员之间达成一致。从这个意义上来讲,Rust 非常适合团队开发。
下面再补充一些关于 trait 的知识。
where 子句
当泛型后面有多个 trait 约束、或者有多个泛型的时候,会显得头重脚轻,比较难看。所以 Rust 提供了 Where 语法来解决这个问题,Where 关键字可用来把约束关系统一放在后面表示。
fn bar<T: Trait1 + Trait2 + Trait3, E: Trait2 + Trait3 + Trait4, W: Trait5 + Trait6>(a: T, b: E, c: W) -> i32 {
}
这种代码虽然是合法的,但看起来有点太丑了,因此可以写成下面这种形式。
fn bar<T, E, W>(a: T, b: E, c: W) -> i32
where
T: Trait1 + Trait2 + Trait3,
E: Trait2 + Trait3 + Trait4,
W: Trait5 + Trait6
{
}
这样看起来是不是就清爽多了呢?不会让过多的 trait 约束干扰函数签名的视觉完整性。
约束依赖
Rust 还提供了一种语法表示约束间的依赖。
trait Trait1: Trait2 {}
初看起来,这跟 C++ 等语言的类的继承有点像,但实际不是,并且差异很大。这个语法的意思是,如果某种类型要实现 Trait1,那么它也要同时实现 Trait2,举个例子:
trait Circle {
fn radius(&self) -> f64; // 获取半径
}
// 如果一个结构体类型要实现 Shape,那么它也要实现 Circle
trait Shape : Circle {
fn area(&self) -> f64; // 获取表面积
}
struct Ball {
r: f64
}
impl Circle for Ball {
fn radius(&self) -> f64 {
self.r
}
}
// 如果 Ball 没有实现 Circle,那么 impl Shape for Ball 就是非法的
// 因为 trait Shape : Circle 决定了,任何实现 Shape 的类型,也必须实现 Circle
impl Shape for Ball {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius() * self.radius() * 4.
}
}
fn main() {
let ball = Ball{r: 0.2};
println!("半径:{}米", ball.radius()); // 半径:0.2米
println!("表面积:{:.3}平方米", ball.area()); // 面积:0.503平方米
}
对于当前这个例子来说,即使不实现 trait 也无所谓,直接把 radius 和 area 当做两个普通的方法即可。但如果有很多的结构体类型,它们具备相同的方法,而我们需要一个统一的接口去调用,那么 trait 就很有用了。
然后一个 trait 也可以依赖多个 trait,比如:
trait Trait1: Trait2 + Trait3 {}
// 那么 T: Trait1 等价于 T: Trait1 + Trait2 + Trait3
约束之间是完全平等的,理解这一点非常重要,通过刚刚的这些例子可以看到约束依赖是消除约束条件冗余的一种方式。在约束依赖中,冒号后面的叫 supertrait,冒号前面的叫 subtrait。可以理解为 subtrait 在 supertrait 的约束之上,又多了一套新的约束,这些不同约束的地位是平等的。
约束中同名方法的访问
有的时候多个约束上会定义同名方法,像下面这样:
trait Circle {
fn play(&self) { // 定义了 play() 方法
println!("1");
}
}
trait Shape : Circle {
fn play(&self) { // 也定义了 play() 方法
println!("2");
}
}
struct A {}
impl Shape for A {}
impl Circle for A {}
fn main() {
let a = A{};
// 如果是实现普通的方法,那么方法名是不能重复的
// 但如果是实现 trait,而不同的 trait 可以有同名方法,因此可以有多个同名方法
// 可调用的时候,会调用哪一个呢?所以我们不能直接使用 a.play() 的方式进行调用,会出现歧义
// 同样的,a.play() 底层会转成 A::play(&a),但同样会出现歧义,因此可以考虑将 A 转换一下
<A as Circle>::play(&a); // 1
<A as Shape>::play(&a); // 2
// 这就没问题了,这是当出现歧义时,获取指定方法所使用的特殊语法
// 然后我们说 trait 不能获取内部的条目,应该由具体的类型去获取
// 因为 trait 只是一个接口,不同的类型都可以实现它,但下面是可以的
// 因为基于参数 &a,Rust 知道具体的类型,所以它等价于 <A as ...>::play(&a)
// 但如果不是获取方法,而是获取关联类型或者关联常量(或者方法的第一个参数不是 self),那么就不能使用下面这种做法了
Circle::play(&a); // 1
Shape::play(&a); // 2
}
除了为 trait 实现方法,自己本身也可以实现方法,如果再定义一个普通的方法 play 呢?
trait Circle {
fn play(&self) { // 定义了 play() 方法
println!("1");
}
}
trait Shape : Circle {
fn play(&self) { // 也定义了 play() 方法
println!("2");
}
}
struct A {}
impl Shape for A {}
impl Circle for A {}
impl A {
fn play(&self) { // 自己又实现了 play 方法
println!("3")
}
}
fn main() {
let a = A{};
<A as Circle>::play(&a); // 1
<A as Shape>::play(&a); // 2
Circle::play(&a); // 1
Shape::play(&a); // 2
a.play(); // 3
A::play(&a); // 3
}
因此结论很清晰了,如果自己实现了 play 方法,那么 a.play() 和 A::play(&a) 会直接调用自己的(前者是后者的语法糖)。如果自身没有实现,那么会去找为 trait 实现的,要是找到了并且唯一(不会出现歧义),那么直接调用。但如果发现了多个同名方法,此时就出现歧义了,并且也说明该类型实现了多个 trait,这些 trait 之间存在同名方法。
那么这时候为了不出现歧义,要显式地告诉 Rust,去调用为哪一个 trait 实现的方法。比如类型 A 为 Shape 和 Circle 都实现了 play 方法,直接调用的话会出现歧义,于是可以通过 <A as Circle>::play(&a); 告诉 Rust 去调用为 Circle 实现的 play 方法。或者也可以直接通过 Circle::play(&a); 去调用,因为通过参数可以判断出具体类型。
<A as Circle>::play(&a); 叫做完全限定语法,是类型上某一个方法的完整路径表达。如果 impl 和 impl trait 时有同名方法,用这个语法就可以明确区分出来。
然后还需要注意:在转化的时候,要使用尖括号,不是小括号。如果使用小括号,那么起到的只是一个限定作用。
fn main() {
let x = 6;
let y = 8;
let square = (x * x + y * y);
// 计算 (x, y) 距离原点的距离
// 但由于整数没有计算平方根的函数,所以需要先转成浮点数
let root = (square as f64).sqrt();
println!("{}", root); // 10
}
比如这里的 square as f64 的外面就是小括号,但它只是起到一个限定优先级的作用,否则 f64.sqrt() 就是一个整体。而为了不出现歧义,将具体类型转成 trait 时,则必须使用尖括号,这是专门的语法,因此不要混淆了。
当然也别忘了属性操作符,实例通过 . 来获取,类型通过 :: 来获取。就拿上面浮点数的 sqrt 方法来说:
fn main() {
let x = 100f64;
// x 是浮点数,是实例,直接通过 . 来调用,并且自动传递第一个参数
// 不管第一个参数是 &self、&mut self、还是 self,都会自动处理
let root1 = x.sqrt();
// 但 `实例.方法()` 只是语法糖,背后都是通过具体的类型去调用的,Python 也是如此
// x 的类型是 f64,那么 x.sqrt() 其实会被转成如下
let root2 = f64::sqrt(x);
// f64 是类型,要通过 :: 来调用,但此时需要我们手动处理第一个参数
// 根据 &self、&mut self、还是 self,来决定是传 &x、&mut x 还是 x
println!("{} {}", root1, root2); // 10 10
}
比较简单,都是说过的内容,但由于内容有关联,所以再复习一遍。总之一句话:实例.方法() 就是 类型::方法(实例、&实例、&mut 实例) 的语法糖,一般来说我们通过实例去调用即可,但当产生歧义时,就只能通过类型去调用了。
对啦,说到这儿,我又想起了泛型。泛型我们上面已经详细介绍过了,这里再简单说一下。Rust 的泛型代码是要执行单态化的,当你调用时,会生成针对特定类型的普通函数,所以泛型不影响效率。要执行单态化,就要确定类型,而类型可以基于传递的值来推断,也可以显式指定。
use std::fmt::{Debug, Display};
fn print_obj<T: Display, W: Debug>(a: T, b: W) {
println!("{} {:?}", a, b);
}
fn main() {
// 执行单态化,会生成特定于 u8 和 Option<i32> 的函数
// 等价于调用 print_obj_u8_String(a: u8, b: String)
// 当然这里的函数名只是打个比方,总之 Rust 会基于 u8 和 String 生成一个普通类型的函数
// 在执行时,泛型不会影响效率
print_obj(123u8, "hello".to_string()); // 123 "hello"
// 上面的单态化是先获取参数值的类型,然后执行的单态化
// 当然我们也可以显式指定泛型代表的类型,告诉 Rust 怎么执行单态化
print_obj::<String, &str>("hello".to_string(), "world"); // hello "world"
}
如果要显式指定,那么所有的泛型参数都要指定,当然啦,普通参数是不需要的。所以 ::<T, W, ...> 也是一种特殊的语法,不过它是针对泛型参数的。再比如动态数组 Vec:
use std::fmt::{Debug, Display};
fn print_obj<T: Display, W: Debug>(a: T, b: W) {
println!("{} {:?}", a, b);
}
fn main() {
// 动态数组的类型是 Vec<T>,T 代表什么类型取决于数组的元素
let vec1 = vec![1, 2, 3];
// 如果只是创建一个数组,没有指定元素,那么需要显式指定类型,否则 Rust 无法推断
let vec2: Vec<String> = Vec::new();
// 同样的,我们也可以使用专门针对泛型的语法,vec2 和 vec3 是等价的
let vec3 = Vec::<String>::new();
// 再比如迭代器的 collect 方法,它也接收一个泛型参数,叫 B
// 负责将迭代器的元素收集起来,然后创建指定的结构,并将元素添加进去
// 至于创建的结构是什么,则由泛型 B 指定,但泛型 B 没有出现在参数中,而是体现在返回值中
// 我们可以通过 let new_vec: Vec<i32> 来告诉 Rust collect 的返回值是 Vec<i32>
// 也可以通过 collect::<Vec<i32>>() 手动指定,明确告知编译器 collect 执行时的泛型 B 是 Vec<i32>
// 只不过 Vec 里面的元素类型 i32 编译器可以原始输入推断出来,因此我们写成 Vec<_> 就好
let new_vec = vec1.iter().cloned().map(|c| c * 10).collect::<Vec<_>>();
println!("{:?}", new_vec); // [10, 20, 30]
}
相信在初学 Rust 的时候看到这些复杂的语法一定会感到头疼,所以这里我们单独拿出来,把这些语法的含义总结一下。
用 trait 实现能力配置
Rust 是怎么检查一个实例有没有某个方法的呢?
检查有没有直接在这个类型上实现这个方法
检查有没有在这个类型上实现某个 trait,而 trait 中有这个方法
一个类型可能实现了多个 trait,不同的 trait 中各有一套方法,这些不同的方法中可能还会出现同名方法。Rust 在这里采用了一种惰性的机制,由开发者指定在当前的 mod 或 scope 中使用哪套或哪几套的能力。因此,对应地需要开发者手动地将要用到的 trait 引入当前 scope。
比如下面这个例子,我们定义两个隔离的模块,并在 module_b 里引入 module_a 中定义的类型 A。
mod module_a {
pub trait Shape {
fn play(&self) {
println!("1");
}
}
pub struct A;
impl Shape for A {}
}
mod module_b {
use super::module_a::A; // 这里只引入了另一个模块中的类型
fn bar() {
let a = A;
a.play();
}
}
fn main() {
}
如果你编译这段代码,会报错,提示 A 没有 play 方法。
但是问题来了,我们明明让 A 实现了 Shape trait,而 Shape 里面是有 play 方法的呀。很简单,因为我们只引入了类型,而没有引入 trait。
mod module_a {
pub trait Shape {
fn play(&self) {
println!("1");
}
}
pub struct A;
impl Shape for A {}
}
mod module_b {
use super::module_a::A;
use crate::module_a::Shape; // 将shape 也引入进来
pub fn bar() {
let a = A;
a.play();
}
}
fn main() {
module_b::bar(); // 1
}
也就是说,在当前 mod 不引入对应的 trait,你就得不到相应的能力。因此 Rust 的 trait 需要引入当前 scope 才能使用的方式,可以看作是能力配置(Capability Configuration)机制。
约束可按需配置
有了 trait 这种能力配置机制,我们可以在需要的地方按需加载能力,需要什么能力就引入什么能力(提供对应的约束)。不需要一次性限制过死,比如下面的示例演示了几种约束组合的可能性。
trait Trait1 {}
trait Trait2 {}
trait Trait3 {}
struct A;
struct B;
struct C;
impl Trait1 for A {}
impl Trait2 for A {}
impl Trait3 for A {} // 类型 A 实现了 Trait1, Trait2, Trait3
impl Trait2 for B {}
impl Trait3 for B {} // 类型 B 实现了 Trait2, Trait3
impl Trait3 for C {} // 类型 C 实现了 Trait3
// 7 个版本的 bar() 函数
fn bar1<T: Trait1 + Trait2 + Trait3>(t: T) {}
fn bar2<T: Trait1 + Trait2>(t: T) {}
fn bar3<T: Trait1 + Trait3>(t: T) {}
fn bar4<T: Trait2 + Trait3>(t: T) {}
fn bar5<T: Trait1>(t: T) {}
fn bar6<T: Trait2>(t: T) {}
fn bar7<T: Trait3>(t: T) {}
fn main() {
bar1(A);
bar2(A);
bar3(A);
bar4(A);
bar5(A);
bar6(A);
bar7(A); // A 的实例能用在所有的 7 个函数中
bar4(B);
bar6(B);
bar7(B); // B 的实例只能用在 3 个函数中
bar7(C); // C 的实例只能用在 1 个函数中
}
示例里,A 的实例能用在全部的(7 个)函数中,B 的实例只能用在 3 个函数中,C 的实例只能用在 1 个函数中。
我们再来看一个示例,这个示例演示了如何对带类型参数的结构体在实现方法的时候,按需求施加约束。
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
// 第一次 impl,注意:我们是为 Pair<T> 实现的方法,意味着 x、y 两个字段的类型必须也是 T
// 因此赋值给结构体字段的参数 a、b 也必须是 T 类型,否则它无法赋值给结构体字段
// 再比如 impl Pair<i32>,那么 a、b 就必须是 i32 类型
fn new(a: T, b: T) -> Self {
Self {x: a, y: b}
}
}
impl<T: Display + PartialOrd> Pair<T> {
// 第二次 impl
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
这个示例中,我们对类型 Pair 做了两次 impl。可以看到,第二次 impl 时,添加了约束 T: Display + PartialOrd。因为 cmd_display 方法需要用到打印能力和元素比较大小的能力,所以对类型参数 T 施加了 Display 和 PartialOrd 两种约束(两种能力)。而对于 new 和 default 来说,它不需要这些能力,因此 impl 的时候就可以不施加这些约束。我们知道,在 Rust 中对类型是可以多次 impl 的,这样便可以实现按需配置。
然后我们来实例化上面的结构体:
fn main() {
let p1 = Pair::new(110, 12);
let p2 = Pair::<&str>::new("110", "12");
p1.cmp_display(); // The largest member is x = 110
p2.cmp_display(); // The largest member is y = 12
}
需要里面的 p2,Pair::<&str>::new 表示我们告诉 Rust 单态化之后要作用于哪种特定类型,而不是让它基于参数去推断。而且里面的 ::<&str> 是可以省略的,但我想说的是,我们不可以写成下面这个样子:
Pair::new::<&str>("110", "12")
因为 xxx::<T> 意味着 xxx 一定定义了泛型参数,但对于当前来说,泛型参数来自于 Pair<T>,而不是 new 方法。如果想让这个操作合法的话:
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new<W>(a: T, b: T, c: W) -> Self {
Self {x: a, y: b}
}
}
fn main() {
let p1 = Pair::new(110, 12, "Anyway");
let p2 = Pair::new(110, 12, "Anyway".to_string());
let p3 = Pair::<&str>::new::<Option<&str>>("110", "12", Some("Anyway"));
}
此时是合法的,因为 new 方法定义了泛型,所以我们可以通过 new::<Option<&str>> 的方式告诉 Rust 怎么单态化,当然啦,此时的第三个参数的类型也就确定了。另外下面的代码也是没问题的:
// 一部分显式指定,一部分基于参数类型判断,都是可以的
let p4 = Pair::<f64>::new(3.14, 2.71, "嘿嘿");
let p5 = Pair::<f64>::new(3.14, 2.71, "哈哈");
let p6 = Pair::new::<Vec<i32>>("3.14", "2.71", vec![1, 2, 3]);
我们可以手动告诉 Rust 怎么单态化(针对哪种类型),也可以让 Rust 自己根据传递的参数的类型进行单态化,这两者是没有区别的。前者是我们告诉了 Rust 泛型代表的类型(也意味着传递的参数的类型要和指定的类型匹配),后者是让 Rust 基于参数去自己判断泛型代表的类型。
再举个例子,我们看一下字符串的 parse 方法:
字符串的 parse 方法也带了一个泛型,但这个泛型没有体现在类型参数中,而是体现在了返回值里面。
fn main() {
// 需要告诉 Rust 泛型参数 F 代表的类型
// 执行单态化,F 就是 64,内部就会执行 f64::from_str
let f = "3.14".parse::<f64>();
println!("{:?}", f); // Ok(3.14)
// 泛型要么体现在参数的类型签名中,要么体现在返回值的类型签名中
// 总之定义的泛型必须要使用它,否则该泛型就是无意义的
// 不管泛型体现在哪儿,我们都可以通过 ::<> 的方式手动指定,告诉 Rust 泛型代表的类型
// 但如果我们不手动指定,那么要让 Rust 能够推断出来,如果泛型用在了参数中,那很好办
// 直接基于给参数传递的值的类型,即可得到泛型对应的类型。但这里泛型没有体验在参数里,怎么办?
// 没有体现在参数中,那么就只能基于返回值来推断了,在将返回值赋给某个变量时,通过对变量做类型标注,来让 Rust 得到泛型信息
let n: Result<i32, _> = "123".parse();
// 返回值是 Result<F, F::Err>,而我们指定了 Result<i32, _>,那么 Rust 就知道了 F 是 i32
println!("{:?}", n); // Ok(123)
}
所以当泛型没有出现在参数中(一定出现在返回值中),那么可以通过将返回值赋给变量的时候,显式地规定变量的类型,来告诉 Rust 泛型代表哪一种类型。因为 Rust 要执行单态化,它必须知道函数调用时的泛型代表什么?如果泛型在参数里面,那么可以通过参数的值进行判断;如果不在参数里面,那么就需要通过将返回值赋给某个变量时,显式指定的变量类型来判断。
当然啦,不管是哪一种,通过 ::<> 手动指定这种方式肯定是没有问题的。
然后我们再来看一看字符串的 parse 方法,泛型 F 要求实现了 FromStr trait,所以字符串不可能转成任意类型,它只能转成那些实现了 FromStr 的类型。它最终会调用 FromStr 的 from_str 方法,虽然 FromStr 是个 trait,但由于这是在方法里面,调用字符串的 parse 方法、泛型 F 就确定了。对于源码来说:
F::from_str
FromStr::from_str
<F as FromStr>::from_str
这三者是等价的,但需要注意:这是类型调用的,所以要使用双冒号。如果是参数调用,那么表示类型的实例调用,要使用点号。
然后我们不妨也实现一下 FromStr trait,只要实现了,那么字符串的 parse 方法便可以转成对应的类型。
use std::str::FromStr;
#[derive(Debug)]
struct Girl {
name: String
}
impl FromStr for Girl {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "admin" {
Ok(Self {name: "管理员".to_string()})
} else {
Err("只允许管理员".to_string())
}
}
}
fn main() {
println!("{:?}", "admin".parse::<Girl>()); // Ok(Girl { name: "管理员" }
println!("{:?}", "nobody".parse::<Girl>()); // Err("只允许管理员")
}
因此这就是 trait,不管你是什么类型,只要你实现了指定的 trait。然后根据不同的具体实现,调用对应实现的方法。
相信你对 trait 已经拥有了足够深的认识,它就是一个约定,里面定义了实现该约定所以需要实现的方法。就好比你需要人,那么人就是泛型 T,但显然此时 T 是不受限制的,任何人都可以。但如果你需要 18~25 岁的小姐姐、长得好看、会做红烧排骨和糖醋鱼,那么就相当于给人施加了一些约束。
- 年龄、性别、容貌、会做饭,便是要实现 trait
- 18 ~ 25 岁、女、好看、会做红烧排骨和糖醋鱼,便是 trait 内部定义的方法。只有实现了 trait 内部的方法,才算实现了 trait。比如当前一个人要实现年龄对应的 trait,那么必须在 18 ~ 25 岁,否则就没有实现指定的 trait
所以 trait 既是一种约束,也是一种赋能(能力配置),它过滤掉了一些不满足条件的类型,同时也保证了满足条件的类型一定具备相应的能力。
然后你需要满足上面条件的小姐姐给你做红烧排骨,整个过程伪代码如下:
trait 年龄 {
18 ~ 25 岁
}
trait 性别 {
女
}
trait 容貌 {
好看
}
trait 会做饭 {
红烧排骨,
糖醋鱼
}
fn <T: 年龄 + 性别 + 容貌 + 会做饭> cook(人: T) {
人.红烧排骨;
}
只要实现了这些 trait,那么都可以调用 cook 函数,比如有很多小姐姐都满足这些约束。那么你就可以根据小姐姐的不同,吃到不同的红烧排骨。
trait 的孤儿规则
为了不导致混乱,Rust 定义了一个规则:如果要对一个类型实现某个 trait,那么这个类型和 trait 其中必须有一个是在当前模块中定义的,比如下面这两种情况都是可以的。
// 合法
use std::fmt::Display;
struct A;
impl Display for A {
// 省略具体方法
}
// 也合法
trait Trait1 {}
impl Trait1 for u32 {}
但是下面这样不可以,会编译报错。
use std::fmt::Display;
impl Display for u32 {}
错误信息如下:
只有定义在当前 crate 下的 trait,才可以被原生类型实现。而我们想给一个外部类型实现一个外部 trait,这是不允许的,但 Rustc 小助手提示我们,如果实在想用的话,可以用 new type 模式。
use std::fmt::{Display, Formatter};
struct MyU32(u32); // 用自定义类型将原生类型包裹起来
impl Display for MyU32 {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0.to_string())
}
}
impl MyU32 {
fn get(&self) -> u32 {
self.0
}
}
fn main() {
let n = MyU32(123);
println!("{}", n); // 123
println!("{}", n.get()); // 123
}
通过将原生类型包裹起来,便可为它实现不在当前模块内的 trait。
trait 的泛化实现
现在有一个需求,假设有两个 trait,分别是 Person 和 Female,如果我希望所有实现了 Female 的类型都自动实现 Person,该怎么做呢?
trait Person {
fn person(&self);
}
trait Female {
fn female(&self);
}
// 如果是 impl Person for T,则表示为类型 T 实现 Person trait,并且 T 一定要已定义
// 但 <T: Female> 表示 T 是一个泛型,并施加了 Female 约束,所以它代表任意实现了 Female trait 的类型
// 因此 impl <T: Female> Person for T,表示为实现了 Female trait 的类型实现 Person trait
// 怎样才算实现 Person 呢?显然要实现 Person 内部定义的方法
impl <T: Female> Person for T {
fn person(&self) {
println!("I am a Person")
}
}
// 因此,所有实现了 Female 的类型都会自动实现 Person
struct Girl;
// Girl 实现了 Female,那么会自动为它实现 Person
impl Female for Girl {
fn female(&self) {
println!("I am a Female")
}
}
fn main() {
let girl = Girl{};
girl.female(); // I am a Female
girl.person(); // I am a Person
}
此时我们不需要手动去实现 Person,只要它实现了 Female,会自动实现 Person。并且此时也不能实现 Person,因为默认已经实现了,再手动实现的话就会报错,因为一个 trait 在同一个类型上只能实现一次。我们再试试为 Girl 实现 Person,看看效果如何:
告诉我们冲突了。
此外还有一点需要注意,和实现普通的方法不同,实现普通方法可以分散在多个 impl 块,但实现 trait 的方法,只能有一个 impl 块。
依旧告诉我们冲突了,还是那句话同一个 trait 在同一个类型上只能实现一次。
带泛型的 trait
前面我们说过,为类型实现方法时,方法名不能重复,但是请看下面这个例子。
#[derive(Debug)]
struct Point<T> {
x: T,
y: T
}
impl Point<i32> {
fn compose(&self) {}
}
impl Point<String> {
fn compose(&self) {}
}
问题来了,这里实现了两个 compose 方法,为啥它没有报错呢?非常简单,因为 Point 是一个带泛型的结构体,而 compose 针对的是不同的类型,在调用的时候不会出现歧义。但如果改成下面这个样子:
use std::fmt::Display;
#[derive(Debug)]
struct Point<T> {
x: T,
y: T
}
impl Point<i32> {
fn compose(&self) {}
}
impl<T: Display> Point<T> {
fn compose(&self) {}
}
但是这样就报错了,因为 i32 实现了 Display,所以两个 compose 方法都可以作用于 i32。那么当字段为 i32 类型的结构体调用 compose 方法,要调用哪一个呢?此时就出现歧义了,因为这个声明不合法。
同样的,在实现 trait 的时候也是如此。
use std::fmt::Display;
trait Season {
fn season(&self);
}
struct Summer<T> {
v: T
}
// 因为 Summer 是带泛型的,不管是实现普通方法,还是实现 trait 的方法
// 都要指定泛型,比如这里就表示为 T 为 i32 的 Summer 实现 Season trait
impl Season for Summer<i32> {
fn season(&self) {
println!("我是春夏秋冬的第 {} 个季节", self.v)
}
}
impl Season for Summer<String> {
fn season(&self) {
println!("我是 {}", self.v)
}
}
fn main() {
let summer1 = Summer{v: 2};
let summer2 = Summer{v: "夏天".to_string()};
summer1.season(); // 我是春夏秋冬的第 2 个季节
summer2.season(); // 我是 夏天
}
咦,不是说一个 trait 在同一个类型上面只能实现一次吗?为啥这里实现了两次呢?很简单,因为 Summer<i32> 和 Summer<String> 不是一种类型,或者说它们代表不了彼此,在调用时不会出现歧义。
impl<T: Display> Season for Summer<T> {
fn season(&self) {
println!("我是春夏秋冬的第 {} 个季节", self.v)
}
}
impl Season for Summer<String> {
fn season(&self) {
println!("我是 {}", self.v)
}
}
但此时就不行了,因为 T 是 Display,而 String 满足 Display,那么对于 String 来说调用就会出现歧义。
但如果把 T: Display 改成 T: Display + Copy 就没有问题了,因为 String 不满足 Copy trait,所以不会出现歧义。
总结起来就两句话,在实现 trait 时:
- 如果结构体(S)不带泛型,那么 impl Trait for S 只能存在一次。
- 但如果结构体 S 带了泛型,那么 impl Trait for S<具体类型> 或者 impl<泛型: ...> Trait for S<泛型> 可以存在很多个,只要结构体的泛型字段代表的类型之间不存在交集即可。
不过还是要提醒一下,实现 trait 的方法时,它的方法要在一个 impl 块里面全部写完,不管结构体有没有带泛型,我们都不能分开写。比如为 S<i32> 实现 trait,那么所有的方法要一次性全部实现,不可以存在两个 impl SomeTrait for S<i32>。
总结一下泛型(类型参数)和 trait 出现的地方:
用 trait 对 T 作类型空间的约束,比如 T: TraitA
泛化实现里的 T,比如 impl<T: TraitB> TraitA for T {}
函数里的 T 参数,比如 fn bar<T>(a: T) {}
要注意区分不同位置的 T,它的基础意义都是类型参数,但是放在不同的位置其侧重的意义有所不同。
T: TraitA 里的 T 表示类型参数,强调“参数”,使用 TraitA 来削减它的类型空间
impl TraitA for T {} 末尾的 T 更强调泛型对应的的类型部分,为某些类型实现 TraitA
bar<T>(a: T) {} 中第二个 T 表示某种类型,更强调泛型对应的类型部分
以上的内容算是开胃菜,为了不和下面要说的内容产生混淆,单独啰嗦了一遍。我们上面说的泛型是定义在结构体里面的,但 trait 也是可以带泛型的。
trait Summer<T> {}
Summer 带了一个泛型 T,表示这个 trait 里面的函数或方法,可能会用到这个泛型。在定义 trait 的时候,还没确定这个类型参数的具体类型,要等到 impl 甚至使用类型方法的时候,才会具体化这个 T 的具体类型。
注意,这个时候Summer<T> 是一个整体,表示一个 trait。比如 Summer<u8> 和Summer<u32> 就是两个不同的 trait,单独把 Summer 拿出来说是没有意义的。然后实现时需要在 impl 后面先定义类型参数,比如:
impl<T> Summer<T> for MyType {}
// 当然也可以在对类型实现时,将 T 参数具体化,比如:
impl Summer<i32> for MyType {}
而如果被实现的类型上自身也带类型参数,那么情况会更复杂。
// 注意:Summer 和 MyType 的两个泛型是独立的,没有关系,虽然都叫 T
trait Summer<T> {}
struct MyType<T>(T)
// 为 MyType<W> 实现 Summer<U>
impl<U, W> Summer<U> for MyType<W> {}
泛型可以代表任意类型,所以叫什么名字不重要,这里起名为 U,W,当然也可以继续使用 T 这个字母。如果两个泛型是一样的,比如:
impl<T> Summer<T> for MyType<T> {}
那么当你为 MyType<i32> 实现 Summer 时,必须也是 Summer<i32>。然后在实现方法的时候,trait 里面的泛型继续施加 trait。
trait Summer<T> {}
struct MyType<T>(T)
// 为 MyType<W> 实现 Summer<U>
impl<U, W> Summer<U> for MyType<W> {
where
U: Debug,
W: PartialOrd
}
光说不练假把式,我们来举个实际的例子测试一下,需求如下:
平面上的一个点与平面上的另一个点相加,形成一个新的点,算法是两个点的 x 分量和 y 分量分别相加
平面上的一个点加一个整数 i32,形成一个新的点,算法是分别在 x 分量和 y 分量上面加这个 i32 参数
// 定义一个带类型参数(泛型)T 的 trait
trait Add<T> {
// 方法的返回值可能不固定,因此起一个类型别名叫 Output
type Output;
// 具体类型在实现的时候,add 方法的返回值类型是啥
// 取决于它在实现 Add 的时候,将 Output 对应的类型是啥,这样就实现了动态化
fn add(self, rhs: T) -> Self::Output;
}
#[derive(Debug)]
struct Point {
x: i32,
y: i32
}
// 为 Point 实现 Add<Point>
impl Add<Point> for Point {
type Output = Self;
// 这里的 Self::Output 其实就是 Self,但这种方式可以动态改变返回值类型
fn add(self, rhs: Point) -> Self::Output {
Self {x: self.x + rhs.x, y: self.y + rhs.y}
}
}
// 为 Point 实现 Add<i32>
impl Add<i32> for Point {
type Output = Self;
// 注意:Add<T> 里面的 add 方法的 rhs 参数也是 T,所以这两者是一致的
// 因此在实现 Add<i32> 时,里面的 add 方法的 rhs 参数也要是 i32。上面的 Add<Point> 也是同理
fn add(self, rhs: i32) -> Self::Output {
Self {x: self.x + rhs, y: self.y + rhs}
}
}
fn main() {
let p1 = Point{ x: 3, y: 4};
let p2 = Point{ x: 5, y: 8};
// 需要注意的是:Add<Point> 和 Add<i32> 时两个不同的 trait
// 这里的 p2 是 Point 类型,所以 p1.add(p2) 会调用为 Add<Point> 实现的方法
let p3 = p1.add(p2);
println!("{:?}", p3); // Point { x: 8, y: 12 }
// 调用为 Add<i32> 实现的方法
let p4 = p3.add(10);
println!("{:?}", p4); // Point { x: 18, y: 22 }
}
Add<T> 这个 trait,带一个泛型 T,还带一个关联类型 Output。对 Point 类型,我们实现了两个 trait,分别是 Add<Point> 和 Add<i32>。注意这已经是两个不同的 trait 了,所以能对同一个类型实现。
通过这种形式,我们在同一个类型上实现了同名方法(add 方法)参数类型的多种形态,在这里看起来就是,Point 实例的 add 方法既可以接收 Point 参数,又可以接收 i32 参数,Rustc 小助手可以根据不同的参数类型自动找到对应的方法调用。在 Java、C++ 这些语言中,有语言层面的函数重载特性来支持这种功能,Rust 中自身并不直接支持函数重载特性,但是它用 trait 就轻松实现了同样的效果,这是一种全新的思路。
当然啦,之前是结构体带泛型,但 trait 没有;现在是 trait 带泛型,而结构体没有。那么下面就来看看,当结构体和 trait 都带泛型会怎么样。
use std::fmt::Debug;
trait Add<T> {
type Output;
fn add(self, rhs: T) -> Self::Output;
}
#[derive(Debug)]
struct Point<T: Debug> {
x: T,
y: T
}
impl Add<f64> for Point<i32> {
type Output = Self;
// 我们是为 Point<i32> 实现 Add<f64>,那么参数 rhs 显然就是 f64
// 而 Self 和 Self::Output 就是 Point<i32>
fn add(self, rhs: f64) -> Self::Output {
println!("impl Add<f64> for Point<i32>");
Self {
x: (self.x as f64 + rhs) as i32,
y: (self.y as f64 + rhs) as i32,
}
}
}
// 标准库也自带了一个 Add,为了不冲突,我们起一个别名
use std::ops::{Add as _Add};
impl<T> Add<T> for Point<T>
where
// 这里给 T 添加了三个约束,前两个是 Copy 和 Debug,因为后面要涉及拷贝,所以要求 T 是可 Copy 的
// 然后定义结构体的时候,泛型 T 就添加了 Debug 约束,因此我们只能为 Point<实现 Debug 的类型> 实现 trait
// 所以当为 Point<T> 实现 trait 的时候,必须要给 T 添加 Debug 约束,所以这两个比较简单
// 但 _Add<Output=T> 估计有人会晕,首先 T: _Add 表示 T 必须支持加法运算,但由于它里面存在关联类型,而关联类型也必须指定
// 由于 Output 作为了加法运算的返回值类型,所以 T: _Add<Output=T> 表示 T 必须支持 +,运算结果也是 T
T: Copy + Debug + _Add<Output=T>
{
type Output = Self;
// 实现的 trait 是 Add<T>,所以里面的 add 方法的 rhs 参数也是 T
// 然后我们是为 Point<T> 实现 trait,因此 Self 就是 Point<T>
fn add(self, rhs: T) -> Self::Output {
println!("impl<T> Add<T> for Point<T>");
// 注意:self.x 和 self.y 都和 rhs 相加,而 Add trait 的 add 方法接收的是值
// 所以 T 才要是可 Copy 的,否则 self.x 加完之后,self.y 就加不了了
// 因为 T 实现了 _Add<Output=T>,所以它是可以相加的,并且相加之后的类型也是 T
Self {x: self.x + rhs, y: self.y + rhs}
}
}
fn main() {
let p1 = Point{ x: 3i32, y: 4};
println!("{:?}", p1.add(3f64));
/*
impl Add<f64> for Point<i32>
Point { x: 6, y: 7 }
*/
let p2 = Point{ x: 5u8, y: 8u8};
println!("{:?}", p2.add(3u8));
/*
impl<T> Add<T> for Point<T>
Point { x: 8, y: 11 }
*/
let p3 = Point{ x: 3.14f32, y: 2.71f32};
println!("{:?}", p3.add(1.414f32));
/*
impl<T> Add<T> for Point<T>
Point { x: 4.554, y: 4.124 }
*/
}
这个例子稍微有点复杂,尽管代码说的很详细了,这里再描述一遍。我们先为 Point<i32> 实现了 Add<f64>,表示当 Point 的泛型是 i32,调用的 add 方法里面的参数是 f64 时,这个调用才合法。然后为 Point<T> 实现了 Add<T>,表示 T 类型不限,只要实现了规定的三个约束即可,但由于 Point 的泛型和 Add 的泛型都是 T,所以当调用 add 方法时,结构体字段 x、y 的类型必须和参数 rhs 的类型保持一致。
整个过程还是不难理解的,然后还需要注意的是,实现不要冲突。比如我们改将第一个实现改一下:
impl Add<f64> for Point<i32>
// 改成如下
impl Add<i32> for Point<i32>
那么你会发现冲突了,因为 impl<T> Add<T> for Point<T> 已经包含了 impl Add<i32> for Point<i32>,事实上只要 Point 的泛型和 Add 的泛型一样,那么 impl<T> Add<T> for Point<T> 都能够表示,所以此时 Rust 编译器会报错。
当然啦,上面的实现,我们还可以改的更优雅一点。因为结构体实现的自定义 Add,它只能通过 add 方法进行相加,泛型 T 实现的是标准库的 Add,直接支持 + 操作符。所以我们改一下,统一用标准库的 Add 实现。
use std::ops::Add;
use std::fmt::Debug;
#[derive(Debug, Copy, Clone)]
struct Point<T: Debug> {
x: T,
y: T
}
// 注意:这里的 Add 是标准库提供的 Add
// 当执行 + 的时候,会自动调用内部的 add 方法,而不需要手动调用
impl Add<f64> for Point<i32> {
type Output = Self;
fn add(self, rhs: f64) -> Self::Output {
Self {
x: (self.x as f64 + rhs) as i32,
y: (self.y as f64 + rhs) as i32,
}
}
}
impl<T> Add<T> for Point<T>
where
T: Copy + Debug + Add<Output = T>
{
type Output = Self;
fn add(self, rhs: T) -> Self::Output {
Self {x: self.x + rhs, y: self.y + rhs}
}
}
// 上面的是让结构体和一个标量相加,这里让两个结构体相加
impl<T> Add<Point<T>> for Point<T>
where
T: Copy + Debug + Add<Output = T>
{
// 这里也可以不返回结构体本身,返回其它类型也可以,比如我们返回一个元组
type Output = (T, T);
fn add(self, rhs: Point<T>) -> Self::Output {
(self.x + rhs.x, self.y + rhs.y)
}
}
fn main() {
let p1 = Point{ x: 3i32, y: 4};
println!("{:?}", p1.add(3f64));
println!("{:?}", p1 + 3f64);
/*
Point { x: 6, y: 7 }
Point { x: 6, y: 7 }
*/
let p2 = Point{ x: 5u8, y: 8u8};
println!("{:?}", p2.add(3u8));
println!("{:?}", p2 + 3u8);
/*
Point { x: 8, y: 11 }
Point { x: 8, y: 11 }
*/
let p3 = Point{ x: 3.14f32, y: 2.71f32};
println!("{:?}", p3.add(1.414f32));
println!("{:?}", p3 + 1.414f32);
/*
Point { x: 4.554, y: 4.124 }
Point { x: 4.554, y: 4.124 }
*/
let p4 = Point{x: 11, y: 22};
let p5 = Point{x: 1, y: 2};
println!("{:?}", p4.add(p5)); // (12, 24)
println!("{:?}", p4 + p5); // (12, 24)
}
现在的话,实现是不是变得优雅多了呢?所以这就是 trait 的威力,Rust 一切皆类型,由 trait 定义类型的行为逻辑。
trait 泛型的默认值
我们看一下 Add trait 的具体定义,会发现有一些不一样。
这里 Rhs 可以理解为泛型 T,当然它本来就是泛型,只不过名字不一样,泛型的名称不要求一定是单个字母。但是我们看到它有个默认值,这是什么意思呢?一般来说,如果 trait 带了泛型,那么你在实现 trait 时,泛型也需要考虑。比如 impl Add<i32>,impl Add<String>等等,当然在实现时也可以指定泛型,比如 impl Add<T>,总之光讨论 Add 是没有意义的,或者说如果只是 impl Add 的话是会报错的,因为没有考虑 trait 的泛型。
但如果 trait 的泛型有默认值就不一样了,比如 Add<T = i32>,那么在实现的时候 impl Add 等价于 impl Add<i32>。而如果默认值是 Self,那么这个 Self 就等价于实现它的类型,我们举个例子:
use std::ops::Add;
use std::fmt::Debug;
#[derive(Debug, Copy, Clone)]
struct Point<T: Debug> {
x: T,
y: T
}
// 实现它的是 Point<T>,那么 Add 就等价于 Add<Point<T>>
impl<T> Add<Point<T>> for Point<T>
where
T: Copy + Debug + Add<Output = T>
{
type Output = (T, T);
fn add(self, rhs: Point<T>) -> Self::Output {
(self.x + rhs.x, self.y + rhs.y)
}
}
fn main() {
let p1 = Point{x: 11, y: 22};
let p2 = Point{x: 1, y: 2};
println!("{:?}", p1.add(p2)); // (12, 24)
println!("{:?}", p1 + p2); // (12, 24)
}
所以默认参数给表达上带来了一定程度的简洁,但也增加了识别和理解上的困难。
另外这个语法和使用 trait 时具化关联类型比较像,举个例子:
trait Season<T = Self> {
type Item;
fn season(a: T) {}
}
// T 的约束是 Season<i32, Item=String>,表示 T 要实现 Season<i32>
// 并且里面的 Item 要具化为 String
struct Foo<T: Season<i32, Item=String>> {
x: T
}
// 等价于 struct Bar<T: Season<T, Item=String>>
struct Bar<T: Season<Item=String>> {
x: T
}
虽然语法类似,但含义是完全不同的。
trait 中的类型参数(泛型)与关联类型的区别
现在你可能会有些疑惑:trait 上的泛型和关联类型都具有延迟具化的特点,那么它们的区别是什么呢?为什么要设计两种不同的机制呢?首先要明确的一点是,Rust 本身也在持续演化过程中,有些特性先出现,有些特性后出现,最后演化出功能相似但是不完全一样的特性是完全有可能的。
具体到这两者来说,它们主要有两点不同。
1. 泛型可以在 impl 类型的时候具化,也可以延迟到使用的时候具化。而关联类型在被 impl 时就必须具化
2. 由于泛型和 trait 名一起组成了完整的 trait,不同的具化类型会构成不同的 trait,所以看起来同一个定义可以在目标类型上实现多次,而关联类型没有这个作用
下面我们举个例子,先看第一条:
use std::fmt::{Debug, Display};
// 定义一个 trait,带个泛型参数 T
// 但这个 T 不仅可以有默认值,我们同样可以继续用 trait 给它施加约束
trait Season<T = i32> // 也可以写成 trait Season<T: Copy = i32>
where T: Copy
{
fn season(&self, v: T) {}
}
struct Summer;
// 由于 Season 的泛型 T 被施加了 Copy 约束,那么就不能随心所欲的实现了
// 我们只能实现 Season<可 Copy 的类型>,同理上面的默认值也要是可 Copy 的
// 如果我们写成了 Season<T = String>,那么就不对,因为 String 没有实现 Copy
// 但是呢,我们也可以施加更强的约束,目前来说只要 T 是 Copy 的,那么就可以实现 Season<T>
// 但我们希望某些方法在被调用时,Season<T> 里面的 T 具有更强的约束
impl<T> Season<T> for Summer
// 定义 trait 的时候,给 T 施加了 Copy 约束,所以在实现的时候,也必须给 T 施加 Copy 约束
// 所以只要满足 T: Copy,那么 Season<T> 就可以实现,但是还不够,我们希望 T 还满足 Display
where T: Copy + Display
{
fn season(&self, v: T) {
println!("{}", v)
}
}
fn main() {
let summer = Summer{};
summer.season(123); // 123
// 不合法,因为数组虽然满足 Copy,但不满足 Display,如果将 Display 换成 Debug 是没问题的
// summer.season([1, 2, 3]);
}
这个示例展示了几个要点。
定义带泛型的 trait 时可以用 where 表达,并提供约束,当然也可以不使用 where,直接 T: ... 也行
impl trait 时可以对类型参数加强约束,对应例子中的 Copy + Display
在 impl trait 的时候不需要知道泛型代表的类型,而是在调用方法的时候再确定
对应的,对关联类型来说,如果你在 impl 时不对其具化,就无法编译通过。所以对于第二点也举个例子,我们把前面对 Point 类型实现 Add 的模型尝试用关联类型实现一遍。
trait Add {
type Input;
type Output;
fn add(self, rhs: Self::Input) -> Self::Output;
}
#[derive(Debug)]
struct Point {
x: i32,
y: i32
}
// 只能实现一次,因为都没有泛型,所以我们无法再一次 impl Add for Point
impl Add for Point {
type Input = Point; // 关联类型在 impl 的时候就必须具化
type Output = Point;
fn add(self, rhs: Self::Input) -> Self::Output {
Self {x: self.x + rhs.x, y: self.y + rhs.y}
}
}
fn main() {
let p1 = Point{x: 1, y: 2};
let p2 = Point{x: 2, y: 3};
println!("{:?}", p1.add(p2)); // Point { x: 3, y: 5 }
}
这么写是没有问题的,但我希望输入和输出不仅可以是 Point,还可以是别的,比如支持 Point 和 i32 相加,但目前是无法做到的。因为我们没有泛型,trait 在同一个类型上只能实现一次。
要这么看起来,好像带泛型的 trait 功能更强大,那用这个不就够了?但关联类型也有它的优点,比如关联类型没有泛型,不存在多引入了一个参数的问题。而而且泛型是具有传染性的,特别是在一个调用层次很深的系统中,增删一个类型参数可能会导致整个项目文件到处都需要改,非常头疼。而关联类型没有这个问题。在一些场合下,关联类型正好是减少泛型数量的一种方法。更不要说,有时模型比较简单,不需要多态特性,这时用关联类型就更简洁,代码可读性更好。
trait object
先来看一个常见的需求,要在一个 Rust 函数中返回可能的多种类型,应该怎么写?如果我们写成返回固定类型的函数签名,那么它就只能返回那个类型。
struct Spring;
struct Summer;
struct Autumn;
struct Winter;
fn get_season() -> Spring {
Spring{}
}
如果希望这四个结构体类型都能作为返回值,你首先想到的可能是使用枚举。
struct Spring;
struct Summer;
struct Autumn;
struct Winter;
enum Season {
A(Spring),
B(Summer),
C(Autumn),
D(Winter),
}
fn get_season(v: char) -> Season {
if v == 'A' {
Season::A(Spring{})
} else if v == 'B' {
Season::B(Summer{})
} else if v == 'C' {
Season::C(Autumn{})
} else {
Season::D(Winter{})
}
}
enum 常用于聚合类型,这些类型之间可以没有任何关系,用 enum 可以无脑 + 强行把它们揉在一起。enum 聚合类型是编码时已知的类型,也就是说在聚合前,需要知道待聚合类型的边界,一旦定义完成,之后运行时就不能改动了,它是封闭类型集。换句话说,使用 enum,你必须知道所有可能返回的类型,并为它们设置一个枚举变体。
第二种办法是利用泛型,我们试着引入一个类型参数,改写一下。
struct Spring;
struct Summer;
struct Autumn;
struct Winter;
fn get_season<T>() -> T {
Summer{}
}
但显然这个函数是无法通过编译的,我们看一下报错:
意思是返回值类型是 T,但我们却返回了 Summer,但这里问题就产生了,不是说 T 可以代表任意类型吗,那为啥返回 Summer 不行呢?
如果 T 是在参数里面,那么确实没问题,因为它可以代表任意类型,所以不管什么参数都可以传给它,基于类型执行单态化。但反过来则不行,单态化之后 T 不一定会和返回值类型一致,比如我们上面返回的是 Summer 实例,但没有任何依据表明,单态化之后 T 的类型一定是 Summer。
如果像下面这么做就没问题:
struct Spring;
struct Summer;
struct Autumn;
struct Winter;
fn get_season<T>(season: T) -> T {
season
}
fn main() {
get_season(Spring{});
get_season(Summer{});
get_season(123);
}
因为参数 season 是 T 类型,返回值类型也是 T,所以不管 season 接收的是什么,它作为返回值都没问题,它的类型就是返回值的类型。但如果不接收参数,那么就是有问题的,因为无法确保单态化之后的 T 和返回值类型是一致的。
但在介绍字符串的 parse 方法时,我们看到 parse 方法有泛型,但却没有接收参数,那它是怎么返回的泛型呢?
原来它没有直接返回,而调用了对应泛型的方法,那么我们也可以实现一个。
struct Spring;
struct Summer;
struct Autumn;
struct Winter;
impl Spring {
fn get_season() -> Self {
Spring{}
}
}
impl Summer {
fn get_season() -> Self {
Summer{}
}
}
impl Autumn {
fn get_season() -> Self {
Autumn{}
}
}
impl Winter {
fn get_season() -> Self {
Winter{}
}
}
fn get_season<T>() -> T {
T::get_season()
}
但这里问题又产生了,我们怎么知道 T 的内部一定有 get_season 方法呢?所以你肯定想到了,要使用 trait。
trait Season {
fn get_season() -> Self;
}
#[derive(Debug)]
struct Spring;
#[derive(Debug)]
struct Summer;
#[derive(Debug)]
struct Autumn;
#[derive(Debug)]
struct Winter;
impl Season for Spring {
fn get_season() -> Self {
Spring{}
}
}
impl Season for Summer {
fn get_season() -> Self {
Summer{}
}
}
impl Season for Autumn {
fn get_season() -> Self {
Autumn{}
}
}
impl Season for Winter {
fn get_season() -> Self {
Winter{}
}
}
// T 必须实现 Season trait
fn get_season<T: Season>() -> T {
// 此处写成 Season:: get_season() 也是可以的,单态化之后(比如 T 是 Summer)
// 那么无论哪种写法,最终都会变成 Summer:: get_season()
T::get_season()
}
fn main() {
let spring = get_season::<Spring>();
let summer: Summer = get_season();
let autumn: Autumn = get_season();
let winter = get_season::<Winter>();
println!("{:?}", spring); // Spring
println!("{:?}", summer); // Summer
println!("{:?}", autumn); // Autumn
println!("{:?}", winter); // Winter
}
此时就没有任何问题了,因为 T 必须实现 Season trait,那么它一定有 get_season 方法。然后我们看一下 get_season 方法的签名,它返回的是 Self,也就是实现它的类型。不管是 Spring、Summer,还是其它实现了 Season trait 的类型,调用 get_season 之后返回的都是它自身的实例。换句话说,T:: get_season() 之后返回的类型还是 T,此时就满足了返回值类型和签名中的类型是一致的,Rust 在看到这一层约束后,也就不会报编译错误了。
所以泛型要么出现在参数中,要么出现在返回值中。如果出现在参数中,那么非常简单,泛型可以表示任意类型,因此可以接收任意参数,然后基于参数的类型进行单态化。但如果出现在返回值中,整个过程就是相反的,正因为它可以表示任意类型,那么返回的时候就不能随心所欲了,一定要保证单态化之后的 T 和返回值类型是一致的。
而通过实现 trait,我们就做到了这一点。因为 get_season 方法返回的是 Self,所以不管什么方法,只要实现了 Season,那么 get_season 方法返回的就是它本身,因此 T::get_season() 返回的就是 T,符合返回值的签名。
与此同时,我们再来看看字符串的 parse 方法:
显然不管 F 代表什么,它都能保证 F:: from_str 返回的一定是 Result<F, F::Err>。而 F 实现了 FromStr trait,那么 FromStr 内部一定定义了 from_str 方法,这个方法的返回值签名一定是 Result<Self, Self::Err>。
结果和我们分析的是一样的,看一下 FromStr 的定义,还记得这个语法吗?它表示 trait 的继承,任何实现 FromStr 的类型都必须实现 Sized,否则就不叫实现 FromStr。然后它里面的 from_str 方法的返回值签名是 Result<Self, Self::Err>,这样不管 parse 方法单态化后的泛型 F 代表什么,都没有问题。因为它施加了 FromStr 约束,一定有 from_str 方法,而该方法的返回值类型和 parse 方法要求的返回值类型是一致的。
比如单态化后的 F 是 i32,那么 parse 方法的返回值就是 Result<i32, ...>,而内部会调用 i32::from_str,最终返回的也一定是 Result<i32, ...>。如果 i32::from_str 返回的不是 Result<i32, ...>,那它就不可能实现 FromStr 这个 trait,因为 Self 表达的含义就是实现它的类型。因此通过这一层层的关系,就可以确保函数实际的返回值和函数的返回值签名一定是匹配的,最终通过挑剔的 Rust 编译器的检测。
我们再看一下具体类型的 from_str 源码实现:
这是 bool 类型的 from_str 实现,因为 FromStr trait 的 from_str 方法的返回值签名是 Result<Self, ...>,那么 bool 类型在实现时,返回值类型就是 Result<bool, ...>。
至于里面的 ... 则表示关联类型,这里的是 Self:: Err,因此 FromStr trait 还要求在实现的时候起个类型别名叫 Err。因为解析成不同的结构出错时,那么报错信息也应该不一样,比如 ParseBoolError、ParseIntError、ParseFloatError 等等。这些错误类型如何统一呢?显然是通过关联类型,这样只需用一个 Self:: Err 即可全部代替了,至于 Err 到底是啥,则取决于实现它的类型,因此便实现了动态化。
到目前为止,本文写的应该比较长了,主要是 Rust 确实复杂,里面的概念比较多,因此我们一直在不停地啰嗦,但我相信这一切都是值得的。但面对开始说的这个情况,其实 Rust 提供了一个更优雅的解决方法。
trait Season {}
#[derive(Debug)]
struct Spring;
#[derive(Debug)]
struct Summer;
#[derive(Debug)]
struct Autumn;
#[derive(Debug)]
struct Winter;
impl Season for Spring {}
impl Season for Summer {}
impl Season for Autumn {}
impl Season for Winter {}
// T 必须实现 Season trait
fn get_season() -> impl Season {
Spring{} // 或者返回 Summer{}, Autumn{}, Winter{} 也可以
}
fn main() {
let spring = get_season();
}
注意:现在返回值类型变成了 impl Season,意思是只要返回值的类型实现了 Season trait 即可,显然这就方便很多了。不过说到这你可能会有一个疑问:
fn get_season1<T: Season>() -> T {
Spring{}
}
fn get_season2() -> impl Season {
Spring{}
}
这不都表示返回值是一个实现了 Season trait 的类型吗,为啥第一种不行,第二种可以呢?
- 很简单,get_season1 返回的是泛型 T,这意味着函数返回的类型将由调用者决定,而不是由函数内部决定。调用者在调用函数时指定了 T 的具体类型,函数必须返回这个指定的类型。而我们不能保证单态化之后,T 的类型一定是 Spring,如果调用者期望得到一个 Summer 类型的实例,但函数返回了 Spring,这就造成了类型不匹配。
- get_season2 不同,它表示函数将返回某个实现了 Season trait 的具体类型,但这个具体类型是由函数内部决定的。调用者不需要知道返回的确切类型,只需要知道它实现了 Season trait,这种方式返回的是一个具体的但不明确的类型。
但是光有 impl Season 还不够,什么意思呢?
fn get_season(i: u32) -> impl Season {
if i == 1 {
Spring{}
} else if i == 2 {
Summer{}
} else if i == 3 {
Autumn{}
} else {
Winter{}
}
}
你觉得这个函数合法吗?我们说过这种方式表达的是:函数返回一个具体的、但不确定的类型。意思就是函数返回啥,我们不知道,但只要它实现了 Season trait 即可。但函数的返回值是固定的,不能一会儿返回 Spring、一会儿返回 Summer。
告诉我们 if 表达式的所有分支都应该返回相同的类型,注意:是相同的类型,虽然它们都实现了 Season,但类型明显是不同的。
因为 impl Season 作为函数返回值这种语法,其实也只是指代某一种类型而已,而这种类型是在函数体中由返回值的类型来自动推导出来的。例子中,Rustc 小助手遇到第一个分支时,就已经确定了函数返回类型为 Spring,因此当它分析到后面的分支时,就发现类型不匹配了,问题就在这里。你可以将条件分支顺序换一下,看一下报错的提示,加深印象。
那我们应该怎么处理这种问题呢?Rust 还给我们提供了进一步的措施:trait object,形式上,就是在 trait 名的前面加 dyn 关键字修饰,在这个例子里就是 dyn Season。dyn TraitName 本身就是一种类型,它和 TraitName 这个 trait 相关,但是它们不同,dyn TraitName 是一个独立的类型。
但是将返回值签名改成 dyn Season 的时候也是会报错的,因为它表达的正是我们期望的含义,返回值类型可以不同,只要实现了 Season 即可。所以 dyn Season 不是一个固定尺寸类型,因为可以有多种类型,但大小不确定的话,Rust 编译器是不能容忍的。于是执行的时候,编译器会报错,给我们两个建议:
- 使用 impl Season 解决,但所有分支必须返回同一种类型。
- 通过 Box 将 dyn Season 包起来。
第一种显然这不是我们想要的,我们来试试第二种。
fn get_season(i: u32) -> Box<dyn Season> {
if i == 1 {
Box::new(Spring{})
} else if i == 2 {
Box::new(Summer{})
} else if i == 3 {
Box::new(Autumn{})
} else {
Box::new(Winter{})
}
}
这下完美了,编译通过,达成目标,我们成功地将不同类型的实例在同一个函数中返回了。
这里我们使用了 Box,我们说它是智能指针,可以理解为一个小盒子。它可以保证盒子里面的内容会在堆上分配,即使是 Copy 类型的值,并且它持有堆内存的所有权。所以我们不能返回 &dyn Season,因为会造成悬空引用,也就是值没了,但是它的引用被返回了。而 Box<T> 则没有问题,因为它是自持所有权的类型,智能指针还在,那么值就还在(不会发生指针还在、但值没了的情况)。
利用 trait object 传参
trait object 可以作为返回值,那么可不可以作为参数呢?毫无疑问是可以的。
fn get_season1(v: impl Season) {}
fn get_season2<T: Season>(v: T) {}
此时的 get_season1 和 get_season2 就是等价的,因为作为参数的话,不管第一种还是第二种,v 的类型都是由调用者确定的,所以两者是等价的。但如果作为返回值,那么使用 impl 就是由函数内部决定,而使用泛型则依旧由调用者决定,此时两者就不等价了。
当作为参数时,无论是泛型还是 impl trait,这两者都会由编译器静态展开,也就是编译时具化(单态化)。
fn get_season(v: impl Season) {}
fn main() {
get_season(Spring{});
get_season(Summer{});
}
上面代码就等价于如下:
fn get_season_Spring(v: Spring) {}
fn get_season_Summer(v: Summer) {}
fn main() {
get_season_Spring(Spring{});
get_season_Summer(Summer{});
}
但如果将 impl 换成 dyn 就不一样了:
fn get_season(v: &dyn Season) {}
fn main() {
get_season(&Spring{});
get_season(&Summer{});
}
dyn trait 的版本不会在编译期间做任何展开,dyn Season 自己就是一个类型,这个类型相当于一个代理类型,用于在运行时代理相关类型及调用对应方法。既然是代理,也就是调用方法的时候需要多跳转一次,从性能上来说,当然要比在编译期直接展开一步到位调用对应函数要慢一点。
但静态展开也有问题,就是会使编译出来的内容体积增大,而 dyn trait 则不会。所以它们各有利弊,可以根据需求视情况选择,总之 impl trait 和 dyn trait 都是消除类型参数的办法。
另外这里的 dyn Season 前面使用了引用,因为 dyn 表示动态分发的 trait 对象,允许对实现了特定 trait 的不同类型进行统一处理,这是 Rust 的一种多态机制。但与此同时 dyn trait 也意味着大小不固定,而 Rust 要求函数参数的大小是固定的,这时候要么使用引用,要么使用智能指针,这样在传参的时候大小就是固定的。但如果 dyn trait 作为了返回值,那么就只能使用智能指针了,因为引用不持有值的所有权,但智能指针持有。
和 enum 相比有什么区别
impl trait 和 dyn trait 都可以用来消除泛型,那它们和 enum 相比有什么不同呢?
enum 是封闭类型集,可以把没有任何关系的任意类型包裹成一个统一的单一类型。后续的任何变动,都需要改这个统一类型,以及基于这个 enum 的模式匹配等相关代码。而 impl trait 和 dyn trait 是开放类型集,只要对新的类型实现 trait,就可以传入使用了 impl trait 或 dyn trait 的函数,其函数签名不用变。
上述区别对于库的提供者非常重要,如果你提供了一个库,里面的多类型使用的 enum 包装,那么库的使用者没办法对你的 enum 进行扩展。因为一般来说,我们不鼓励去修改库里面的代码。而用 impl trait 或 dyn trait 就可以让接口具有可扩展性,用户只需要给他们的类型实现你的库提供的 trait,就可以代入库的接口使用了。
对于 impl trait 来说,它目前只能用于少数几个地方,一个是函数参数,另一个是函数返回值,其它的静态展开场景就得用泛型了。dyn trait 本身是一种非固定尺寸类型,这就注定了相比于 impl trait 它能应用于更多场合,比如利用 trait object 把不同的类型装进集合里。
利用 trait object 将不同的类型装进集合里
我们上面定义了四种季节,如何将它们放在一个数组里面呢?因为数组要求所有元素的类型是一致的,所以我们肯定不能直接放。
impl Season for Spring {}
impl Season for Summer {}
impl Season for Autumn {}
impl Season for Winter {}
fn main() {
// 再次强调,dyn Season 是一个无法在编译期间确定大小的类型
// 所以它不能作为函数参数或返回值的类型,也不能用来表示数组元组的类型
// 因为数组要求元素大小固定,而 dyn Season 的大小都不确定,自然也就谈不上固定了
// 因此在使用 dyn Season 的时候,要么使用引用、要么使用智能指针
let vec1: Vec<Box<dyn Season>> = vec![
Box::new(Spring{}),
Box::new(Summer{}),
Box::new(Autumn{}),
Box::new(Winter{}),
];
let vec2: Vec<&dyn Season> = vec![
&Spring{}, &Summer{},
&Autumn{}, &Winter{},
];
}
成功了,不同类型的实例(准确来说是实例的引用)竟然被放进了同一个 Vec 中,你可以自己尝试一下,将不同类型的实例放入 HashMap 中。
问题来了,既然 trait object 这么好用,那是不是可以随便使用呢?不是的,除了前面提到的性能损失之外(无法单态化或者说编译展开),还有一个问题,不是所有的 trait 都可以做 dyn 化,也就是说,不是所有的 trait 都能转成 trait object 使用。
哪些 trait 能用作 trait object?
只有满足对象安全(object safety)的 trait 才能被用作 trait object,Rust 参考手册上有关于 object safety 的详细规则,但比较复杂,这里我们了解常用的模式就行。
trait NotObjectSafe {
// 1) 不能包含关联常量
const CONST: i32 = 1;
// 2) 不能包含没有 &self 的关联函数
fn foo() {}
// 3) 不能将 Self 所有权传入,也就是第一个参数应该是引用
fn selfin(self);
// 4) 不能返回 Self,因为 trait object 在产生时,原来的类型会被抹去,所以 Self 究竟是谁不知道
fn returns(&self) -> Self;
// 5)方法中不能有泛型,因为泛型在编译时会单态化,而 trait object 是运行时的产物,两者不能兼容
fn typed<T>(&self, x: T) {}
}
规则确实稍微有点复杂,但是我们可以简单记住几种场景。
不要在 trait 里面定义构造函数,比如 new 这种返回 Self 的关联函数。你可以发现,确实在整个 Rust 生态中都没有将构造函数定义在 trait 中的习惯。
trait 里面尽量定义传引用 &self 或 &mut self 的方法,而不要定义传值 self 的方法。
因此并不是所有的 trait 都能以 trait object 形式(dyn trait)使用,所以你可以在遇到编译器报错的时候再回头来审视 trait 定义得是否合理,大部分情况下都可以放心使用。
trait object 的实现原理
虽然 trait object 是 Rust 独有的概念,但是这个概念并不新鲜,为什么这么说呢,来看它的实现机理。
trait Season {
fn get_season(&self) -> &str;
}
#[derive(Debug)]
struct Autumn {
name: String
}
impl Season for Autumn {
fn get_season(&self) -> &str {
&self.name
}
}
fn main() {
// 赋值一个变量也是同理,不管是作为函数参数、返回值、还是作为容器的某个元素,大小都必须要固定
// 我们不能把 trait 或 dyn trait 赋值给一个变量,因为它们的类型不固定,大小也不固定
// 所以只能赋值 &dyn Season 或者 Box<dyn Season>,因为引用和智能指针的大小是固定的
// 还是那句话:堆上的内存一定有一个栈上的指针指向它,所以 dyn trait 大小不固定没关系
// 我们只需要赋值它的引用或者智能指针即可,这样保存的值就是大小固定的,然后通过引用间接地操作动态数据
let season: &dyn Season = &Autumn{name: "秋天".to_string()};
println!("{}", season.get_season()); // 秋天
}
这是一个简单的例子,我们将 Autumn 实例的引用赋值给了 season 变量,赋值之后会生成一个 trait object,也就是这里的 season。
可以看到 trait object 的底层逻辑就是胖指针,其中,一个指针指向数据本身,另一个则指向虚函数表(vtable)。
vtable 是一张静态的表,Rust 在编译时会为使用了 trait object 的类型所实现的 trait 生成一张表,放在可执行文件中(一般在 TEXT 或 RODATA 段),如下所示:
在这张表里,包含具体类型的一些信息,如 size、aligment 以及一系列函数指针:
这个接口支持的所有的方法,比如 format()
具体类型的 drop trait,当 trait object 被释放,它用来释放其使用的所有资源
这样在调用方法时,trait object 就可以从 vtable 里找到对应的函数指针,执行具体的操作。所以 Rust 里的 trait object 没什么神秘的,它不过是 C++ / Java 中 vtable 的一个变体而已。
这里说句题外话,C++ / Java 指向 vtable 的指针,在编译时放在类结构里,而 Rust 放在 trait object 中。这也是为什么 Rust 很容易对原生类型做动态分派,而 C++/Java 不行。事实上,Rust 也并不区分原生类型和组合类型,对 Rust 来说,所有类型的地位都是一致的。
小结
以上我们就详细复习了 trait,并补充了 trait 带类型参数的情况,各种符号组合起来,确实越来越复杂了。不过还是那句话,模式就那几种,只要花点时间熟悉理解,其实并不难。开始的时候能认识就行,后面在实践中再慢慢掌握。
然后我们使用带泛型的 trait 实现了其它语言中函数重载的功能,看起来途径有点曲折,但是带给了我们一条全新的思路:以往的语言必须给自身添加各种特性来满足用户的要求,但在 Rust 中,用好 trait 就能搞定。这让我们对 Rust 的未来充满期待,随着时间的发展,它不会像 C++、Java 那样永不停歇地添加可能会导致组合爆炸的新特性,而让自身越来越臃肿。
我们还讨论了带类型参数的 trait 与关联类型的区别。它们之间并不存在绝对优势的一方,在合适的场景下选择合适的方案是最重要的。
然后我们通过一个问题:如何让一个 Rust 函数返回可能的多种类型?推导出了引入 trait object 方案的必要性。整个推导过程比较曲折,同时也是对 Rust 类型系统的一次精彩探索。在这个探索过程中,我们和 Rustc 小助手成为了好朋友,在它的协助下找到了最佳方案。
最后我们了解了 trait object 的一些用途,并讨论了 trait object、impl trait,还有使用枚举对类型进行聚合这三种方式之间的区别。类型系统(类型 + trait)是 Rust 的大脑,你可以多加练习,熟悉它的形式,掌握它的用法。
**本文来自于,极客时间:《Rust 语言从入门到实战》
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏