当前位置: 首页 > news >正文

【投稿】北海 - Rust与面向对象(二)

模板方法

Rust提供了trait,类似于面向对象的接口,不同的是,将传统面向对象的虚函数表从对象中分离出来,trait仍然是一个函数表,只不过是独立的,它的参数self指针可以指向任何实现了该trait的结构。

从对象中分离出虚函数表的trait,带来了使用上与面向对象一些根本的不同,这在我看来算是“很大”的不同了。让我们以模版方法设计模式为例来感受一下。先想一下,rust怎么依赖trait和结构继承,实现模板方法?所谓模板方法,就是父类留一个空白方法作为虚函数,交给子类实现,这样子类只负责不同的算法部分,是面向对象中很基础很常用的手法了。用Rust语言照葫芦画瓢先描述一下大概框架,如下:

/// 一个父类A
struct A {...
}impl A {fn do_step1_common(&mut self) { ... }// 缺省实现,留给子类实现,如果是C++/Java这类面向对象语言,很容易。若是Rust,该怎么搞?fn do_step2_maybe_different(&mut self) { ... }fn do_step3_common(&mut self) { ... }pub fn do_all_steps(&mut self) {self.do_step1_common();self.do_step2_maybe_different();self.do_step3_common();}
}// 具体的某个子类实现
struct A1 {a: A,...
}impl A1 {// 开始实现fn do_step2_maybe_different(&mut self) {// A1提供一种实现}
}

不瞒大家,我初识rust时就被这样一个面向对象上的简单案例,用rust实现给难住了!当时卡在父类看起来像是一个完整的类型,Rust怎么能未卜先知调用子类的方法呢?

其实,Rust要想实现这种效果,不能A1继承A这种了,而是A包含A1子类来实现,反着来,将不同的实现单独拆出来作为trait,再交给子类实现。

trait DoStep2 {fn do_step2_maybe_different(&mut self);
}/// 另一个父类B
struct B<T: DoStep2> {t: T, // 或者Box<&dyn DoStep2>...
}impl<T> B<T> {fn do_step1_common(&mut self) { ... }fn do_step3_common(&mut self) { ... }
}impl<T: DoStep2> B<T> {pub fn do_all_steps(&mut self) {self.do_step1_common();self.t.do_step2_maybe_different();self.do_step3_common();}
}/// 具体的子类实现
struct B1 {...
}impl DoStep2 for B1 {fn do_step2_maybe_different(&mut self) {// B1提供一种实现}
}// 这样,
// B<B1> 相当于面向对象中的 A1
// B<B2> 相当于面向对象中的 A2

感觉不错,看起来颇为妥当,这种方式已经能在适合它的场景中工作,也是模板方法的体现。对比下,AB都不是完整的父类实现,A1B<B1>才是真正的具体类型,且它们都包含了父类的结构,虽然B<B1>的写法有点不合常规。若子类还拥有自己的独立的扩展结构的话,那Rust这种方式更优雅一些,拆分的更原子、更合理。实践中,往往不会这么完美的套用,会复杂很多,比如子类作为具体类型,想访问父类的成员,才能配合完成do_step2,Rust又该怎么做?面向对象的this指针则轻松支持。Rust不可能让B1再直接包含B,那样循环包含了,只能用引用或者指针来存在B1里面,但这样的话,岂不是太麻烦了,循环引用/包含都是我们极力避免的东西,麻烦到都想放弃模板方法了!

为何会有这种差异?因为面向对象的子类this指针其实指向的是整体,子类的函数表是个本身就包含父类的整体;而上述为B1实现DoStep2 trait的时候,self指向的仅仅是B1,并不知道B的存在。那怎么办?得让self指向整体B<B1>,那为B<B1>实现DoStep2行不行?像下面这样:

impl DoStep2 for B<B1> {fn do_step2_maybe_different(&mut self) {// 这里self可以访问“父类”B的成员了}
}

但回过头来,B::do_all_steps(&mut self)就没法在“父类”B中统一实现了,因为B<T>B<B1>具象化之前,还不知道哪来的do_step2,因此要在impl B<B1>中实现,每个不同的具像化的子类都得单独实现相同的do_all_steps!你能接受不?

也许你能接受,为每个B<B1>B<B2>...重复拷贝一遍各自的do_all_steps!本文基于专业探讨,还是要寻找一下编写通用的do_all_steps方法的,有没有?当然是有的,前提是,你得把do_step1_commondo_step3_common也得trait化,然后在用一个trait组合限定搞定,如下:

trait DoStep1 {fn do_step1_common(&mut self);
}trait DoStep3 {fn do_step2_common(&mut self);
}// 因为B<T>是泛型,只需为泛型编码实现一次DoStep1、DoStep3就行
impl<T> DoStep1 for B<T> { ... }
impl<T> DoStep3 for B<T> { ... }// 最后,实现通用的do_all_steps,还得靠泛型。
// 此时,B<B1>已经满足T,会为其实现下面的函数
// 可以这样读:为所有实现了DoStep1/DoStep2/DoStep3特质的类型T实现do_all_steps
impl<T> T 
whereT: DoStep1 + DoStep2 + DoStep3
{pub fn do_all_steps(&mut self) {self.do_step1_common();self.do_step2_maybe_different();self.do_step3_common();}
}

如何,这样应该能接受了吧。Rust通过把问题解构的更细粒度,完成了任务。客观对比下,面向对象的实现还是简单些,父类的do_step1do_step3函数永远指向了同一个实现,而Rust靠泛型应该是指向了3个不同的实现?不知道编译期有没有优化,盲猜应该有。可以说语法如此,Rust只能做到如此了。与面向对象的模板方法相比,最后一点小瑕疵,就是要多定义DoStep1DoStep2 2个trait,并用一个T: DoStep1 + DoStep2 + DoStep3通用类型包含同样实现了DoStep1 + DoStep2 + DoStep3B<T>,进而代表它。可我们想仅仅为B<T>类型实现,其他类型也不太可能这样实现了,一个T则把范围不必要地扩大了。要是能按照我们想要的,就仅为B<T>且实现了DoStep2B<T>来实现do_all_steps,就完美了。要做到此种程度,必须能对自身Self进行限定,如下:

/// 可以这样读:为所有自身实现了DoStep2的B<T>实现do_all_steps
impl<T> B<T>
whereSelf: DoStep2
{pub fn do_all_steps(&mut self) {self.do_step1_common();self.do_step2_maybe_different();self.do_step3_common();}
}

这种写法还真可以,也不用额外定义DoStep1、DoStep3了,因为本身B<T>已经有do_step1_common/do_step3_common的实现了,Rust最新的稳定版就支持这样写!

一段完整的Rust代码,可以参考这里:https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b80de6d4e6d75bf59bb37db386264fed

一个小小的模板方法,Rust分离出2种不同的方式,这是模板方法设计模式都没提到的,2种方式还各有韵味。从定义的顺序上,C++的模板方法,是 “子类后续扩展父类” ,Rust的模板方法,则是 “父类提前包含子类泛型” ,写法上还真是一开始不太好扭过来。可一旦扭转过来,发现Rust挺强,仍不失面向对象技巧。

反观面向对象,一个模板方法案例,让大家看到了些许面向对象的束缚,其实也无伤大雅,面向对象也能用纯组合的方式实现模板方法,也不用继承,如果需要组合的对象再通过构造动态传递进来,那就跟策略模式很像了,这种组合传递来的对象不止一个时,就是策略模式!然后,让我想起了一个小争论,子类应该严格不准访问父类的成员,让父类的变化完全掌控在父类手中。面向对象的确可以做到,全部private。但Rust的处理方式,显示出了其对这些细节的语法表达更合乎逻辑。

总结

模板方法是面向对象虚函数继承的基本应用,是面向对象很多设计模式的基础,如装饰器模式。一篇讲解下来,Rust从一开始别别扭扭到更好地支持模板方法,其实能体会到,Rust强迫你去拆解,即便都是同一个模板方法,但不同的细节要求,子类是否需要访问父类,都有不同的处理变化,分出来的形式还更严格。写到最后,Rust都感觉不到面向对象那味了,那是什么味?

相关文章:

【投稿】北海 - Rust与面向对象(二)

模板方法 Rust提供了trait&#xff0c;类似于面向对象的接口&#xff0c;不同的是&#xff0c;将传统面向对象的虚函数表从对象中分离出来&#xff0c;trait仍然是一个函数表&#xff0c;只不过是独立的&#xff0c;它的参数self指针可以指向任何实现了该trait的结构。 从对象中…...

HarmonyOS构建第一个ArkTS应用(FA模型)

构建第一个ArkTS应用&#xff08;FA模型&#xff09; 创建ArkTS工程 若首次打开DevEco Studio&#xff0c;请点击Create Project创建工程。如果已经打开了一个工程&#xff0c;请在菜单栏选择File > New > Create Project来创建一个新工程。 选择Application应用开发&a…...

阿里云 ARMS 应用监控重磅支持 Java 21

作者&#xff1a;牧思 & 山猎 前言 今年的 9 月 19 日&#xff0c;作为最新的 LTS (Long Term Support) Java 版本&#xff0c;Java 21 正式 GA&#xff0c;带来了不少重量级的更新&#xff0c;详情请参考 The Arrival of Java 21 [ 1] 。虽然目前 Java 11 和 Java 17 都…...

C++ 类的析构函数和构造函数

构造函数 类的构造函数是类的一种特殊的成员函数&#xff0c;它会在每次创建类的新对象时执行。主要用来在创建对象时初始化对象即为对象成员变量赋初始值。 构造函数的名称与类的名称是完全相同的&#xff0c;并且不会返回任何类型&#xff0c;也不会返回 void。构造函数可用…...

STM32——CAN协议

文章目录 一.CAN协议的基本特点1.1 特点1.2 电平标准1.3 基本的五个帧1.4 数据帧 二.数据帧解析2.1 帧起始和仲裁段2.2 控制段2.3 数据段和CRC段2.4 ACK段和帧结束 三.总线仲裁四.位时序五.STM32CAN控制器原理与配置5.1 STM32CAN控制器介绍5.2 CAN的模式5.3 CAN框图 六 手册寄存…...

数据结构-如何巧妙实现一个栈?逐步解析与代码示例

文章目录 引言1.栈的基本概念2.选择数组还是链表&#xff1f;3. 定义栈结构4.初始化栈5.压栈操作6.弹栈操作7.查看栈顶和判断栈空9.销毁栈操作10.测试并且打印栈内容栈的实际应用结论 引言 栈是一种基本但强大的数据结构&#xff0c;它在许多算法和系统功能中扮演着关键角色。…...

web前端之拖拽API、vue3实现图片上传拖拽排序、拖放、投掷、复制、若依、vuedraggable

MENU vue2html5原生dom原生JavaScript实现跨区域拖放vue2实现跨区域拖放vue2mousedown实现全屏拖动&#xff0c;全屏投掷vue3element-plusvuedraggable实现图片上传拖拽排序vue2transition-group实现拖动排序原生拖拽排序 vue2html5原生dom原生JavaScript实现跨区域拖放 关键代…...

第11章 GUI Page403~405 步骤三 设置滚动范围

运行效果&#xff1a; 源代码&#xff1a; /**************************************************************** Name: wxMyPainterApp.h* Purpose: Defines Application Class* Author: yanzhenxi (3065598272qq.com)* Created: 2023-12-21* Copyright: yanzhen…...

【Spring Security】打造安全无忧的Web应用--使用篇

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Spring Security的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.Spring Security中的授权是…...

体验一下 CodeGPT 插件

体验一下 CodeGPT 插件 0. 背景1. CodeGPT 插件安装2. CodeGPT 插件基本配置3. (可选)CodeGPT 插件预制提示词原始配置(英文)4. CodeGPT 插件预制提示词配置(中文)5. 简单验证一下 0. 背景 看到B站Up主 “wwwzhouhui” 一个关于 CodeGPT 的视频&#xff0c;感觉挺有意思&#…...

深度学习 | 基础卷积神经网络

卷积神经网络是人脸识别、自动驾驶汽车等大多数计算机视觉应用的支柱。可以认为是一种特殊的神经网络架构&#xff0c;其中基本的矩阵乘法运算被卷积运算取代&#xff0c;专门处理具有网格状拓扑结构的数据。 1、全连接层的问题 1.1、全连接层的问题 “全连接层”的特点是每个…...

[字符编码]windwos下使用libiconv转换编码格式(二)

在http://t.csdnimg.cn/PLUuz笔记中实现了常用编码格式转换的功能,但这还是一个demo。因为代码中向libiconv库函数传递的字符串是存放在堆空间中的(我也是从网上找例子测试,是否一定要开辟堆空间存放还有待考证),如果一次性转换的字节数很巨大的话,就会导致内存空间不足,进而引…...

textile 语法

1、文字修饰 修饰行内文字 字体样式textile 语法对应的 XHTML 语法实际显示效果加强*strong*<strong>strong</strong>strong强调_emphasis_<em>emphasis</em>emphasis加粗**bold**<b>bold</b>bold斜体__italics__<i>italics</i…...

【快速开发】使用SvelteKit

自我介绍 做一个简单介绍&#xff0c;酒架年近48 &#xff0c;有20多年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0e;因为工作需要&#xff0c;另外也因为兴趣涉猎比较广&#xff0c;为了自己学习建立了三个博客&#xff0c;分别是【全球IT瞭望】&#xff0c;【…...

【docker笔记】docker常用命令

1、帮助启动类命令 1.1 启动、重启、查询当前状态、停止 systemctl start docker systemctl stop docker systemctl restart docker systemctl status docker1.2 设置开机启动 systemctl enable docker1.3 查看docker概要信息 docker info1.4 查看docker帮助文档 docker -…...

API 接口怎样设计才安全?

设计安全的API接口是确保应用程序和数据安全的重要方面之一。下面是一些设计安全的API接口的常见实践&#xff1a; 1. 身份验证和授权&#xff1a; 使用适当的身份验证机制&#xff0c;如OAuth、JWT或基本身份验证&#xff0c;以确保只有经过身份验证的用户可以访问API。实施…...

网站被CC攻击了怎么办?CC攻击有什么危害

网络爆炸性地发展&#xff0c;网络环境也日益复杂和开放&#xff0c;同时各种各样的恶意威胁和攻击日益增多&#xff0c;其中网站被CC也是常见的情况。 CC攻击有什么危害呢&#xff1f; 被CC会导致&#xff1a; 1.访问速度变慢&#xff1a;网站遭受CC攻击后&#xff0c;由于…...

Docker - 镜像 | 容器 日常开发常用指令 + 演示(一文通关)

目录 Docker 开发常用指令汇总 辅助命令 docker version docker info docker --help 镜像命令 查看镜像信息 下载镜像 搜索镜像 删除镜像 容器命令 查看运行中的容器 运行容器 停止、启动、重启、暂停、恢复容器 杀死容器 删除容器 查看容器日志 进入容器内部…...

要参加微软官方 Copilot 智能编程训练营了

GitHub Copilot 是由 GitHub、OpenAI 和 Microsoft 联合开发的生成式 AI 模型驱动的。 GitHub Copilot 分析用户正在编辑的文件及相关文件的上下文&#xff0c;并在编写代码时提供自动补全式的建议。 刚好下周要参加微软官方组织的 GitHub Copilot 工作坊-智能编程训练营&…...

Python入门学习篇(五)——列表字典

1 列表 1.1 定义 ①有序可重复的元素集合 ②可以存放不同类型的数据 ③个人理解:类似于java中的数组1.2 相关方法 1.2.1 获取列表长度 a 语法 len(列表名)b 示例代码 list2 [1, 2, "hello", 4] print(len(list2))c 运行结果 1.2.2 获取列表值 a 语法 列表名…...

React尝鲜

组件 React的组件就是一个js函数&#xff0c;函数内部return一个由jsx语法创建的html代码片段。 //MyComp.js export default function MyComp(){return (<h1>我是新组件MyComp</h1>) } 在需要引入组件的地方import导入组件&#xff0c;并放在相应位置 //App.js…...

锯齿云服务器租赁使用教程

首先登陆锯齿云账号 网盘上传数据集与代码 随后我们需要做的是将所需要的数据集与代码上传到网盘&#xff08;也可以直接在租用服务器后将数据集与代码传到服务器的硬盘上&#xff0c;但这样做会消耗大量时间&#xff0c;造成资源浪费&#xff09; 点击工作空间&#xff1a;…...

HarmonyOS和OpenHarmony的区别

1.概要 众所周知&#xff0c;鸿蒙是华为开发的一款分布式操作系统。因为开发系统&#xff0c;最重要的是集思广益&#xff0c;大家共同维护。为了在IOS和Android之间生存&#xff0c;鸿蒙的茁壮成长一定是需要开源&#xff0c;各方助力才能实现。   在这种思想上&#xff0c;…...

Redis Stream消息队列之基本语法与使用方式

前言 本文的主角是Redis Stream&#xff0c;它是Redis5.0版本新增加的数据结构&#xff0c;主要用于消息队列&#xff0c;提供了消息的持久化和主备复制功能&#xff0c;可以让任何客户端访问任何时刻的数据&#xff0c;并且能记住每一个客户端的访问位置&#xff0c;还能保证…...

制造行业定制软件解决方案——工业信息采集平台

摘要&#xff1a;针对目前企业在线检测数据信号种类繁多&#xff0c;缺乏统一监控人员和及时处置措施等问题。蓝鹏测控开发针对企业工业生产的在线数据的集中采集分析平台&#xff0c;通过该工业信息采集平台可将企业日常各种仪表设备能够得到数据进行集中分析处理存储&#xf…...

[python]用python实现对arxml文件的操作

目录 关键词平台说明一、背景二、方法2.1 库2.2 code 关键词 python、excel、DBC、openpyxl 平台说明 项目Valuepython版本3.6 一、背景 有时候需要批量处理arxml文件(ARXML 文件符合 AUTOSAR 4.0 标准)&#xff0c;但是工作量太大&#xff0c;阔以考虑用python。 二、方…...

pdf 在线编辑

https://smallpdf.com/edit-pdf#rapp 参考 https://zh.wikihow.com/%E5%B0%86%E5%9B%BE%E5%83%8F%E6%8F%92%E5%85%A5PDF...

自然语言处理(NLP):理解语言,赋能未来

目录 前言1 什么是NLP2 NLP的用途3 发展历史4 NLP的基本任务4.1 词性标注&#xff08;Part-of-Speech Tagging&#xff09;4.2 命名实体识别&#xff08;Named Entity Recognition&#xff09;4.3 共指消解&#xff08;Co-reference Resolution&#xff09;4.4 依存关系分析&am…...

FastAPI使用loguru时,出现重复日志打印的解决方案

首先看图&#xff0c;发现每个日志都被打印了3条。其实这个和uvicorn日志打印的设计有关&#xff0c;在uvicorn中有多个logger&#xff0c;分别是uvicorn、uvicorn.error、uvicorn.access 而LOGGING默认有一个属性propagate&#xff0c;这个属性为True时&#xff0c;子日志记录…...

构建每个聚类的profile和deletion_mean特征

通过summarize_clusters函数构建每个聚类的protein[cluster_profile]和protein[cluster_deletion_mean]特征。目的是把extra_msa信息反映到msa中。 集成函数数据处理流程&#xff1a; sample_msa ->make_masked_msa -> nearest_neighbor_clusters -> summarize_clu…...