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>是一个拥有内部数据所有权的指针,所以经常用在返回值里

相关推荐
代码对我眨眼睛15 分钟前
springboot从分层到解耦
spring boot·后端
The Straggling Crow24 分钟前
go 战略
开发语言·后端·golang
ai安歌30 分钟前
【JavaWeb】利用IDEA2024+tomcat10配置web6.0版本搭建JavaWeb开发项目
java·开发语言·后端·tomcat·web·intellij idea
尘浮生43 分钟前
Java项目实战II基于Java+Spring Boot+MySQL的作业管理系统设计与实现(源码+数据库+文档)
java·开发语言·数据库·spring boot·后端·mysql·spring
程序员阿鹏2 小时前
ArrayList 与 LinkedList 的区别?
java·开发语言·后端·eclipse·intellij-idea
java_heartLake3 小时前
微服务中间件之Nacos
后端·中间件·nacos·架构
GoFly开发者4 小时前
GoFly快速开发框架/Go语言封装的图像相似性比较插件使用说明
开发语言·后端·golang
苹果酱05674 小时前
通过springcloud gateway优雅的进行springcloud oauth2认证和权限控制
java·开发语言·spring boot·后端·中间件
豌豆花下猫5 小时前
Python 潮流周刊#70:微软 Excel 中的 Python 正式发布!(摘要)
后端·python·ai
芯冰乐6 小时前
综合时如何计算net delay?
后端·fpga开发