当前位置: 首页 > 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 语法 列表名…...

测试微信模版消息推送

进入“开发接口管理”--“公众平台测试账号”&#xff0c;无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息&#xff1a; 关注测试号&#xff1a;扫二维码关注测试号。 发送模版消息&#xff1a; import requests da…...

【Linux】shell脚本忽略错误继续执行

在 shell 脚本中&#xff0c;可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行&#xff0c;可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令&#xff0c;并忽略错误 rm somefile…...

【HTML-16】深入理解HTML中的块元素与行内元素

HTML元素根据其显示特性可以分为两大类&#xff1a;块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...

高防服务器能够抵御哪些网络攻击呢?

高防服务器作为一种有着高度防御能力的服务器&#xff0c;可以帮助网站应对分布式拒绝服务攻击&#xff0c;有效识别和清理一些恶意的网络流量&#xff0c;为用户提供安全且稳定的网络环境&#xff0c;那么&#xff0c;高防服务器一般都可以抵御哪些网络攻击呢&#xff1f;下面…...

初学 pytest 记录

安装 pip install pytest用例可以是函数也可以是类中的方法 def test_func():print()class TestAdd: # def __init__(self): 在 pytest 中不可以使用__init__方法 # self.cc 12345 pytest.mark.api def test_str(self):res add(1, 2)assert res 12def test_int(self):r…...

uniapp手机号一键登录保姆级教程(包含前端和后端)

目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号&#xff08;第三种&#xff09;后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...

C语言中提供的第三方库之哈希表实现

一. 简介 前面一篇文章简单学习了C语言中第三方库&#xff08;uthash库&#xff09;提供对哈希表的操作&#xff0c;文章如下&#xff1a; C语言中提供的第三方库uthash常用接口-CSDN博客 本文简单学习一下第三方库 uthash库对哈希表的操作。 二. uthash库哈希表操作示例 u…...

Bean 作用域有哪些?如何答出技术深度?

导语&#xff1a; Spring 面试绕不开 Bean 的作用域问题&#xff0c;这是面试官考察候选人对 Spring 框架理解深度的常见方式。本文将围绕“Spring 中的 Bean 作用域”展开&#xff0c;结合典型面试题及实战场景&#xff0c;帮你厘清重点&#xff0c;打破模板式回答&#xff0c…...

Vue ③-生命周期 || 脚手架

生命周期 思考&#xff1a;什么时候可以发送初始化渲染请求&#xff1f;&#xff08;越早越好&#xff09; 什么时候可以开始操作dom&#xff1f;&#xff08;至少dom得渲染出来&#xff09; Vue生命周期&#xff1a; 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...

前端高频面试题2:浏览器/计算机网络

本专栏相关链接 前端高频面试题1&#xff1a;HTML/CSS 前端高频面试题2&#xff1a;浏览器/计算机网络 前端高频面试题3&#xff1a;JavaScript 1.什么是强缓存、协商缓存&#xff1f; 强缓存&#xff1a; 当浏览器请求资源时&#xff0c;首先检查本地缓存是否命中。如果命…...