爱国的张浩予 发表于 2023-07-22 22:55
Tags:object-safety,trait,polymorphism,dst,fst,dynamic-dispatch,dynamic-sized-type,fixed-sized-type,vtable,fat-pointer,trait-methods
意译解构Object Safety for trait
借助【虚表vtable
】对被调用成员函数【运行时·内存寻址】的作法允许系统编程语言Rust
模仿出OOP
高级计算机语言才具备的【专用·多态Ad-hoc Polymorphism
】特性。
计算机高级语言中的“多态”术语是一个泛指。它通常可被细化为
- 基于继承关系的“子类·多态”
Subtype Polymorphism
— 形状相似而类名不同即是不同。重“名分”轻“事实”。代表语言JAVA
- 基于接口抽象的“专用·多态”
Ad-hoc Polymorphism
— 突出不同类型间的共性,淡化类型差异。- 基于“鸭子类型”的“行·多态”
Row Polymorphism
— 类名不同却形状相似即是相兼容。重“事实”轻“名分”。代表语言JS
因为
Rust
不支持类继承,所以它的多态方式仅收敛于
- 由【
trait Object
+trait method
动态分派】的“专用·多态”- 由【Lens设计模式 + 过程宏】的“行·多态”
仅抛砖引玉,就不再展开了。
就Rust
生产实践而言,这也是“以时间换空间”缩小编译输出二进制文件体积的绝佳手段。其对WEB
汇编技术方向更是意义深远,因为只要性能够用,WASM
体积越小越好。对webapp
来讲,用过剩的性能换取发布包更小的体积还是很划算的。但,rustc
要求凡是参与【专用多态】抽象的trait
都必须Object Safety
。“对象安全”的中文直译非常令人费解。
但结合【专用多态】技术语境,Object Safety
可“啰嗦地”意译表达为:“trait method
调用端不需要对trait
实现类及其实例对象有任何了解与假设,而仅凭trait
描述自身,就能顺利地寻址和执行trait method
,以及获得trait method
执行反馈”。因此,Safety
不是直译的“安全”,而是意译的“不知”。
@Rustacean 也可将Object Safety
精炼地领会为“对象不知”或倒装一下“不知(类型与)对象(就能执行它的成员方法)”。
trait
对象安全的核心原则
【专用多态】抽象要求trait
将其具体实现类以【动态大小类型DST
】的?Sized
形式呈现给trait method
调用端。即,胖指针(= 数据指针 + 虚表指针)
- 在编译时,不锁定数据类型。但因指针大小是固定的,所以编译操作依旧能够成功完成。
- 在运行时,实时度量变量大小,不论它是【堆】变量
Box<dyn Trait>
,还是【栈】变量&dyn Trait
。
以代码语言概括之,trait
和(动态分派)trait method
都必须满足DST
的where Self: ?Sized
限定条件。事实上,where Self: ?Sized
也是rustc
对trait
自身与trait
关联函数的默认限定。
名词解释:
DST
缩写词的全称Dynamic Sized Type
。其含义是“运行时确定大小的数据类型”。所以,它的trait
限定条件是?Sized
。FST
缩写词的全称Fixed Sized Type
。其含义是“编译时确定大小的数据类型”。所以,它的trait
限定条件是Sized
。
对照【泛型类型参数】记忆
对照点一:
- 泛型类型参数默认是
FST
,但可where T: ?Sized
选择退出默认约定 trait
与trait method
缺省都是DST
,但同时也支持where Self: Sized
选择退出初始限定
对照点二:例程1
- 泛型类型参数的
Sized
限定条件是可以被书面重申的,虽然这完全没有必要。 trait
与trait method
定义却不能书面地限定where Self: ?Sized
。这会导致编译失败,因为?Sized
仅能书面地限定泛型类型参数(的形参)。
判断trait
是否对象安全的极简checklist
旧版The Rust Programming Language
教程曾经列举过操作性极强的筛选标准:
trait method
返回值类型不是Self
trait method
不是【泛型函数】
虽至今其仍在互联网上广为流传,但它对知识内核的过度简化极易误导 @Rustacean 认为Object Safe trait
的全部trait method
都必须是【动态分派】的。其实不然,对象安全trait
也被允许包含编译时【静态分派】的成员方法。事实上,只要trait
自身满足Object Safety
基本规则,它的成员方法
- 既可以被收录入
vtable
和参与【动态分派】 — 对trait method
隐式类型参数Self
不做任何限定 - 也能编译时被单态化和参与【静态分派】 — 以
where Self: Sized
限定trait method
隐式类型参数Self
同一个trait
定义动/静两用,没毛病!例程2 走出这个知识点误区有助于避免在业务功能开发过程中频繁地“钻牛角尖”和减轻心智痛苦。
trait
自身对象安全的基本原则
-
trait
定义的隐式类型参数Self
必须是?Sized
的。这也意味着:-
若有
supertrait
,那么supertrait
也必须是?Sized
的,因为trait Trait: Supertrait {}
就是trait Trait where Self: Supertrait {}
的语法糖。例程3// 因为`supertrait`不是`?Sized`,所以该`trait`不是`Object Safety`的。 trait Trait: Sized {} // 等效写法 - trait Trait where Self: Supertrait {} struct S; impl Trait for S {} let obj: Box<dyn Trait> = Box::new(S); // 不可动态分派。
-
若
supertrait
是泛型trait
,那么supertrait
泛型类型参数的实参一定不能是Self
,因为Self
编译时类型不确定和不能作为单态化参数。例程4trait Super<A> {} // 该`trait`不是`Object Safety`的,因为它的隐式类型参数`Self`是`Sized`的。 // - 若抹掉`trait`的`where`从句,那么泛型的【静态分派】会抱怨:“编译时,Self的 // 类型大小未知”。总之,左右为难。 trait Trait: Super<Self> where Self: Sized {} struct S; impl<A> Super<A> for S {} impl Trait for S {} let obj: Box<dyn Trait> = Box::new(S); // 失败,因为`Self: Sized`
-
-
trait
定义不能包含【关联常量】。例程5// 该`trait`不是`Object Safety`的, trait NotObjectSafe { // 因为它包含了【关联常量】 const CONST: i32 = 1; } struct S; impl NotObjectSafe for S {} let obj: Box<dyn NotObjectSafe> = Box::new(S);
-
trait
定义中非成员方法【关联函数】的隐式类型参数Self
必须被显式地限定为Sized
例程6。即,where Self: Sized
。// `trait`不是`Object Safety`,因为 trait NotObjectSafe { // 它的非成员方法关联函数的隐式类型参数`Self`不是`Sized`, // 而是缺省的`?Sized` fn foo() {} } struct S; impl NotObjectSafe for S {} let obj: Box<dyn NotObjectSafe> = Box::new(S); // 编译失败
因为隐式类型参数
Self
的缺省限定条件就是?Sized
,所以 @Rustacean 需要利用where
从句书面地退出初始限定和重置Self
为Sized
的。// `trait`是`Object Safety`,因为 trait NotObjectSafe { // 它的非成员方法关联函数的隐式类型参数`Self`被显式地限定为`Sized`, fn foo() where Self: Sized {} } struct S; impl NotObjectSafe for S {} let obj: Box<dyn NotObjectSafe> = Box::new(S); // 编译成功
至此,若不考虑trait method
,获得一个【对象安全】的trait
并不难。
对象安全trait
的成员方法
【重申强调】即便trait
定义的全部成员方法都不参与【动态分派】(即,与它配对的虚表是空),但只要满足上节罗列的三项条件,该trait
依旧是“对象安全”的。只不过,它的trait Object
没啥实用意义。
静态分派trait method
因为trait
【关联函数】的缺省抽象形式是【动态分派】,所以 @Rustacean 需要显式地将trait method
隐式类型参数Self
限定为Sized
。即,给trait method
声明添加where Self: Sized
限定条件和退出DST
内存布局模式 例程7。然后,你就再也不用担心这些trait method
- 是否是【泛型函数】
- 非
self
形参与返回值类型是否是Self
self
参数数据类型
虽然省心了,但胖指针(堆Box<dyn Trait>
或栈&dyn Trait
)也再点不出这些trait method
了。请仔细阅读下面例程代码中的注释和体会其中的差别。
// 虽然`trait`是`Object Safety`,
trait Trait {
// (1) 但它的`trait method`都是静态分派的,和不能从`Box<dyn Trait>`上被调用
// — `trait method`的隐式类型参数`Self`都被显示地限定为`Sized`的,
// (2) 于是,成员方法的
fn returns(&self) -> Self // a. 返回值类型被允许是`Self`
where Self: Sized;
fn param(&self, other: Self) // b. 非`self`形参也被允许是`Self`数据类型
where Self: Sized {}
fn typed<T>(&self, x: T) // c. 接受【泛型函数】成员方法
where Self: Sized {}
// (3) 非成员方法的关联函数必须是静态分派的
fn foo()
where Self: Sized {} // 手工限定其是静态分派函数
}
struct S;
impl Trait for S {
fn returns(&self) -> Self where Self: Sized { S {field: 10} }
}
// 虽然`trait`是`Object Safety`,但
let obj: Box<dyn Trait> = Box::new(S {field: 12});
// (1) 它没有可运行时寻址调用的成员方法。
// obj.returns(); // 失败,因为 where Self: Sized
// (2) 它的`trait method`都必须从实现类的实例对象上被调用
<S as Trait>::foo();
let obj = S {field: 13};
obj.returns();
obj.typed(1);
对象安全的动态分派trait method
虽然【动态分派】是全部trait method
的“天赋技能”,但 @Rustacean 也有义务从编程环节确保trait method
不依赖于trait
实现类的任何【元信息】。即,trait method
函数体对trait
实现类的类型信息不知。
“不知”即是“安全”。“对象安全”还真不如意译为“对象不知”。这多有趣呀!
在书面代码上,@Rustacean 仅需要做到在trait method
定义中,
- 不出现【泛型类型参数】 例程8。例外,【泛型生命周期参数】还是被允许的。例程9
- 非
self
形参与返回值类型不能是Self
。关键字Self
代指trait
实现类,但Object safe trait
需要对实现类不知。 self
形参的数据类型必须是如下六种之一 例程10- 只读引用
&Self / &self
- 可修改引用
&mut Self / &mut self
- 智能指针
Box<Self>
- 引用计数
Rc<Self>
- 原子引用计数
Arc<Self>
- 不可
swap
内存Pin<P>
。其中,泛型类型参数P
可以是前五种类型中的任意一种。
- 只读引用
- 千万别限定
trait method
的隐式类型参数Self
为Sized
。
条条框框还是比较多的,可得常记频用,才可应用自如。
对象安全trait
的非成员方法关联函数
这类associated functions
概念对等于Typescript
的静态成员方法。“静态”意味着这类关联函数一定不会参与动态分派,但出于未知原因rustc
依旧偏好将其收录虚表vtable
和造成trait Object
实例化失败。所以,Object safe trait
的重要原则之一,就是:
- 要么,没有非成员方法关联函数
- 要么,显式地书面限定每个非成员方法关联函数的隐式类型参数
Self
为Sized
。例程11
否则,编译失败。
// `trait Trait`不是对象安全的,
trait Trait {
// 因为它的非成员方法关联函数不可动态分派,但还被收录`vtable`
fn foo() {} // 给加添加`where Self: Sized`限定条件,可解编译失败
}
struct S;
impl Trait for S {}
let obj: Box<dyn Trait> = Box::new(S);
结束语
【动态分派】是trait
和trait method
初始开启的天赋技能。除了性能极客,@Rustacean 一般想不起刻意地对定它们做静态化处理。但,由于项目历史包袱,在旧trait
定义内遗留的
- 泛型函数
Self
滥用- 非成员方法关联函数
导致其不再“对象安全”。咱们既不必埋怨旧代码作者(哎!谁的认知不是逐步深化的呀),也别慌,更别像我一样傻乎乎地立即重构代码(很伤的)。而仅只需要将仅能静态分派关联函数的隐式类型参数Self
限定为Sized
即可。只要虚表不再收录它们,rustc
就不会抱怨了。于是,“同一个trait
既兼容于新/旧代码,还动/静两用”岂不美哉 例程12!可是不值得炫技,因为大量这类trait
代码是馁馁的后期维护心智灾难 — 只能算是变通的“歪招”。
这次分享的内容就是这些。创作不易,希望路过的神仙哥哥、仙女妹妹们评论、点赞、转发呀!相信我在【WEB
汇编】技术方向Rust
栈至今都是最优选择。
评论区
写评论where Self: Sized是点睛之笔,好文章,直达究竟。 object safety可否理解为object agnostic ?
大神,你又有新作了! 支持你!
这个很清楚的写了Trait Object的一些细节
期待有时间讲一下 Any Trait
写的有深度,值得好好研读
受益匪浅,多种语言的多态分析很到位,使我对rust的多态有了更为深刻的理解。高实在是高。👍🏻👍🏻
大佬写得太好了!以我的水平,还只能看得似懂非懂的。
写的非常棒,言简意赅,通俗易懂。大神,请收下我的膝盖。