【Rust笔记】意译解构 Object Safety for trait

news2025/1/12 12:57:44

意译解构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都必须满足DSTwhere Self: ?Sized限定条件。事实上,where Self: ?Sized也是rustctrait自身与trait关联函数的默认限定

名词解释:

  • DST缩写词的全称Dynamic Sized Type。其含义是“运行时确定大小的数据类型”。所以,它的trait限定条件是?Sized

  • FST缩写词的全称Fixed Sized Type。其含义是“编译时确定大小的数据类型”。所以,它的trait限定条件是Sized

对照【泛型类型参数】记忆

对照点一:

  • 泛型类型参数默认是FST,但可where T: ?Sized选择退出默认约定

  • traittrait method缺省都是DST,但同时也支持where Self: Sized选择退出初始限定

对照点二:例程1

  • 泛型类型参数的Sized限定条件是可以被书面重申的,虽然这完全没有必要。

  • traittrait method定义却不能书面地限定where Self: ?Sized。这会导致编译失败,因为?Sized仅能书面地限定泛型类型参数(的形参)。

判断trait是否对象安全的极简checklist

旧版The Rust Programming Language教程曾经列举过操作性极强的筛选标准:

  1. trait method返回值类型不是Self

  2. 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自身对象安全的基本原则

  1. trait定义的隐式类型参数Self必须是?Sized的。这也意味着:

    1. 若有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); // 不可动态分派。
    2. supertrait是泛型trait,那么supertrait泛型类型参数的实参一定不能是Self,因为Self编译时类型不确定和不能作为单态化参数。例程4

      trait 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`
  2. 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);
  3. 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从句书面地退出初始限定和重置SelfSized的。

    // `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定义中,

  1. 不出现【泛型类型参数】 例程8。例外,【泛型生命周期参数】还是被允许的。例程9

  2. self形参与返回值类型不能是Self。关键字Self代指trait实现类,但Object safe trait需要对实现类不知。

  3. self形参的数据类型必须是如下六种之一 例程10

    1. 只读引用&Self / &self

    2. 可修改引用&mut Self / &mut self

    3. 智能指针Box<Self>

    4. 引用计数Rc<Self>

    5. 原子引用计数Arc<Self>

    6. 不可swap内存Pin<P>。其中,泛型类型参数P可以是前五种类型中的任意一种。

  4. 千万别限定trait method的隐式类型参数SelfSized

条条框框还是比较多的,可得常记频用,才可应用自如。

对象安全trait的非成员方法关联函数

这类associated functions概念对等于Typescript静态成员方法。“静态”意味着这类关联函数一定不会参与动态分派,但出于未知原因rustc依旧偏好将其收录虚表vtable和造成trait Object实例化失败。所以,Object safe trait的重要原则之一,就是:

  • 要么,没有非成员方法关联函数

  • 要么,显式地书面限定每个非成员方法关联函数的隐式类型参数SelfSized。例程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);

结束语

【动态分派】是traittrait method初始开启的天赋技能。除了性能极客,@Rustacean 一般想不起刻意地对定它们做静态化处理。但,由于项目历史包袱,在旧trait定义内遗留的

  • 泛型函数

  • Self滥用

  • 非成员方法关联函数

导致其不再“对象安全”。咱们既不必埋怨旧代码作者(哎!谁的认知不是逐步深化的呀),也别慌,更别像我一样傻乎乎地立即重构代码(很的)。而仅只需要将仅能静态分派关联函数的隐式类型参数Self限定为Sized即可。只要虚表不再收录它们,rustc就不会抱怨了。于是,“同一个trait既兼容于新/旧代码,还动/静两用”岂不美哉 例程12!可是不值得炫技,因为大量这类trait代码是馁馁的后期维护心智灾难 — 只能算是变通的“歪招”。

这次分享的内容就是这些。创作不易,希望路过的神仙哥哥、仙女妹妹们评论、点赞、转发呀!相信我在【WEB汇编】技术方向Rust栈至今都是最优选择。

a1414dd3ef24e6d19ef65c2fd75a8431.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/805377.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

时间复杂度介绍及其计算

时间复杂度 1.算法效率 如何衡量一个算法的好坏呢&#xff1f;看这段代码&#xff1a; long long Fib(int N) {if(N < 3)return 1;return Fib(N-1) Fib(N-2); }这是斐波那契数列的递归代码&#xff0c;非常简洁&#xff0c;那么这就一定说明它好吗&#xff1f;答案显而易…

智能家居是否可与ChatGPT深度融合?

​ ChatGPT自2022年面世以来&#xff0c;已为亿万网民提供智能问答服务。然而我们是否曾想到&#xff0c;这一人工智能驱动的聊天机器人&#xff0c;是否可为智能家居赋能? 要实现ChatGPT与智能家居设备之间的无缝对话&#xff0c;单单依靠一台终端是远远不够的。ChatGPT必须…

Java基础_多线程

Java基础_多线程 什么是多线程并发, 并行多线程的实现方式继承Thread类实现Runnable接口实现Callable接口和Future接口实现方式对比 常见的成员方法常见方法进程的优先级守护线程礼让线程插入线程 线程安全线程的生命周期售票模拟同步代码块同步方法lock锁 死锁 生产者和消费者…

【C语言】通讯录1.0 (静态版)

前言 通讯录是一种记录联系人信息的工具&#xff0c;包括姓名、电话号码、电子邮件地址、住址等。 此通讯录是基于自定义类型的基础上进行制作&#xff0c;通讯录&#xff08;静态版&#xff09;&#xff0c;后期会进行通讯录的更新 ****** 有需要源代码&#xff0c;见文章末尾…

大于号在python中怎么打,python大于等于怎么写

大家好&#xff0c;小编为大家解答python中大于并小于一个数代码的问题。很多人还不知道python中大于等于且小于等于&#xff0c;现在让我们一起来看看吧&#xff01; 1、python 中不等于怎么表示 #!/usr/bin/python a1 b2 if ab: print "a 等于 b" if a!b: print &…

ArcGIS Pro 制作一张立体地形图

在各位关掉文章之前,先把成果贴上来 下面开始操作步骤贴图,这个真的很简单,没有什么复杂的软件联动和操作 这是哥斯达黎加部分区域的30mDEM,数据链接我放在最后。 首先,找到工具【栅格函数】—【统计分析】,选择下载好的栅格,领域设置行列数都改为6,点击创建新图层。然…

从官网认识 JDK,JRE,JVM 三者的关系

点击下方关注我&#xff0c;然后右上角点击...“设为星标”&#xff0c;就能第一时间收到更新推送啦~~~ JVM 是一些大厂面试必问点&#xff0c;要想解决 OOM、性能调优方面的问题&#xff0c;掌握 JVM 知识必不可少&#xff0c;从今天开始&#xff0c;将为大家介绍 JVM 的常用知…

ShardingSphere Pipeline 兼容 MySQL 时间类型 解读

背景 ShardingSphere 在日常中&#xff0c;开发者经常会用到时间类型&#xff0c;如果熟悉的时间类型涉及到了时区、精度&#xff0c;哪种时间类型更合适&#xff1f;JDBC 驱动中又有哪些注意事项&#xff1f;因此&#xff0c;对数据库时间类型有更深入的了解&#xff0c;有助于…

android framework车载桌面CarLauncher的TaskView详细源码分析

1、构建相关的TaskView&#xff0c;装载到对应的ViewGroup b站免费视频教程讲解&#xff1a; https://www.bilibili.com/video/BV1wj411o7A9/ //packages/apps/Car/Launcher/src/com/android/car/carlauncher/CarLauncher.java void onCreate() { //ignoresetContentView(R.…

管理类联考——写作——记忆篇——论证有效性分析——析错口诀

析错口诀 概念不明确&#xff0c;我就说它概念模糊&#xff0c;并做不利它的解释。【有概念模糊之嫌&#xff0c;A是理解1&#xff1f;还是理解2&#xff1f;】概念有变换&#xff0c;我就说它混淆概念&#xff0c;并指出混淆的环节。&#xff08;概念推概念&#xff0c;我就看…

类方法(成员方法)与构造方法的区别与联系

&#xff08;粗略理解为&#xff1a;除了构造方法以外的所有方法都是成员方法&#xff09; 区别&#xff1a; 构造方法在创建对象时用new调用&#xff0c;是为了给对象的数据进行初始化&#xff0c;没有返回值。 成员方法是实现对类中成员变量的操作&#xff0c;提供某些功能…

【图论】kruskal算法

一.介绍 Kruskal&#xff08;克鲁斯卡尔&#xff09;算法是一种用于解决最小生成树问题的贪心算法。最小生成树是指在一个连通无向图中&#xff0c;选择一棵包含所有顶点且边权重之和最小的树。 下面是Kruskal算法的基本步骤&#xff1a; 将图中的所有边按照权重从小到大进行…

JUC并发工具类

一、ReentrantLock 特点&#xff1a;独占、可重入、公平/非公平、可中断、支持多个条件变量 1、常用api ReentrantLock实现了Lock接口&#xff0c;Lock类规范定义了如下方法 lock()&#xff1a;获取锁&#xff0c;调用该方法的线程会获取锁&#xff0c;当锁获得后&#xff0…

想做上位机,学C#还是QT?

学习C#还是Qt&#xff0c;取决于你的具体需求和偏好。 如果你计划开发跨平台的桌面应用程序&#xff0c;并且希望使用一种更轻量级、直观的界面框架&#xff0c;那么Qt可能是一个不错的选择。Qt是一个功能丰富且成熟的跨平台框架&#xff0c;支持多种开发语言&#xff08;包括…

第J2周:ResNet50V2算法实战与解析

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f366; 参考文章&#xff1a;365天深度学习训练营-第J2周&#xff1a;ResNet50V2算法实战与解析&#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制 目录 一、论文解读1. ResNetV2结构与…

【需求响应】一种新的需求响应机制DR-VCG研究

目录 1 主要内容 2 部分代码 3 程序结果 4 程序链接 1 主要内容 该程序对应文章《Contract Design for Energy Demand Response》&#xff0c;电力系统需求响应&#xff08;DR&#xff09;用来调节用户对电能的需求&#xff0c;即在预测的需求高于电能供应时&#xff0c;希…

VS Code调试Darknet

一、安装插件 二、连接服务器 三、调试darknet工程 {"version": "2.0.0","options": {"cwd": "${workspaceFolder}"},"tasks": [{"label": "clean","type": "shell",&qu…

数据结构之动态顺序表(附带完整程序)

&#x1f388;基本概念 &#x1f308;一.线性表、顺序表的定义 ☀️&#xff08;1&#xff09;线性表&#xff1a; 是n个具有相同特性的数据元素的有限序列。线性表在逻辑上是线性结构&#xff0c;但在物理上存储时&#xff0c;通常以数组和链式结构的形式存储。 ☀️&…

C# 关于使用newlife包将webapi接口寄宿于一个控制台程序、winform程序、wpf程序运行

C# 关于使用newlife包将webapi接口寄宿于一个控制台程序、winform程序、wpf程序运行 安装newlife包 Program的Main()函数源码 using ConsoleApp3; using NewLife.Log;var server new NewLife.Http.HttpServer {Port 8080,Log XTrace.Log,SessionLog XTrace.Log }; serv…

hcip——ospf综合

要求 1. 搭建toop 2.地址规划 协议范围路由器地址 RIP 172.16.0.0 17 R12 loop0&#xff1a;172.16.0.0 24 loop1&#xff1a;172.16.1.0 24 OSPF 172.16.128.0 17 area1 172.16.144.0 20 R1 g0:172.16.144.1 24 loop0:172.16.145.1 24 R2 g0:172.16.144.2 24 loop:172…