一切从年底换工作,遇到疫情开始。在家无聊,读了几首诗。突然想写一个可以浏览和朗诵诗歌的TUI程序。我选择了RustTUI库Cursive。实现时有这样一个函数,它会根据不同的参数返回某个组件(如Button、TextView等)。在Cursive中,每个组件都实现了View特性。一开始这个函数只会返回某个组件,所以函数签名可以这样写.我下意识地写了一段类似于下面的代码}else{//做点什么...returnTextView{};啪啪打脸,Rust编译器报错如下-->src\main.rs:19:16|13|fnsome_fn(param1:i32,param2:i32)->implView{|----------预期,因为此返回类型......16|返回按钮{};|----------...在这里发现是`Button`...19|返回文本视图{};^^^^^^^^^^^期望结构`Button`,发现结构`TextView`错误:由于先前的原因而中止s错误有关此错误的更多信息,请尝试`rustc--explainE0308`。从编译器报错信息来看,虽然函数的返回值是implView,但是从if分支推断返回值类型是Button,所以不再接受else分支返回的TextView。这与Rust要求ifelse的两个分支的返回值类型相同是一致的。函数可以返回多种类型吗?Rust之所以要求函数不能返回多种类型,是因为Rust需要在编译时确定返回值所占用的内存大小。显然,不同类型的返回值占用的内存大小不一定相同。这样的话,把返回值装箱,返回一个胖指针,这样我们的返回值的大小就可以确定了,所以也许就可以了。尝试将函数更改为如下所示:fnsome_fn(param1:i32,param2:i32)->Box{ifparam1>param2{//dosomething...returnBox::new(Button{});}else{//做点什么...returnBox::new(TextView{});}}现在代码编译通过了,但是如果你使用Rust2018,你会发现编译器会抛出一个警告:warning:traitobjectswithoutanexplicit`dyn`aredeprecated-->src\main.rs:13:45|13|fnsome_fn(param1:i32,param2:i32)->Box{|^^^^帮助:使用`dyn`:`dynView`|=note:`#[warn(bare_trait_objects)]`onbydefault编译器告诉我们使用trait对象时不使用dyn的形式已经被废弃,还贴心提醒我们将Box改为Box,根据编译器的提示修改代码。此时代码没有警告,没有报错,完美。但是,implTrait和Box允许多个返回值类型还有其他区别吗?什么是特征对象?为什么不推荐使用Box形式的返回值并引入新的dyn关键字?ImplTrait和dynTrait在RustDispatch和dynamicdispatch中分别被称为static。在RustBook的第一版中,对此进行了解释。当代码涉及多态性时,需要有一种机制来确定实际运行的是哪个特定版本。这称为“派遣”。调度主要有两种形式:静态调度和动态调度。虽然Rust支持静态调度,但它也通过称为“特征对象”的机制支持动态调度。也就是说,当代码涉及多态时,需要某种机制来确定实际的调用类型。Rust的Trait可以认为是具有传递特性的某些类型的集合。以上面的代码为例,我们在写代码的时候并不关心具体的类型,但是在编译或者运行的时候一定要判断是Button还是TextView。静态分发,就像静态类型语言中的“static”一样,顾名思义,具体的调用类型是在编译时确定的。Rust编译器会通过Monomorphization来扩展泛型函数。假设Foo和Bar都实现了Noop特性,Rust会将函数fnx(...)->implNoop展开为fnx_for_foo(...)->Foofnx_for_bar(...)->Bar(just原理解释,不保证编译器会这样扩充功能几个名字)。通过单态化,编译器消除了泛型,没有性能损失。这也是Rust提倡的形式。缺点是展开过多可能会导致编译生成的二进制文件过大。这个时候,可能代码需要重构。静态分发虽然性能高,但是文章开头也体现了另一个缺点,就是函数不能返回多种类型,所以Rust也支持通过trait对象进行动态分发。既然Trait是具有一定特征的类型的集合,那么我们可以把Trait看作是某种类型,但它是“抽象的”,就像OOP中的抽象类或基类一样,不能直接实例化。Rust的trait对象使用一个类似于c++的vtable实现来实现。trait对象包含一个指向实际类型的数据指针,和一个指向实现trait函数的实际类型的vtable,从而实现动态分配。更详细的介绍可以参考ExploringDynamicDispatchinRust。既然trait对象的大小在实现的时候就可以确定,为什么不使用fnx()->Trait的形式呢?虽然Trait对象的大小在实现上是可以确定的,但是从逻辑上讲,因为Trait代表了一个类型的集合,所以它的大小是不能确定的。允许fnx()->Trait会导致语义不一致。fnx()->&Trait怎么样?当然!显然需要添加生命周期:fnsome_fn(param1:i32,param2:i32)->&'staticView{ifparam1>param2{//dosomething...return&Button{};}else{//做点什么。..返回&TextView{};}}我不喜欢添加额外的生命周期说明,我想你也喜欢。所以我们可以使用带所有权的Box智能指针来避免烦人的生命周期指令。于是Box终于出现了。那么问题来了,为什么编译器会提示Boxwillbedeprecated,特地引入了dyn关键字?答案可以在RFC-2113中找到。RFC-2113明确解释了引入dyn的原因,即语义模糊不清。原因是因为没有dyn让Trait和trait对象看起来完全一样,RFC列举了3个例子来说明。第一个例子,添加你看到下面的代码,你知道作者要做什么吗?implSomeTraitforAnotherTraitimplSomeTraitforTwhereT:Another明白了吗?老实说,我也看不懂:)PASS第二个例子,implMyTrait{}是正确的语法,但这会让人认为这会在Trait上添加默认实现,扩展方法或其他一些操作特质本身。其实这是在trait对象上添加一个方法。如下代码说明,Trait默认实现的正确定义方法是在定义Trait时指定,而不是在implTrait{}语句中block.traitFoo{fndefault_impl(&self){println!("correctimpl!");}}implFoo{fntrait_object(){println!("traitobjectimpl");}}structBar{}implFooforBar{}fnmain(){letb=Bar{};b.default_impl();//b.trait_object();Foo::trait_object();}Bar可以传b。default_impl调用没有额外的实现,但是b.trait_object不起作用,因为trait_object方法是Foo的特征对象上的一个方法。如果是Rust2018,编译器应该也会显示一个警告,告诉我们应该使用impldynFoo{}第三个这个例子比较了函数类型和函数特征,两者的区别仅在于不管首字母大写与否(Fn代表函数trait对象,fn是函数类型),两者难免混淆。有关更详细的说明,您可以转到RFC-2113。总结一下impltrait和dyntrait的区别就是静态分发是动态分发,静态分发性能好,但是大量使用可能会导致二进制文件膨胀;动态分配是通过具有traitobject概念的虚表来实现的,会带来一定的运行时开销。并且由于trait对象和Trait没有引入dyn,往往会导致语义混淆,所以Rust特地引入了dyn关键字,在Rust2018中已经稳定下来。以下是本文Rust版指南impltrait社区追踪RFC的参考资料-2113Trait和TraitObjectDynamicvsStaticDispatch探索Rust中的动态调度