rust中trait使用方法 - 2

上一篇文章讲述了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中的类型参数与关联类型的区别

  1. 类型参数可以在impl类型的时候具化,也可以延迟到使用的时候具化。而关联类型在呗impl时就必须具化。

  2. 由于类型参数和trait名一起组成了完整的trait名字,不同的具化类型会构成不同的trait,所以看起来同一个定义可以再目标类型上多次实现。而关联类型不能。

    rust 复制代码
    use 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
    }

    这个示例展示了几个要点

    1. 定义带类型参数的trait时可以使用where表达提供约束
    2. impl trait时可以对类型参数加强约束
    3. impl trait时可以不具化类型参数
    4. 可以在使用方法时具化类型参数

这么看来,类型参数比关联类型更加强大,但关联类型也有它的优点,比如管理按类型没有类型参数,不存在多引入了一个参数的问题,而类型参数是具有传染性的,特别是在一个调用层次很深的系统中,增删一个类型参数可能会导致整个项目文件到处都需要修改。

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。

规则比较复杂,可以简单记住几个场景

  1. 不要在trait里面定义构造函数,比如new这种返回Self的关联函数。确实在整个Rust生态中都没有将构造函数定义在trait中的习惯
  2. trait里面尽量定义传引用&self或&mut self的方法,而不要定义传值self的方法

&dyn traitA是一个不拿所有权的指针,所以经常用作参数里

Box<dyn trait>是一个拥有内部数据所有权的指针,所以经常用在返回值里

相关推荐
假装我不帅1 小时前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
神仙别闹1 小时前
基于ASP.NET+SQL Server实现简单小说网站(包括PC版本和移动版本)
后端·asp.net
计算机-秋大田2 小时前
基于Spring Boot的船舶监造系统的设计与实现,LW+源码+讲解
java·论文阅读·spring boot·后端·vue
货拉拉技术2 小时前
货拉拉-实时对账系统(算盘平台)
后端
掘金酱2 小时前
✍【瓜分额外奖金】11月金石计划附加挑战赛-活动命题发布
人工智能·后端
代码之光_19803 小时前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi3 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
颜淡慕潇3 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
尘浮生4 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea