rust中如何利用generic与PhantomData来实现更清晰的接口

news2025/1/19 8:08:17

前两天看了一个在 rustlang 中如何利用 generic 和 PhantomData 来让我们的 api 更加合理的视频, 当时看完就想写一篇相关内容的文章, 但是没有立即动手,一推迟,不出意外的忘了。这两天又接手了一个半成品的项目, 需要维护原有代码和添加新的特性, 看代码的时候感慨良多, 又想起了前两天看的那个视频, 觉得必须得写点什么了。

假设我们要创建一个密码管理器, 这个管理器有两种状态, 一种是锁定状态, 在锁定状态下我们可以添加新的密码; 一种是非锁定状态, 在非锁定状态下我们可以列出所有的密码; 在任何状态下, 我们都可以查看管理器的版本。

代码如下:

use std::collections::HashMap;

#[derive(Debug, Eq, PartialEq)]
enum State {
    Locked,
    Unlocked,
}
struct PasswordManager {
    state: State,
    passwords: HashMap<String, String>,
    version: String,
}

impl PasswordManager {
    fn new(version: &str) -> Self {
        Self {
            state: State::Unlocked,
            passwords: HashMap::new(),
            version: version.into(),
        }
    }

    fn lock(&mut self) -> Result<(), String> {
        if self.state == State::Locked {
            return Err("cannot lock a locked manager".into());
        }
        self.state = State::Locked;
        Ok(())
    }

    fn unlock(&mut self) -> Result<(), String> {
        if self.state == State::Unlocked {
            return Err("cannot unlock a unlocked manager".into());
        }
        self.state = State::Unlocked;
        Ok(())
    }

    fn add_password(&mut self, username: &str, password: &str) -> Result<(), String> {
        if self.state == State::Unlocked {
            return Err("cannot add password unless lock this manager".into());
        }
        self.passwords.insert(username.into(), password.into());
        Ok(())
    }

    fn all_passwords(&self) -> Result<&HashMap<String, String>, String> {
        if self.state == State::Locked {
            return Err("cannot get all passwords unless unlock this manager".into());
        }
        Ok(&self.passwords)
    }

    fn version(&self) -> &str {
        &self.version
    }
}

在如上的代码里我们定义了一个PasswordManager类型, 其内部通过维护一个State字段来实现状态的切换, 这样实现了我们的需求, 但是在实际使用的时候如果不对这些方法进行说明的话,可能会对用户造成困惑。比如如下的代码:


fn password_manager_demo() {
    let mut m = PasswordManager::new("0.1");
    // 未调用lock(), 会返回Error
    m.add_password("Marquez", "93");
    // 对一个unlocked的manager调用unlock(), 会返回Error
    m.unlock();
    m.lock();
    // all_passwords()只能在unlocked的状态下调用
    println!("{:?}", m.all_passwords());
}

以上代码我把应有的异常检查部分省略掉了, 有的同学可能会说, 加上异常检查不就可以了么, 但是异常检查是运行时的检查, 对编译时没有任何作用, 也就是说, 如果不运行这些代码, 我们不会知道这里面有没有错误。编译器不会给出任何警告, 自动补全也会列出所有的方法

在这里插入图片描述

既然这样不好用, 我们就改一下:

struct LockedPasswordManager {
    passwords: HashMap<String, String>,
    version: String,
}

struct UnlockedPasswordManager {
    passwords: HashMap<String, String>,
    version: String,
}

impl LockedPasswordManager {
    fn new(version: &str) -> Self {
        Self {
            passwords: HashMap::new(),
            version: version.into(),
        }
    }

    fn unlock(self) -> UnlockedPasswordManager {
        UnlockedPasswordManager {
            passwords: self.passwords,
            version: self.version,
        }
    }

    fn add_password(&mut self, username: &str, password: &str) {
        self.passwords.insert(username.into(), password.into());
    }

    fn version(&self) -> &str {
        &self.version
    }
}

impl UnlockedPasswordManager {
    fn new(version: &str) -> Self {
        Self {
            passwords: HashMap::new(),
            version: version.into(),
        }
    }

    fn lock(self) -> LockedPasswordManager {
        LockedPasswordManager {
            passwords: self.passwords,
            version: self.version,
        }
    }

    fn all_passwords(&self) -> &HashMap<String, String> {
        &self.passwords
    }

    fn version(&self) -> &str {
        &self.version
    }
}

这次我们把PasswordManager直接分成了两个类型, LockedPasswordManagerUnlockedPasswordManager, 不用内置的字段来维护状态了, 两种类型本身就代表了不同的状态, 这种方法是我平时写 go 的比较常用的方法, 这样两种类型各自实现自己的方法, 不小心调用了对方的方法,编译器会报错, 而且自动补全也只会列出响应状态的方法了。

在这里插入图片描述
在这里插入图片描述

看上去好了很多, 但是这也引入了两个问题

  1. 我们的lock()unlock()方法接受的参数从&mut self变成了self, 当然你也可以选择不换, 但最终这两个方法都需要返回一个新的 object, 因为现在状态是靠不同类型的 struct 来实现的

  2. version()方法我们要分别在两种类型里各实现一遍,这就犯了代码不能复用的忌讳, 因为如果未来要修改这个方法, 我们需要提醒维护者要分别在这两个类型中进行修改。一旦忘了一个, 那本该相同的返回值就可能会变得不同了

对于第一个问题, 我们无能为力, 这是分开类型的必然结果, 但是如果我们从函数式编程的角度来看, 这种问题就不叫问题了, 这正是他们所鼓励的不可变变量的形式, 我们不改变变量的值, 而应该将它输入到一个函数中然后获得一个新的符合我们要求的变量。

第二个问题确实是一个比较严重的问题, 在 go 里, 如果遇到这种情况, 我通常会将version()单独封装在一个 struct 里, 然后以匿名字段的形式将这个 struct 放到LockedPasswordManagerUnlockedPasswordManager中, 这样即不会影响version()的调用又不会出现代码不能复用的问题。

那 rust 呢?今天的主题终于来了, 那就是利用 generic 和 PhantomData 来解决这个问题, 上代码:

use std::marker::PhantomData;

struct Locked;

struct Unlocked;

struct GenericPasswordManager<S = Unlocked> {
    passwords: HashMap<String, String>,
    version: String,
    state: PhantomData<S>,
}

impl<S> GenericPasswordManager<S> {
    fn new(version: &str) -> Self {
        Self {
            passwords: HashMap::new(),
            version: version.into(),
            state: PhantomData,
        }
    }

    fn version(&self) -> &str {
        &self.version
    }
}

impl GenericPasswordManager<Unlocked> {
    fn lock(self) -> GenericPasswordManager<Locked> {
        GenericPasswordManager {
            passwords: self.passwords,
            version: self.version,
            state: PhantomData,
        }
    }

    fn all_passwords(&self) -> &HashMap<String, String> {
        &self.passwords
    }
}

impl GenericPasswordManager<Locked> {
    fn unlock(self) -> GenericPasswordManager<Unlocked> {
        GenericPasswordManager {
            passwords: self.passwords,
            version: self.version,
            state: PhantomData,
        }
    }

    fn add_password(&mut self, username: &str, password: &str) {
        self.passwords.insert(username.into(), password.into());
    }
}

先说一下 PhantomData, 这货其实跟它的名字一样, 你说它存在, 你看不到它也摸不到它, 你说它不存在, 它还确实以自己的方式对外界施加着影响。

Zero-sized type used to mark things that “act like” they own a T.
Adding a PhantomData<T> field to your type tells the compiler that your
type acts as though it stores a value of type T, even though it doesn’t
really. This information is used when computing certain safety properties.

以上是官方文档里对它的解释, 其实就是个 size 为 0 的 struct, 或者说是个 type 标识, 目的就是为了防止编译器 bb, 你引入的 generic 变量没有在 struct 中使用到。有了它, 我们就可以新定义两个 struct, LockedUnlocked, 然后再把它们以PhantomData的形式放到我们的GenericPasswordManager中, 这样我们就得到了两种类型GenericPasswordManager<Locked>GenericPasswordManager<Unlocked>, 同时我们还可以以impl<S> GenericPasswordManager<S>的形式来实现实现两种类型共用的version()

问题到此告一段落,但是回到开头的位置,看着最初的那段代码,不禁思考,真的有必要这样做吗?

有必要,很有必要,我们写代码就像在讲故事,怎样能把故事讲的条理清晰,没有歧义其实往往比故事本身的精彩程度要重要的多。就一套 API 而言,如果你能让你大部分的用户在不看文档不看代码的情况下,单看函数签名就可以用的得心应手,那这一定是一套高质量的 API,而你也一定是一位经验丰富的程序员。写的代码越多,越觉得什么数据结构和算法,什么设计模式和架构,这些看上去高大上的东西其实只占很小的比重,甚至这些你都不了解,大部分的情况下仍然不妨碍你写出高质量的代码。写代码真正难的是如何把代码写的符合大多数人的直觉, 这里的人可能是一个线上产品的最终用户,也可能是搞二次开发的开发者,也可能是你的这段代码的后续维护者。不管你用什么样的方法,你能让他们在用你的代码的时候惊呼"想一块去了", 那你就是一个真正成功的开发者。但是,很多时候我们做不到这一点,有些现实当中的问题从根本上就是反直觉的,映射到代码中,我们也不得不做出妥协,但是妥协不意味着我们一点办法都没有,我们可以通过调整代码的结构,给变量起意义更明确的名字,写更多的注释,提供更详尽的文档, 来让使用者尽可能多的理解我们的用意。所以,代码写到最后,你会发现,那些当年你觉得不入流的"手段",其实才是码农这一行当的精髓。

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

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

相关文章

C++程序调用IsBadReadPtr或IsBadWritePtr引发内存访问违例问题的排查

目录 1、问题描述 2、VS中看不到有效的信息&#xff0c;尝试使用Windbg去分析 3、使用Windbg分析 4、最后 VC常用功能开发汇总&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&#xff09;https://blog.csdn.net/chenlycly/article/details/12427258…

数据结构-链表-单链表(3)

目录 1. 顺序表的缺陷 2. 单链表 2.1 单链表的基本结构与接口函数 2.2 重要接口 创建新节点的函数&#xff1a; 2.2.1 尾插 2.2.2 头插 2.2.3 尾删 2.2.4 头删 2.2.5 查找 2.2.6 插入 2.2.7 删除 2.2.8 从pos后面插入 2.2.9 从pos后面删除 3. 链表的缺陷与优势&…

传输数据格式:JSON 异步加载

JSON JSON是一种传输数据的格式&#xff08;以对象为样板&#xff0c;本质上就是对象&#xff0c;但用途有区别&#xff0c;对象就是本地用的&#xff0c;json是用来传输的&#xff09;JSON.parse();string --> jsonJSON.stringify();json --> string json ---> {n…

关于安卓的一些残缺笔记

安卓笔记Android应用项目的开发过程Android的调试Android项目文档结构Intent的显式/隐式调用Activity的生命周期1个Activity界面涉及到生命周期的情况2个Activity界面涉及到生命周期的情况Android布局的理论讲解Activity界面布局ContentProvider是如何实现数据共享Android整体架…

mysql视图和存储过程

视图视图就是将一条sql查询语句封装起来&#xff0c;之后使用sql时&#xff0c;只需要查询视图即可&#xff0c;查询视图时会将这条sql语句再次执行一遍。视图不保存数据&#xff0c;数据还是在表中。SELECT 语句所查询的表称为视图的基表&#xff0c;而查询的结果集称为虚拟表…

ATTCK v10版本战术实战研究—持久化(一)

一、前言“在网络安全的世界里&#xff0c;白帽子与黑帽子之间无时无刻都在进行着正与邪的对抗&#xff0c;似乎永无休止。正所谓&#xff0c;道高一尺魔高一丈&#xff0c;巨大的利益驱使着个人或组织利用技术进行不法行为&#xff0c;花样层出不穷&#xff0c;令人防不胜防。…

udk2017环境搭建编译步骤

win10 64bit系统 1.参考minnowboard-max-rel-1-01-bin-releasenotes-for-binary-firmware-images.TXT MyWorkspace.rar 解压到c:\&#xff0c;参考txt中的git操作 3.复制ASL,NASM 到c&#xff1a;\ 安装vs2015 &#xff0c;勾选sdk 5.安装 python-2.7.10.amd64.msi&#xf…

【论文泛读】NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis

NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis | NeRF: 用于视图合成的神经辐射场的场景表示 | 2020年 出自文献&#xff1a;Mildenhall B, Srinivasan P P, Tancik M, et al. Nerf: Representing scenes as neural radiance fields for view synth…

泼辣修图Polarr5.11.4 版,让你的创意无限延伸

泼辣修图是一款非常实用的图片处理软件&#xff0c;它不仅拥有丰富的图片处理功能&#xff0c;而且还能够轻松地实现自定义操作。泼辣修图的操作界面非常简洁&#xff0c;功能也非常丰富&#xff0c;使用起来非常方便快捷。 泼辣修图拥有非常丰富的图片处理功能&#xff0c;包括…

【冲刺蓝桥杯的最后30天】day1

大家好&#x1f603;&#xff0c;我是想要慢慢变得优秀的向阳&#x1f31e;同学&#x1f468;‍&#x1f4bb;&#xff0c;断更了整整一年&#xff0c;又开始恢复CSDN更新&#xff0c;从今天开始逐渐恢复更新状态&#xff0c;正在备战蓝桥杯的小伙伴可以支持一下哦&#xff01;…

Rockchip Android13 GKI开发指南

Rockchip Android13 GKI开发指南 文章目录Rockchip Android13 GKI开发指南GKI介绍Google upstream kernel下载及编译Rockchip SDK中GKI相关目录介绍Rockchip GKI编译代码修改编译固件烧写KO编译及修改添加新的模块驱动的方法调试ko方法开机log确认uboot阶段Android阶段KO加载KO…

Java IO流详解

文章目录一、File1.1 构造方法1.2 文件操作 方法1.3 目录操作 方法1.4 文件检测 方法1.5 获取文件信息 方法1.6 应用练习二、IO 流2.1 InputStream 字节输入流 (读)&#x1f353;FileInputStream&#x1f353;BufferedInputStream2.2 OutputStream 字节输出流 (写)&#x1f34c…

【Redis】redis大key和大value的危害,如何处理?

前序 还记得上次和同事一起去面试候选人时&#xff0c;同事提了一个问题&#xff1a;Redis的大key有什么危害&#xff1f;当时候选人主要作答的角度是一个key的value较大时的情况&#xff0c;比如&#xff1a; 内存不均&#xff1a;单value较大时&#xff0c;可能会导致节点之…

[经验分享]gpt-3.5-Turbo|unity中实现http接口调用gpt新接口以及信息处理的实现案例分享

最近openAI发布了目前chatGPT所使用的模型gpt-3.5-Turbo&#xff0c;之前使用了text-davinci-003模型做了一个galgame的AI女友对话的demo。这次趁着新接口的发布&#xff0c;对这个demo也同步更新了模型调用的代码。本篇文章将分享一下&#xff0c;如何在unity里使用UnityWebRe…

记录一次PWM信号异常问题

问题我使用单片机输出PWM控制机械臂&#xff0c;但是控制过程中&#xff0c;机械臂总是会出现莫名的抽动。利用示波器测试PWM信号&#xff0c;发现信号正常。过程&#xff08;1&#xff09;在反复的测试过程中&#xff0c;队友提出&#xff0c;将示波器的地线放在左侧的GND波形…

计算机EI会议论文,和EI期刊论文有什么区别? - 易智编译EaseEditing

EI期刊论文&#xff0c;是期刊论文的一种。顾名思义&#xff0c;就是指发在期刊上的论文。 期刊论文发表的格式需要具体参考各期刊文章的要求学术论文格式&#xff0c;主要会发在月刊/季刊/年刊/不定期的刊上。 目前&#xff0c;国际著名的科技文献检索系统是SCI&#xff08;…

安卓-AndroidManifest.xml修复

解析编译之后的AndroidManifest文件格式&#xff1a;http://www.520monkey.com/archives/575 案例apk jadx打开发现AndroidManifest.xml异常&#xff0c;无法正常显示 那么我们用apktool反编译试试 apktool d APK逆向-2.apk -f可以看到报错了&#xff0c;显示不能解析此xml…

[SSD科普] 固态硬盘物理接口SATA、M.2、PCIe常见疑问,如何选择?

前言犹记得当年Windows 7系统体验指数中&#xff0c;那5.9分磁盘分数&#xff0c;在其余四项的7.9分面前&#xff0c;似乎已经告诉我们机械硬盘注定被时代淘汰。势如破竹的SSD固态硬盘&#xff0c;彻底打破了温彻斯特结构的机械硬盘多年来在电脑硬件领域的统治。SSD数倍于HDD机…

数据结构 “串“ 的补充提升与KMP算法及其优化的具体实现

❤️作者主页&#xff1a;微凉秋意 ✅作者简介&#xff1a;后端领域优质创作者&#x1f3c6;&#xff0c;CSDN内容合伙人&#x1f3c6;&#xff0c;阿里云专家博主&#x1f3c6; ✨精品专栏&#xff1a;C面向对象 &#x1f525;系列专栏&#xff1a;数据结构与课程设计 文章目录…

XSS漏洞基本概念

目录 XSS的原理和分类 XSS漏洞分类 dom 存储型 XSS的危害 XSS漏洞的验证 XSS的黑盒测试 XSS漏洞的白盒测试 XSS的原理和分类 xss全称跨站脚本攻击xss&#xff08;Cross Site Scripting&#xff09; 为了不和层叠样式表&#xff08;Cascading Style Sheets, CSS)的缩写混淆&am…