上一篇文章讲述了trait的基本使用方法,还有一些情况下需要使用更加复杂的trait
trait上带类型参数
trait上也是可以带类型参数的,表示这个trait里面的函数或方法,可能会用到这个类型参数。在定义trait的时候,还没确定这个类型参数的具体类型。要等到impl甚至使用类型方法的时候,才会具体这个T的具体类型。需要注意,这个时候TraitA是一个整体,表示一个trait。比如TraitA和TraitA就是两个不同的trait。
rust
trait TraitA<T> {}
struct Atype<U> {
a: U
}
impl<T, U> TraitA<T> for Atype<U> {}
trait类型参数的默认实现
trait TraitA<T = u64> {}
trait中的类型参数与关联类型的区别
-
类型参数可以在impl类型的时候具化,也可以延迟到使用的时候具化。而关联类型在呗impl时就必须具化。
-
由于类型参数和trait名一起组成了完整的trait名字,不同的具化类型会构成不同的trait,所以看起来同一个定义可以再目标类型上多次实现。而关联类型不能。
rustuse std::fmt::Debug; trait TraitA<T> where T: Debug, { fn play(&self, _t: T) {} } struct Atype; impl<T> TraitA<T> from Atype where T: Debug + PartialEq // impl时对trait加强约束 {} fn main() { let a = Atype; a.play(10u32); // 在使用时才具化T为u32 }
这个示例展示了几个要点
- 定义带类型参数的trait时可以使用where表达提供约束
- impl trait时可以对类型参数加强约束
- impl trait时可以不具化类型参数
- 可以在使用方法时具化类型参数
这么看来,类型参数比关联类型更加强大,但关联类型也有它的优点,比如管理按类型没有类型参数,不存在多引入了一个参数的问题,而类型参数是具有传染性的,特别是在一个调用层次很深的系统中,增删一个类型参数可能会导致整个项目文件到处都需要修改。
trait object
一个函数要返回不同的类型应该怎么做,其中一个解决方法是使用enum。
enum常用于聚合类型。这些类型之间可以没有任何关系,用enum可以无脑+强行把他们揉在一起。enum聚合类型是编码时已知的类型,也就是说在聚合前,需要知道待聚合类型的边界,一旦定义完成,之后运行时就不能改动了,它是封闭类型集。
实际上,Rust提供了更优雅的方案来解决这个需求,利用trait提供了一种特殊语法impl trait。
rust
struct Atype;
struct Btype;
struct Ctype;
trait TraitA {}
impl TraitA for Atype {}
impl TraitA for Btype {}
impl TraitA for Ctype {}
fn doit() -> impl TraitA {
let a = Atype;
a
}
这段代码确实解决了需求,这种表达非常简洁,但还是不够灵活,比如要用if逻辑选择不同的分支返回不同的类型。问题在于,impl TraitA作为函数返回值这种语法,其实也只是指代某种类型而已,而这种类型是在函数体由返回值的类型来自动推到出来的。
Rust还给我们提供了进一步的措施:trait object。形式上就是在trait名前加dyn关键字修饰。
dyn TraitName本身就是一种类型,它和TraitName这个Trait相关,但他们不同,dyn TraitName是一个独立的类型。
rust
struct Atype;
struct Btype;
struct Ctype;
trait TraitA {}
impl TraitA for Atype {}
impl TraitA for Btype {}
impl TraitA for Ctype {}
// dyn TraitA编译时的尺寸未知,Box<T>的作用是可以保证获得里面值得所有权,必要时会进行内存复制
fn doit(i: u32) -> Box<dyn TraitA> {
if i == 0 {
let a = Atype;
Box::new(a)
} else if i == 1 {
let b = Btype;
Box::new(b)
} else {
let c = Ctype;
Box::new(c)
}
}
利用trait object传参
javascript
fn doit(x: impl TraitA) {}
// 相当于 fn doit<T: TraitA> (x: T) {}
javascript
fn doit(x: &dyn TraitA) {}
fn main() {
let a = Atype;
doit(&a);
}
两种方式都可以。impl trail用的是编译器静态展开,也就是编译时具化(单态化)。
而dyn trait的版本不会在编译期间做任何展开,dyn TraitA自己就是一个类型,这个类型相当于一个代理类型,用于在运行时代理相关类型及调用对应的方法。既然是代理,也就是调用方法的时候需要跳转多次,从性能上来说,当然要比在编译期直接展开一步到位调用对应函数要慢一点,但静态展开的问题就是会使编译出来的内容体积增大。
哪些trait能用作trait object
只有满足对象安全的trait才能被用作trait object。
规则比较复杂,可以简单记住几个场景
- 不要在trait里面定义构造函数,比如new这种返回Self的关联函数。确实在整个Rust生态中都没有将构造函数定义在trait中的习惯
- trait里面尽量定义传引用&self或&mut self的方法,而不要定义传值self的方法
&dyn traitA是一个不拿所有权的指针,所以经常用作参数里
Box<dyn trait>是一个拥有内部数据所有权的指针,所以经常用在返回值里