【投稿】北海 - 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都感觉不到面向对象那味了,那是什么味?

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

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

相关文章

Go自定义PriorityQueue优先队列使用Heap堆

题目 分析 每次找最大的&#xff0c;pop出来 然后折半&#xff0c;再丢进去 go写法 go如果想用heap&#xff0c;要实现less\len\swap\push\pop 但可以偷懒&#xff0c;用sort.IntSlice,已经实现了less\len\swap 但由于目前是大根堆&#xff0c;要重写一下less 因此&#xff…

Maven依赖传递和依赖冲突

1 依赖传递 1.1 什么是依赖传递 现有Maven项目A、B、C&#xff0c;如果项目B依赖项目A&#xff0c;项目C依赖项目B&#xff0c;则可以认为项目C依赖项目A。这就是项目依赖的传递性。 此时项目C中会存在项目A和项目B中的所有依赖。 依赖传递的作用 简化依赖导入检测依赖版本…

懒加载图片案例

整体效果&#xff1a; HTML部分&#xff1a; <div class"lazy-box"><img class"lazy" data-original"img/1.jpg" alt"1.jpg" width"960" height"540"><img class"lazy" data-original…

用大白话举例子讲明白云计算

前几天王坚院士在2023云栖大会上发表了关于云计算的演讲&#xff0c;听得我是热血沸腾&#xff0c;王院士称AI和云计算的结合是“云计算的第三次浪潮”&#xff0c;对此我深表认同。但是身边的很多朋友还不知道云计算是什么意思&#xff0c;有些人还认为百度云和百度云盘是一个…

【RabbitMQ】RabbitMQ详解(二)

RabbitMQ详解 死信队列死信来源消息TTL过期队列达到最大长度消息被拒绝 RabbitMQ延迟队列TTL的两种设置队列设置TTL消息设置TTL 整合SrpingBoot队列TTL延时队列TTL优化Rabbtimq插件实现延迟队列 死信队列 先从概念解释上搞清楚这个定义&#xff0c;死信&#xff0c;顾名思义就…

测试:YAML OpenAPI(Swagger)

YAML YAML&#xff08;Yet Another Markup Language&#xff09;是一种数据序列化格式&#xff0c;通常被用来配置文件。它易于阅读&#xff0c;并且以数据结构为中心。YAML文件通常以.yaml或.yml为扩展名。 下面是一个YAML文件的简单示例&#xff1a;定义了一个人的基本信息…

Linux---基础操作命令

内容导航 类别内容导航机器学习机器学习算法应用场景与评价指标机器学习算法—分类机器学习算法—回归机器学习算法—聚类机器学习算法—异常检测机器学习算法—时间序列数据可视化数据可视化—折线图数据可视化—箱线图数据可视化—柱状图数据可视化—饼图、环形图、雷达图统…

GO语言基础笔记(三):复合类型

深入学习Go语言中的复合类型&#xff1a;数组、切片、映射&#xff08;Map&#xff09;和结构体。这些类型是构建复杂数据结构和实现复杂逻辑的基础。 目录 1. 数组&#xff08;Array&#xff09; 遍历数组 多维数组 数组作为函数参数 2. 切片&#xff08;Slice&#xff…

单片机的RTC获取网络时间

理解网络同步校准RTC的原理需要考虑NTP、SNTP、RTC这三个关键组件的作用和交互。下面详细解释这个过程&#xff1a; 1. NTP&#xff08;Network Time Protocol&#xff09;&#xff1a; 协议目的&#xff1a;NTP是用于同步计算机和设备时钟的协议。它通过在网络上与时间服务器通…

Java中Collections详解

Java中Collections详解 在Java中&#xff0c;java.util.Collections 是一个实用类&#xff0c;提供了各种静态方法&#xff0c;用于对集合进行操作和控制。这个类包含了许多有用的方法&#xff0c;用于对集合进行排序、查找、替换等操作。以下是一些 Collections 类中常用方法…

JUC AQS ReentrantLock源码分析

AQS java.util.concurrent.locks.AbstractQueuedSynchronizer AQS &#xff08;抽象队列同步器&#xff09;&#xff1a; AbstractQueuedSynchronizer 是什么 来自jdk1.5&#xff0c;是用来实现锁或者其他同步器组件的公共基础部分的抽象实现&#xff0c;是重量级基础框架以及…

前端性能优化十八:减少浏览器的回流和重绘

1. css: ①. 避免过多样式嵌套:a. 尽量让浏览器一次性找到样式.②. 避免使用css表达式:a. 在每次css绘制的过程中都是会执行.③. 使用绝对定位可以让动画元素脱离文档流:a. 给动画元素做单独定位④. 避免使用table布局:a. 会引起浏览器多次重绘⑤. 尽量不适用float布局:a. 可以…

【MybatisPlus快速入门】(3)SpringBoot整合MybatisPlus 之 Lombok插件安装及MybatisPlus分页代码示例

目录 1.Lombok1.1 步骤1:添加lombok依赖 2.2 步骤2:安装Lombok的插件1.3 步骤3:模型类上添加注解2 分页功能2.1 步骤1:调用方法传入参数获取返回值2.2步骤2:设置分页拦截器2.3 步骤3:运行测试程序 之前我们已学习MyBatisPlus在代码示例与MyBatisPlus的简介&#xff0c;在这一节…

Golang make vs new

文章目录 1.简介2.区别3.new 可以初始化 slice&#xff0c;map 和 channel 吗&#xff1f;4.make 可以初始化其他类型吗&#xff1f;5.小结参考文献 1.简介 在 Go 语言中&#xff0c;make 和 new 是两个用于创建对象的内建函数&#xff0c;但它们有着不同的用途和适用范围。 …

DLLNotFoundException:xxx tolua... 错误打印

DLLNotFoundException:xxx tolua... 错误打印 一、DLLNotFoundException介绍二、Plugins文件夹文件目录结构如下&#xff1a; 三、Plugins中的Android文件夹四、Plugins中的IOS文件夹这里不说了没测试过不过原理应该也是选择对应的平台即可五、Plugins中的x86和X86_64文件夹 一…

浅学JWT跨域认证

Json Web令牌简称JWT JWT的使用 加入依赖 <dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.3.0</version> </dependency> 测试案例 Test //生成token void jwtdemo01(){Calendar in…

【机器学习】模式识别

1 概述 模式识别&#xff0c;简单来讲&#xff0c;就是分类问题。 模式识别应用&#xff1a;医学影像分析、人脸识别、车牌识别、遥感图像 2 模式分类器 分类器的分类&#xff1a;线性分类器、非线性分类器、最近邻分类器 2.1 分类器的训练&#xff08;学习&#xff09;过…

Featured Based知识蒸馏及代码(3): Focal and Global Knowledge (FGD)

文章目录 1. 摘要2. Focal and Global 蒸馏的原理2.1 常规的feature based蒸馏算法2.2 Focal Distillation2.3 Global Distillation2.4 total loss3. 实验完整代码论文: htt

C语言中关于指针的理解

#include <stdio.h> int main() {int a11;int *p&a; //因为a是整型的&#xff0c;所以我们定义指针p的时候要和a的类型一样char b;char *pa&b; //同理&#xff0c;b是字符型&#xff0c;所以这里的pa也要用字符型return 0; }因为*p指向的是地址&…

Pytorch项目,肺癌检测项目之四

# 安装图像处理 的两个包 simpleITK 和 ipyvolume # 安装缓存相关的两个包 diskcache 和 cassandra-driver import gzip from diskcache import FanoutCache, Disk from cassandra.cqltypes import BytesType from diskcache import FanoutCache,Disk,core from diskcache…