【Rust自学】12.3. 重构 Pt.1:改善模块化

news2025/1/14 5:26:30

12.3.0. 写在正文之前

第12章要做一个实例的项目——一个命令行程序。这个程序是一个grep(Global Regular Expression Print),是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。

这个项目分为这么几步:

  • 接收命令行参数
  • 读取文件
  • 重构:改进模块和错误处理(本文)
  • 使用TDD(测试驱动开发)开发库功能
  • 使用环境变量
  • 将错误信息写入标准错误而不是标准输出

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
请添加图片描述

12.3.1. 重构的目的

重构的目的是要增进模块化的程度以及改善错误处理能力。

以下是截止到上一篇文章所写出的全部代码:

use std::env;  
use std::fs;  
  
fn main() {  
    let args:Vec<String> = env::args().collect();  
    let query = &args[1];  
    let filename = &args[2];  
  
    println!("search for {}", query);  
    println!("In file {}", filename);  
  
    let contents = fs::read_to_string(filename)  
        .expect("Somthing went wrong while reading the file");
    println!("With text:\n{}", contents);  
}

这个代码存在4个问题:

  • main函数负责的功能太多,它既负责命令行的功能解析,又负责读取文件。程序代码的编写原则是每一个函数只负责一个功能,所以说最好把函数拆开。

  • queryfilename这两个变量是用来存储程序配置的,contents是用来存储文件内容的。随着代码和变量在编写时越来越多,每个变量的实际意义就变得难以追踪。所以最好把这些变量存在结构体里。

  • 读取文件时使用expect来处理错误,不论读取时出现了什么错误都只会打印出错误信息并恐慌,这并不是最好的处理方式。因为文件读取失败可能是文件找不到,也有可能是权限问题,现在指定的这个恐慌信息"Somthing went wrong while reading the file"并不能帮助用户排查错误。

  • 如果程序里到处都使用expect方法那么用户得到的报错信息是来自于Rust语言内部的,比如"Index out of bound",不是程序员根本不明白到底是什么引发了错误。最好是将错误的代码集中放置,从而使将来的维护者在需要修改错误处理相关的逻辑时只考虑这一处代码,也能确保向用户打印的错误信息是易于理解的。

12.3.2. 二进制程序关注点分离的指导性原则

很多Rust二进制项目都会面临同样的组织结构问题,它们将过多的功能和过多的任务都放到了main函数里面。针对这种情况,Rust社区做了一套为二进制程序进行关注点分离的指导性原则:

  • 将程序拆分为main.rslib.rs,将业务逻辑放入lib.rs
  • 当逻辑较少时,将它放在main.rs也可以
  • 当逻辑变复杂时,需要将它从main.rs提取到lib.rs

经过上述拆分之后,这个例子中应该留在main函数中的功能有:

  • 使用参数值调用命令行解析逻辑
  • 进行其它配置
  • 调用lib.rs中的run函数
  • 处理run函数可能出现的问题

12.3.3. 分离逻辑

再看一眼代码:

use std::env;  
use std::fs;  
  
fn main() {  
    let args:Vec<String> = env::args().collect();  
    let query = &args[1];  
    let filename = &args[2];  
  
    println!("search for {}", query);  
    println!("In file {}", filename);  
  
    let contents = fs::read_to_string(filename)  
        .expect("Somthing went wrong while reading the file");
    println!("With text:\n{}", contents);  
}

先把获取命令行参数的部分独立出来:

fn parse_config(args: &[String]) -> (&str, &str) {  
    let query = &args[1];  
    let filename = &args[2];  
    (query, filename)  
}
  • &[String]表示是一个内部元素为StringVector切片
  • 这里没有打印queryfilename的必要了,所以就去掉

然后改一下main函数,调用parse_config

fn main() {  
    let args:Vec<String> = env::args().collect();  
    let (query, filename) = parse_config(&args);  
  
    let contents = fs::read_to_string(filename)  
        .expect("Somthing went wrong while reading the file");  
    println!("With text:\n{}", contents);  
}

12.3.4. 使用结构体

parse_config内把queryfilename组合成元组返回,在main函数里又把元组的两个值拆分为两个变量,这种来回拆分合成表明程序中建立的抽象结构有问题。

queryfilename都是配置的一部分,两者是彼此相关联的,把这两个东西放在元组里不足以表达出这种抽象的关联。最好的办法是放在结构体里:

struct Config {  
    query: String,  
    filename: String,  
}  
  
fn main() {  
    let args:Vec<String> = env::args().collect();  
    let config = parse_config(&args);  
  
    let contents = fs::read_to_string(config.filename)  
        .expect("Somthing went wrong while reading the file");  
    println!("With text:\n{}", contents);  
}  
  
fn parse_config(args: &[String]) -> Config {  
    let query = args[1].clone();  
    let filename = args[2].clone();  
    Config {  
        query,  
        filename,  
    }  
}

parse_config中必须注意queryfilename的格式:形参args的类型是&[String]是一个引用,没有所有权,所以queryfilename也是引用,但是Config这个结构体接收的是String而不是&String,所以需要通过克隆来获得所有权,把&String转为String

克隆虽然比直接存储引用消耗了更多时间和内存,但它省去了处理生命周期的麻烦,让代码更加直接简单。在某些场景中,放弃一些性能来获取更多的简洁性是非常值得考虑的

当然,使用String::from函数来封装也是可以的:

fn parse_config(args: &[String]) -> Config {  
    let query = &args[1];  
    let filename = &args[2];  
    Config {  
        query: String::from(query),  
        filename: String::from(filename),  
    }  
}

当然可行的代码可能不止这两种,这里我就采用第一种克隆的方法。

12.3.5. 把函数变为结构体的方法

既然parse_config会创建一个Config的实例,也就是说它是一个构造函数。对于构造函数,可以这么写:

impl Config {  
    fn new(args: &[String]) -> Config {  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        Config {  
            query,  
            filename,  
        }  
    }  
}

只需要把这个函数写在Config的方法上即可(对于方法的详细解释,详见 5.3. struct的方法(Method))。这里还给parse_config改了个名叫new,是因为我把它当作了一个构造函数来处理(构造函数一般都命名为new)。

这么改,main函数里面也需要改一下:

let config = Config::new(&args);

12.3.5. 整体代码

以下是截止到本篇文章所写出的所有代码:

use std::env;  
use std::fs;  
  
struct Config {  
    query: String,  
    filename: String,  
}  
  
fn main() {  
    let args:Vec<String> = env::args().collect();  
    let config = Config::new(&args);  
  
    let contents = fs::read_to_string(config.filename)  
        .expect("Somthing went wrong while reading the file");  
    println!("With text:\n{}", contents);  
}  
  
impl Config {  
    fn new(args: &[String]) -> Config {  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        Config {  
            query,  
            filename,  
        }  
    }  
}

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

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

相关文章

excel设置好的可选择列数据后,如何快速输入到单元格中?

当设置好列的【数据】-【数据有效性】-【序列】后&#xff0c;在单元格中输入可选择数据的开头&#xff0c;就会提示出对应的可选择数据&#xff0c;然后&#xff0c;按一下键盘上的【↓】键&#xff0c;再按回车&#xff0c;即可快速输入到单元格中。

Vue3.5 企业级管理系统实战(二):Router、Pinia 及 Element-Plus 集成

1 Vue Router 集成 1.1 安装 vue-router 通过 pnpm 安装 Vue Router pnpm i vue-router 1.2 配置 Router 在 src 文件夹下新建 views 文件夹&#xff0c;新建文件 Home.vue 和 About.vue 在 src 文件夹下新建 router 文件夹&#xff0c;在 router 下新建 index.ts 用来配置…

OPT: Open Pre-trained Transformer语言模型

摘要 大规模语言模型通常需要数十万计算日的训练时间&#xff0c;展现了在零样本和小样本学习中的显著能力。鉴于其计算成本之高&#xff0c;这些模型在没有大量资本投入的情况下难以复现。对于那些通过API提供的少数模型&#xff0c;研究者无法获取完整的模型权重&#xff0c…

【PDF转Word】 PDF在线转word文档 好用!优质网站资源推荐

大家在工作与学习中&#xff0c;经常需要将PDF文件转换为Word格式以便进行编辑和修改。很多人都不知道怎么操作&#xff0c;今天我们介绍一个非常好用的工具&#xff1a;小白工具网&#xff0c;可以在线帮忙大家快速把PDF转换成word格式。 小白工具网提供的PDF转Word功能&…

2025 年前端开发学习路线图完整指南

如果您想成为前端开发人员&#xff0c;本指南适合您。无论您是从零开始还是已经了解基础知识&#xff0c;它都会帮助您专注于真正重要的事情并学习让您脱颖而出的技能。 刚开始的时候&#xff0c;我浪费了几个月的时间在不相关的教程上&#xff0c;因为我不知道从哪里开始&…

妙用编辑器:把EverEdit打造成一个编程学习小环境

1 妙用编辑器&#xff1a;把EverEdit打造成一个编程学习小环境 1.1 应用场景 最近在学习Python语言&#xff0c;由于只是学习和练习&#xff0c;代码规模很小&#xff0c;不想惊动PyCharm、VSCode、WingIDE这些重型武器&#xff0c;只想轻快的敲些代码&#xff0c;记事本虽好&…

【江协STM32】10-4/5 I2C通信外设、硬件I2C读写MPU6050

1. I2C外设简介 STM32内部集成了硬件I2C收发电路&#xff0c;可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能&#xff0c;减轻CPU的负担支持多主机模型支持7位/10位地址模式支持不同的通讯速度&#xff0c;标准速度(高达100 kHz)&#xff0c;快速…

初识 Git——《Pro Git》

Why Git&#xff1f; 1. 本地版本控制系统 Why&#xff1a; 许多人习惯用复制整个项目目录的方式来保存不同的版本&#xff0c;或许还会改名加上备份时间以示区别。 这么做唯一的好处就是简单&#xff0c;但是特别容易犯错。 有时候会混淆所在的工作目录&#xff0c;一不小心…

记一次学习skynet中的C/Lua接口编程解析protobuf过程

1.引言 最近在学习skynet过程中发现在网络收发数据的过程中数据都是裸奔&#xff0c;就想加入一种数据序列化方式&#xff0c;json、xml简单好用&#xff0c;但我就是不想用&#xff0c;于是就想到了protobuf&#xff0c;对于protobuf C/C的使用个人感觉有点重&#xff0c;正好…

使用RSyslog将Nginx Access Log写入Kafka

个人博客地址&#xff1a;使用RSyslog将Nginx Access Log写入Kafka | 一张假钞的真实世界 环境说明 CentOS Linux release 7.3.1611kafka_2.12-0.10.2.2nginx/1.12.2rsyslog-8.24.0-34.el7.x86_64.rpm 创建测试Topic $ ./kafka-topics.sh --zookeeper 192.168.72.25:2181/k…

MySQL(行结构)

后面也会持续更新&#xff0c;学到新东西会在其中补充。 建议按顺序食用&#xff0c;欢迎批评或者交流&#xff01; 缺什么东西欢迎评论&#xff01;我都会及时修改的&#xff01; MySQL 一行记录是怎么存储的&#xff1f; | 小林coding MySQL原理 - InnoDB引擎 - 行记录存…

ros2笔记-6.2 使用urdf创建机器人模型

本节主要跟着小鱼老师的视频操作&#xff0c;不同的仿真平台有不同的建模语言&#xff0c;但是几乎都支持URDF。 本节使用URDF创建一个机器人模型。 6.2.1 帮机器人创建一个身体 URDF使用XML来描述机器人的结构和传感器、执行器等信息。 在chapt6/chap6_ws/src创建功能包:r…

基于mybatis-plus历史背景下的多租户平台改造

前言 别误会&#xff0c;本篇【并不是】 要用mybatis-plus自身的多租户方案&#xff1a;在表中加一个tenant_id字段来区分不同的租户数据。并不是的&#xff01; 而是在假设业务系统已经使用mybatis-plus多数据源的前提下&#xff0c;如何实现业务数据库隔开的多租户系统。 这…

【JAVA基础】Collections方法的具体使用方法

java基础中Collections及collect(toList,toSet,toMap)的用法 package com.gaofeng;import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream;public class demo01 {public static void main(String[] …

HDFS 的API的操作

3.1 客户端环境准备(windows) 1&#xff09;拷贝hadoop-3.1.X到非中文路径&#xff08;比如d:\&#xff09;。 2&#xff09;配置HADOOP_HOME环境变量 3&#xff09;配置Path环境变量。 注意&#xff1a;如果环境变量不起作用&#xff0c;可以重启电脑试试。 也可以直接添加…

【数据库】二、关系数据库

文章目录 二、关系数据库1 关系2 关系数据库3 完整性约束4 关系运算 二、关系数据库 1 关系 域&#xff1a;一组具有相同数据类型的值的集合。 笛卡尔积&#xff1a;所有域&#xff08;域可相同&#xff09;中所有取值的组合 例如&#xff1a;D1{1,2,3}&#xff0c;D2{A,b}&…

[笔记] 使用 Jenkins 实现 CI/CD :从 GitLab 拉取 Java 项目并部署至 Windows Server

随着软件开发节奏的加快&#xff0c;持续集成&#xff08;CI&#xff09;和持续部署&#xff08;CD&#xff09;已经成为确保软件质量和加速产品发布的不可或缺的部分。Jenkins作为一款广泛使用的开源自动化服务器&#xff0c;为开发者提供了一个强大的平台来实施这些实践。然而…

playwright 模拟登录

一、流程如下 C#代码&#xff1a; using Microsoft.Playwright; using Newtonsoft.Json; using System; using System.IO; using System.Net.Http; using System.Text; using System.Xml.Linq;namespace TestProject3 {[TestClass]public class UnitTest1 : PageTest{[TestMet…

使用大数据分析提升电子商务的转化率

&#x1f496; 欢迎来到我的博客&#xff01; 非常高兴能在这里与您相遇。在这里&#xff0c;您不仅能获得有趣的技术分享&#xff0c;还能感受到轻松愉快的氛围。无论您是编程新手&#xff0c;还是资深开发者&#xff0c;都能在这里找到属于您的知识宝藏&#xff0c;学习和成长…

HTB:Bastion[WriteUP]

目录 连接至HTB服务器并启动靶机 信息收集 使用rustscan对靶机TCP端口进行开放扫描 将靶机TCP开放端口号提取并保存 使用nmap对靶机TCP开放端口进行脚本、服务扫描 使用nmap对靶机TCP开放端口进行漏洞、系统扫描 使用nmap对靶机常用UDP端口进行开放扫描 使用enum4linux…