缘起

一切都要从年末换工作碰上特殊时期, 在家闲着无聊又读了几首诗, 突然想写一个可以浏览和背诵诗词的 TUI 程序说起. 我选择了 Cursive 这个 Rust TUI 库. 在实现时有这么一个函数, 它会根据参数的不同返回某个组件(如 Button, TextView 等). 在 Cursive 中, 每个组件都实现了 View 这个 trait, 最初这个函数只会返回某个确定的组件, 所以函数签名可以这样写

fn some_fn(param: SomeType) -> Button

随着开发进度增加, 这个函数需要返回 Button, TextView 等组件中的一个, 我下意识地写出了类似于下面的代码

fn some_fn(param1: i32, param2: i32) -> impl View {if param1 > param2 {// do something...return Button {};} else {// do something...return TextView {};}
}

可惜 Rust 编译器一如既往地打脸, Rust 编译器报错如下

--> srcmain.rs:19:16|
13 | fn some_fn(param1: i32, param2: i32) -> impl View {|                                         --------- expected because this return type...
...
16 |         return Button {};|                --------- ...is found to be `Button` here
...
19 |         return TextView {};|                ^^^^^^^^^^^ expected struct `Button`, found struct `TextView`error: aborting due to previous errorFor more information about this error, try `rustc --explain E0308`.

从编译器报错信息看函数返回值虽然是 impl View 但其从 if 分支推断返回值类型为 Button 就不再接受 else 分支返回的 TextView. 这与 Rust 要求 if else 两个分支的返回值类型相同的特性一致. 那能不能让函数返回多种类型呢? Rust 之所以要求函数不能返回多种类型是因为 Rust 在需要在 编译期确定返回值占用的内存大小, 显然不同类型的返回值其内存大小不一定相同. 既然如此, 把返回值装箱, 返回一个胖指针, 这样我们的返回值大小可以确定了, 这样也许就可以了吧. 尝试把函数修改成如下形式:

fn some_fn(param1: i32, param2: i32) -> Box<View> {if param1 > param2 {// do something...return Box::new(Button {});} else {// do something...return Box::new(TextView {});}
}

现在代码通过编译了, 但如果使用 Rust 2018, 你会发现编译器会抛出警告:

warning: trait objects without an explicit `dyn` are deprecated--> srcmain.rs:13:45|
13 | fn some_fn(param1: i32, param2: i32) -> Box<View> {|                                             ^^^^ help: use `dyn`: `dyn View`|= note: `#[warn(bare_trait_objects)]` on by default

编译器告诉我们使用 trait object 时不使用 dyn 的形式已经被废弃了, 并且还贴心的提示我们把 Box<View> 改成 Box<dyn View>, 按编译器的提示修改代码, 此时代码 no warning, no error, 完美.

impl TraitBox<dyn Trait> 除了允许多种返回值类型的之外还有什么区别吗? trait object 又是什么? 为什么 Box<Trait> 形式的返回值会被废弃而引入了新的 dyn 关键字呢?

埋坑

impl Traitdyn Trait 在 Rust 分别被称为静态分发和动态分发. 在第一版的 Rust Book 这样解释分发(dispatch)

When code involves polymorphism, there needs to be a mechanism to determine which specific version is actually run. This is called ‘dispatch’. There are two major forms of dispatch: static dispatch and dynamic dispatch. While Rust favors static dispatch, it also supports dynamic dispatch through a mechanism called ‘trait objects’.

即当代码涉及多态时, 需要某种机制决定实际调用类型. Rust 的 Trait 可以看作某些具有通过特性类型的集合, 以上面代码为例, 在写代码时我们不关心具体类型, 但在编译或运行时必须确定 Button 还是 TextView. 静态分发, 正如静态类型语言的"静态"一词说明的, 在编译期就确定了具体调用类型. Rust 编译器会通过单态化(Monomorphization) 将泛型函数展开.

假设 FooBar 都实现了 Noop 特性, Rust 会把函数

fn x(...) -> impl Noop

展开为

fn x_for_foo(...) -> Foo
fn x_for_bar(...) -> Bar

(仅作原理说明, 不保证编译会这样展开函数名).

通过单态化, 编译器消除了泛型, 而且没有性能损耗, 这也是 Rust 提倡的形式, 缺点是过多展开可能会导致编译生成的二级制文件体积过大, 这时候可能需要重构代码.

静态分发虽然有很高的性能, 但在文章开头其另一个缺点也有所体现, 那就是无法让函数返回多种类型, 因此 Rust 也支持通过 trait object 实现动态分发. 既然 Trait 是具有某种特性的类型的集合, 那我们可以把 Trait 也看作某种类型, 但它是"抽象的", 就像 OOP 中的抽象类或基类, 不能直接实例化.

Rust 的 trait object 使用了与 c++ 类似的 vtable 实现, trait object 含有1个指向实际类型的 data 指针, 和一个指向实际类型实现 trait 函数的 vtable, 以此实现动态分发. 更加详细的介绍可以在

Exploring Dynamic Dispatch in Rust​alschwalm.com

看到. 既然 trait object 在实现时可以确定大小, 那为什么不用 fn x() -> Trait 的形式呢? 虽然 trait object 在实现上可以确定大小, 但在逻辑上, 因为 Trait 代表类型的集合, 其大小无法确定. 允许 fn x() -> Trait 会导致语义上的不和谐. 那 fn x() -> &Trait 呢? 当然可以! 但鉴于这种场景下都是在函数中创建然后返回该值的引用, 显然需要加上生命周期:

fn some_fn(param1: i32, param2: i32) -> &'static View {if param1 > param2 {// do something...return &Button {};} else {// do something...return &TextView {};}
}

我不喜欢添加额外的生命周期说明, 想必各位也一样. 所以我们可以用拥有所有权的 Box 智能指针避免烦人的生命周期说明. 至此 Box<Trait> 终于出现了. 那么问题来了, 为什么编译器会提示 Box<Trait> 会被废弃, 特地引入了 dyn 关键字呢? 答案可以在 RFC-2113 中找到.

RFC-2113 明确说明了引入 dyn 的原因, 即语义模糊, 令人困惑, 原因在于没有 dyn 让 Trait 和 trait objects 看起来完全一样, RFC 列举了3个例子说明.

第一个例子, 加入你看到下面的代码, 你知道作者要干什么吗?

impl SomeTrait for AnotherTrait impl<T> SomeTrait for T where T: Another

你看懂了吗? 说实话我也看不懂 : ) PASS

第二个例子, impl MyTrait {} 是正确的语法, 不过这样会让人以为这会在 Trait 上添加默认实现, 扩展方法或其他 Trait 自身的一些操作. 实际上这是在 trait object 上添加方法.

如在下面代码说明的, Trait 默认实现的正确定义方法是在定义 Trait 时指定, 而不应该在 impl Trait {} 语句块中.

trait Foo {fn default_impl(&self) {println!("correct impl!");}
}impl Foo {fn trait_object() {println!("trait object impl");}
}struct Bar {}impl Foo for Bar {}fn main() {let b = Bar{};b.default_impl();// b.trait_object();Foo::trait_object();
}

Bar 在实现了 Foo 后可以通过 b.default_impl 调用, 无需额外实现, 但 b.trait_object 则不行, 因为 trait_object 方法是 Foo 的 trait object 上的方法.

如果是 Rust 2018 编译器应该还会显示一条警告, 告诉我们应该使用 impl dyn Foo {}

第三个例子则以函数类型和函数 trait 作对比, 两者差别只在于首字母是否大写(Fn代表函数trait object, fn则是函数类型), 难免会把两者弄混.

更加详细的说明可以移步

RFC-2113​github.com

.

总结

impl traitdyn trait 区别在于静态分发于动态分发, 静态分发性能 好, 但大量使用有可能造成二进制文件膨胀; 动态分发以 trait object 的概念通过虚表实现, 会带来一些运行时开销. 又因 trait object 与 Trait 在不引入 dyn 的情况下经常导致语义混淆, 所以 Rust 特地引入 dyn 关键字, 在 Rust 2018 中已经稳定.

引用

以下是本文参考的资料

impl Trait for returning complex types with ease​doc.rust-lang.orgimpl trait 社区跟踪​github.comrust-lang/rfcs​github.com

Traits and Trait Objects in Rust​joshleeb.comDynamic vs. Static Dispatch​lukasatkinson.deExploring Dynamic Dispatch in Rust​alschwalm.com

PS: 题图为卢浦大桥, 全上海我最喜欢的大桥, 没有之一~

rust为什么显示不了国服_捋捋 Rust 中的 impl Trait 和 dyn Trait相关推荐

  1. 【Rust投稿】捋捋 Rust 中的 impl Trait 和 dyn Trait

    本文来自 PrivateRookie 的知乎投稿:https://zhuanlan.zhihu.com/p/109990547 缘起 一切都要从年末换工作碰上特殊时期, 在家闲着无聊又读了几首诗, 突 ...

  2. rust为什么显示不了国服_Rust编程语言初探

    静态.强类型而又不带垃圾收集的编程语言领域内,很久没有新加入者参与竞争了,大概大部分开发者认为传统的C/C++的思路已经不太适合新时代的编程需求,即便有Ken Tompson这样的大神参与设计的gol ...

  3. rust为什么显示不了国服_AWS偏爱Rust,已将Rust编译器团队负责人收入囊中

    机器之心报道 作者:张倩.杜伟 近日,AWS 透露,该公司已经聘用了 Rust 编译器团队负责人之一 Felix Klock.该消息出自 AWS 开源团队于上周二发布的一篇文章<Why AWS ...

  4. Rust impl Trait和dyn Trait

    impl Trait:静态分发 dyn Trait:动态分发 静态分发:在编译期就确定了具体返回类型,函数只能返回一种类型. 动态分发:在运行时才能确定具体返回类型,函数可以返回多种类型. Trait ...

  5. simulink显示多个数据_如何在 Simulink 中使用 PID Tuner 进行 PID 调参?

    作者 | 安布奇责编 | 胡雪蕊出品 | CSDN(ID: CSDNnews)本文为一篇技术干货,主要讲述在Simulink如何使用PID Tuner进行PID调参. PID调参器( PIDTuner ...

  6. excel整列动态联动下拉_在Excel下拉列表中显示多列

    excel整列动态联动下拉 A data validation drop down list in Excel only shows one column of items. See how to s ...

  7. 表格在整个html居中显示,html 表格字符居中显示_如何在HTML中居中显示表格?

    html 表格字符居中显示_如何在HTML中居中显示表格? html 表格字符居中显示_如何在HTML中居中显示表格? html 表格字符居中显示 HTML table provides the ab ...

  8. vb只显示两位小数_【名师课堂】苏教数学五年级上3.1小数的意义

    微课视频第一课时 微课视频第二课时 电子课本 同步练习 参考答案 教学设计 小数的意义和读.写方法教材第30~32页的内容.1.使学生理解小数的意义.2.结合具体情境教学小数的意义,让学生初步认识小数 ...

  9. python中显示第三行数据_在Python中Dataframe通过print输出多行时显示省略号的实例...

    笔者使用python进行数据分析时,通过print输出dataframe中的数据,当dataframe行数很多时,中间部分显示省略号,如下图所示: 0 项华祥 1 何炅 2 张艺飞 3 李仁港 4 崔 ...

最新文章

  1. RGBD相机模型与图片处理
  2. stm32难不难学,没学51单片机可以直接学STM32吗?
  3. 当Tomcat遇上Netty,我这一系列神操作,同事看了拍手叫绝
  4. boost::ratio_abs相关的测试程序
  5. 做diff_Virtual Dom amp;amp; Diff原理,极简版
  6. 【ArcGIS风暴】ArcGIS栅格数据(分区)统计方法总结
  7. 洛谷 P3320: bzoj 3991: LOJ 2182: [SDOI2015]寻宝游戏
  8. mapreduce引用第三方jar
  9. 今日恐慌与贪婪指数为77 贪婪程度有所缓解
  10. mysql导vertica_vertica系列:数据的导入导出
  11. C++程序调用python3
  12. java线程中yield(),sleep(),wait()区别详解
  13. 【企业】任正非:管理的灰度
  14. 移动运营商ipcc文件_教你如何在苹果官网提取IPCC文件
  15. Ardunio开发:esp32—cam摄像头
  16. 黑苹果升级更新macOS 13 Ventura 问题整理
  17. python爬虫实战--爬取猫眼专业版-实时票房
  18. 阿里M8级铁子整理出SQL手册:收获不止SQL优化,抓住SQL的本质
  19. 计算机网络 第3章 作业1
  20. 设备管理器设置了不允许鼠标唤醒电脑,但是鼠标还是会唤醒电脑的解决方法

热门文章

  1. spring javafx_Oracle Spring Clean JavaFX应该吗?
  2. apache mesos_Apache Mesos:编写自己的分布式框架
  3. jsr303 自定义消息_JSR 303从I18N属性文件加载消息
  4. jvm能识别什么字符集_识别JVM –比预期要难
  5. javafx 调用接口_JavaFX技巧3:使用回调接口
  6. 通过Okta的单点登录保护Spring Boot Web App的安全
  7. 非静态方法可以访问Java中的静态变量/方法吗?
  8. javafx简单吗_JavaFX即将推出您附近的Android或iOS设备吗?
  9. 枚举:如何正确使用name()和toString()方法
  10. 使用UAA OAuth2授权服务器–客户端和资源