当前位置:网站首页>【Rust投稿】捋捋 Rust 中的 impl Trait 和 dyn Trait
【Rust投稿】捋捋 Rust 中的 impl Trait 和 dyn Trait
2022-06-25 03:34:00 【51CTO】
PrivateRookie --作者
缘起
一切都要从年末换工作碰上特殊时期, 在家闲着无聊又读了几首诗, 突然想写一个可以浏览和背诵诗词的 TUI 程序说起. 我选择了 Cursive 这个 Rust TUI 库. 在实现时有这么一个函数, 它会根据参数的不同返回某个组件(如 Button, TextView 等). 在 Cursive 中, 每个组件都实现了 View 这个 trait, 最初这个函数只会返回某个确定的组件, 所以函数签名可以这样写
随着开发进度增加, 这个函数需要返回 Button, TextView 等组件中的一个, 我下意识地写出了类似于下面的代码
可惜 Rust 编译器一如既往地打脸, Rust 编译器报错如下
--> src\main.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 error
For more information about this error, try `rustc --explain E0308`.
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
从编译器报错信息看函数返回值虽然是 impl View 但其从 if 分支推断返回值类型为 Button 就不再接受 else 分支返回的 TextView. 这与 Rust 要求 if else 两个分支的返回值类型相同的特性一致. 那能不能让函数返回多种类型呢? Rust 之所以要求函数不能返回多种类型是因为 Rust 在需要在 编译期确定返回值占用的内存大小, 显然不同类型的返回值其内存大小不一定相同. 既然如此, 把返回值装箱, 返回一个胖指针, 这样我们的返回值大小可以确定了, 这样也许就可以了吧. 尝试把函数修改成如下形式:
现在代码通过编译了, 但如果使用 Rust 2018, 你会发现编译器会抛出警告:
编译器告诉我们使用 trait object 时不使用 dyn 的形式已经被废弃了, 并且还贴心的提示我们把 Box<View> 改成 Box<dyn View>, 按编译器的提示修改代码, 此时代码 no warning, no error, 完美.
但 impl Trait 和 Box<dyn Trait> 除了允许多种返回值类型的之外还有什么区别吗? trait object 又是什么? 为什么 Box<Trait> 形式的返回值会被废弃而引入了新的 dyn 关键字呢?
埋坑
impl Trait 和 dyn 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) 将泛型函数展开.
假设 Foo 和 Bar 都实现了 Noop 特性, Rust 会把函数
展开为
(仅作原理说明, 不保证编译会这样展开函数名).
通过单态化, 编译器消除了泛型, 而且没有性能损耗, 这也是 Rust 提倡的形式, 缺点是过多展开可能会导致编译生成的二级制文件体积过大, 这时候可能需要重构代码.
静态分发虽然有很高的性能, 但在文章开头其另一个缺点也有所体现, 那就是无法让函数返回多种类型, 因此 Rust 也支持通过 trait object 实现动态分发. 既然 Trait 是具有某种特性的类型的集合, 那我们可以把 Trait 也看作某种类型, 但它是"抽象的", 就像 OOP 中的抽象类或基类, 不能直接实例化.
Rust 的 trait object 使用了与 c++ 类似的 vtable 实现, trait object 含有1个指向实际类型的 data 指针, 和一个指向实际类型实现 trait 函数的 vtable, 以此实现动态分发. 更加详细的介绍可以在
Exploring Dynamic Dispatch in Rustalschwalm.com
看到. 既然 trait object 在实现时可以确定大小, 那为什么不用 fn x() -> Trait 的形式呢? 虽然 trait object 在实现上可以确定大小, 但在逻辑上, 因为 Trait 代表类型的集合, 其大小无法确定. 允许 fn x() -> Trait 会导致语义上的不和谐. 那 fn x() -> &Trait 呢? 当然可以! 但鉴于这种场景下都是在函数中创建然后返回该值的引用, 显然需要加上生命周期:
我不喜欢添加额外的生命周期说明, 想必各位也一样. 所以我们可以用拥有所有权的 Box 智能指针避免烦人的生命周期说明. 至此 Box<Trait> 终于出现了. 那么问题来了, 为什么编译器会提示 Box<Trait> 会被废弃, 特地引入了 dyn 关键字呢? 答案可以在 RFC-2113 中找到.
RFC-2113 明确说明了引入 dyn 的原因, 即语义模糊, 令人困惑, 原因在于没有 dyn 让 Trait 和 trait objects 看起来完全一样, RFC 列举了3个例子说明.
第一个例子, 加入你看到下面的代码, 你知道作者要干什么吗?
你看懂了吗? 说实话我也看不懂 : ) PASS
第二个例子, impl MyTrait {} 是正确的语法, 不过这样会让人以为这会在 Trait 上添加默认实现, 扩展方法或其他 Trait 自身的一些操作. 实际上这是在 trait object 上添加方法.
如在下面代码说明的, Trait 默认实现的正确定义方法是在定义 Trait 时指定, 而不应该在 impl Trait {} 语句块中.
Bar 在实现了 Foo 后可以通过 b.default_impl 调用, 无需额外实现, 但 b.trait_object 则不行, 因为 trait_object 方法是 Foo 的 trait object 上的方法.
如果是 Rust 2018 编译器应该还会显示一条警告, 告诉我们应该使用 impl dyn Foo {}
第三个例子则以函数类型和函数 trait 作对比, 两者差别只在于首字母是否大写(Fn代表函数trait object, fn则是函数类型), 难免会把两者弄混.
更加详细的说明可以移步
RFC-2113github.com
.
总结
impl trait 和 dyn trait 区别在于静态分发于动态分发, 静态分发性能 好, 但大量使用有可能造成二进制文件膨胀; 动态分发以 trait object 的概念通过虚表实现, 会带来一些运行时开销. 又因 trait object 与 Trait 在不引入 dyn 的情况下经常导致语义混淆, 所以 Rust 特地引入 dyn 关键字, 在 Rust 2018 中已经稳定.
引用
以下是本文参考的资料
impl Trait for returning complex types with easedoc.rust-lang.org
impl trait 社区跟踪github.com
rust-lang/rfcsgithub.com
Traits and Trait Objects in Rustjoshleeb.comDynamic vs. Static Dispatchlukasatkinson.deExploring Dynamic Dispatch in Rustalschwalm.com
PS: 题图为卢浦大桥, 全上海我最喜欢的大桥, 没有之一~
边栏推荐
- [FPGA] serial port controls temperature acquisition by command
- ICML 2022 | 字节跳动 AI Lab 提出多模态模型:X-VLM,学习视觉和语言的多粒度对齐...
- 2022年海外电商运营三大关键讲解
- 现在,耳朵也要进入元宇宙了
- 亚马逊在中国的另一面
- 马斯克:推特要学习微信,让10亿人「活在上面」成为超级APP
- 孙武玩《魔兽》?有图有真相
- 马斯克被诉传销索赔2580亿美元,台积电公布2nm制程,中科院发现月壤中含有羟基形式的水,今日更多大新闻在此...
- 香蕉为什么能做随机数生成器?因为,它是水果界的“辐射之王”
- 騰訊開源項目「應龍」成Apache頂級項目:前身長期服務微信支付,能hold住百萬億級數據流處理...
猜你喜欢

Redis related-02

Seata四大模式之TCC模式详解及代码实现

A new generation of cascadable Ethernet Remote i/o data acquisition module

Two common OEE monitoring methods for equipment utilization

Collaboration + Security + storage, cloud box helps Shenzhen edetai restructure its data center

Apple's legendary design team disbanded after jobs refused to obey cook

陆奇首次出手投资量子计算

The era of copilot free is over! Student party and defenders of popular open source projects can prostitute for nothing

AI writes its own code to let agents evolve! The big model of openai has the flavor of "human thought"

中国天眼发现地外文明可疑信号,马斯克称星舰7月开始轨道试飞,网信办:APP不得强制要求用户同意处理个人信息,今日更多大新闻在此...
随机推荐
Why can banana be a random number generator? Because it is the "king of radiation" in the fruit industry
The programmer reality show is coming again! Hulan, as the host, carried the lamp to fill the knowledge. The SSS boss had a bachelor's degree in pharmacy. Zhu Jun and Zhang Min from Tsinghua joined th
陆奇首次出手投资量子计算
用指南针开户如何选择证券公司?哪一个是更安全的
MCN institutions are blooming everywhere: bloggers and authors should sign contracts carefully, and the industry is very deep
站在风暴中心:如何给飞奔中的腾讯更换引擎
TensorFlow,危!抛弃者正是谷歌自己
現在,耳朵也要進入元宇宙了
How far is the memory computing integrated chip from popularization? Listen to what practitioners say | collision school x post friction intelligence
指南针在上面开户安全吗?靠谱吗?
Redis related-02
Introduction to database system
X86 CPU, critical! The latest vulnerability has caused heated discussion. Hackers can remotely steal keys. Intel "all processors" are affected
Three key explanations of overseas e-commerce operation in 2022
马斯克被诉传销索赔2580亿美元,台积电公布2nm制程,中科院发现月壤中含有羟基形式的水,今日更多大新闻在此...
ACM. Hj75 common substring calculation ●●
Cloud native database vs traditional database
Does it count as staying up late to sleep at 2:00 and get up at 10:00? Unless you can do it every day
亚马逊在中国的另一面
Program. Launch (xxx) open file