【Rust自学】12.5. 重构 Pt.3:移动业务逻辑

server/2025/1/18 3:53:41/

12.5.0. 写在正文之前

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

这个项目分为这么几步:

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

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

12.5.1. 回顾

之前两节分别做了模块化的优化和错误处理,这节在此基础上还要做进一步的优化。

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

rust">use std::env;  
use std::fs;  
use std::process;  struct Config {  query: String,  filename: String,  
}  fn main() {  let args:Vec<String> = env::args().collect();  let config = Config::new(&args).unwrap_or_else(|err| {  println!("Problem parsing arguments: {}", err);  process::exit(1);  });  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]) -> Result<Config, &'static str> {  if args.len() < 3 {  return Err("Not enough arguments");  }  let query = args[1].clone();  let filename = args[2].clone();  Ok(Config { query, filename})  }  
}

12.5.2. 从main函数中提取逻辑

在 12.3. 重构 Pt.1 中说过二进制程序关注点分离的指导性原则

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

根据上述拆分原则,我们应该把main函数里所有除了配置解析和错误处理之外的所有逻辑单独提取到一个run函数里。把main函数精简到足以通过阅读代码来检查正确性,而其他的逻辑就可以通过测试验证了(对于测试这部分的内容,详见第11章)。

对于这个截止到目前的代码,run函数应该是:

rust">fn run(config: Config) {  let contents = fs::read_to_string(config.filename)  .expect("Somthing went wrong while reading the file");  println!("With text:\n{}", contents);  
}

main函数里也改为通过调用run函数来读取:

rust">fn main() {  let args:Vec<String> = env::args().collect();  let config = Config::new(&args).unwrap_or_else(|err| {  println!("Problem parsing arguments: {}", err);  process::exit(1);  });  run(config);  
}

12.5.3. 改善run函数的错误处理

现在的run函数对于读取错误的情况采用的是expect。而这种错误处理会调用panic!,我们需要的是像config::new这样使用Result类型来传播错误,就应该这么写:

rust">fn run(config: Config) -> Result<(), Box<dyn Error>> {  let contents = fs::read_to_string(config.filename)?;  println!("With text:\n{}", contents);  Ok(())  
}
  • Result类型的Ok对应的是()类型(单元类型),这种类型表示什么也不返回,什么也没有,因为run函数正确执行确实什么都不需要返回。这个函数体的最后一行Ok()里加了()就代表返回Ok变体,并且包裹了一个单元类型。

  • ResultErr对应的是Box<dyn Error>,这个东西你暂且不需要深入了解,之需要知道它代表所有实现了std::error::Error这个trait的类型(这里只写了Error是因为我在代码开头写了use std::error::Error;,把它引入了作用域),但是不需要指定具体的类型。这意味着在不同的场景下可以返回不同的错误类型。dyn是dynamic动态一词的简写。

  • ?这个符号在 9.3. Result枚举与可恢复的错误 Pt.2 中有详细讲过,这里就再简单讲一下:read_to_string的返回值是Result类型。加了?表示如果read_to_string的返回值是Ok,就把Ok所关联的值返回赋值给变量;如果是Err,那么会直接终止这个函数的运行,把Err及其所附带的错误信息返回。也就是说,加?的效果等同于:

rust">let contents = match fs::read_to_string(config.filename){Ok(contents) => contents,Err(e) => return Err(e),
};

这么改之后就会把错误传播给调用者,也就是main函数,所以在main函数里得处理可能出现的错误:

rust">fn main() {  let args:Vec<String> = env::args().collect();  let config = Config::new(&args).unwrap_or_else(|err| {  println!("Problem parsing arguments: {}", err);  process::exit(1);  });  if let Err(e) = run(config) {  println!("Application error: {}", e);  process::exit(1);  }  
}

这里使用到的if letmatch的一个语法糖,把它理解为只处理一种分支的match即可,详细可见 6.4. 简单的控制流-if let。需要强调,if letif不是同一回事,不要把它们相提并论。

12.5.4. 迁移业务逻辑

现在我们完成了所有函数的独立和错误处理,接下来要做的就是把它们移到lib.rs里。

迁移的对象就是这些函数、结构体和相关的引用。

迁移后的成果(lib.rs):

rust">use std::error::Error;  
use std::fs;  pub struct Config {  pub query: String,  pub filename: String,  
}  impl Config {  pub fn new(args: &[String]) -> Result<Config, &'static str> {  if args.len() < 3 {  return Err("Not enough arguments");  }  let query = args[1].clone();  let filename = args[2].clone();  Ok(Config { query, filename})  }  
}  pub fn run(config: Config) -> Result<(), Box<dyn Error>> {  let contents = fs::read_to_string(config.filename)?;  println!("With text:\n{}", contents);  Ok(())  
}

注意:所有的被main.rs使用的结构体、结构体上的方法和函数都得在声明时加pub关键字来声明为公共的才能被调用。

再看看main.rs:

rust">use std::env;  
use std::process;  
use minigrep::Config;  fn main() {  let args:Vec<String> = env::args().collect();  let config = Config::new(&args).unwrap_or_else(|err| {  println!("Problem parsing arguments: {}", err);  process::exit(1);  });  if let Err(e) = minigrep::run(config) {  println!("Application error: {}", e);  process::exit(1);  }  
}

所有的重构任务已经完成,下一步就是编写测试(下一篇文章)。


http://www.ppmy.cn/server/159251.html

相关文章

PostMan测试webSocket接口(保姆级教程)

前言 小编我将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知识&#xff0c;有兴趣的小伙伴可以关注一下&#xff01; 也许一个人独行&#xff0c;可以走的很快&#xff0c;但是一群人结伴而行&#xff0c;才能走的更远&#xff01;让我们在成长的道路上互相学习&…

springboot 整合jsp

注意&#xff1a; 非专业技术人员用于研究 该项目已过时 pom.xml依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance&quo…

《深入理解Mybatis原理》Mybatis中的缓存实现原理

一级缓存实现 什么是一级缓存&#xff1f; 为什么使用一级缓存&#xff1f; 每当我们使用MyBatis开启一次和数据库的会话&#xff0c;MyBatis会创建出一个SqlSession对象表示一次数据库会话。 在对数据库的一次会话中&#xff0c;我们有可能会反复地执行完全相同的查询语句&…

《AI赋能鸿蒙Next,打造极致沉浸感游戏》

在游戏开发领域&#xff0c;鸿蒙Next系统与人工智能技术的结合为开发者们带来了前所未有的机遇&#xff0c;使打造更具沉浸感的游戏成为可能。以下将深入探讨如何利用人工智能在鸿蒙Next上开发出令人身临其境的游戏。 利用AI优化游戏角色智能行为 在传统游戏中&#xff0c;非…

【人工智能】Python中的自动化机器学习(AutoML):如何使用TPOT优化模型选择

《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门&#xff01; 解锁Python编程的无限可能&#xff1a;《奇妙的Python》带你漫游代码世界 随着机器学习在各行业的广泛应用&#xff0c;模型选择和优化成为了数据科学家面临的主要挑战之一。自动化机器学习&am…

【Java】LinkedHashMap (LRU)淘汰缓存的使用

文章目录 **1. initialCapacity&#xff08;初始容量&#xff09;****2. loadFactor&#xff08;加载因子&#xff09;****3. accessOrder&#xff08;访问顺序&#xff09;****完整参数解释示例****示例验证** LinkedHashMap 在 Java 中可维护元素插入或访问顺序&#xff0c;并…

微软震撼发布:Phi-4语言模型登陆Hugging Face

近日&#xff0c;微软公司在Hugging Face平台上正式发布了其最新的语言模型Phi-4&#xff0c;这一发布标志着人工智能技术的又一重要进步。Phi-4模型以其140亿参数的高效配置&#xff0c;在复杂推理任务中表现出色&#xff0c;特别是在数学领域&#xff0c;更是展现出了卓越的能…

MetaPhlAn2-增强版宏基因组分类谱工具-一条命令获得宏基因组物种组成

MetaPhlAn2是分析微生物群落(细菌、古菌、真核生物和病毒)组成的工具&#xff0c;它在宏基因组研究中非常有用&#xff0c;只需一条完命令即可获得微生物的物种丰度信息(扩增子物种组成需要质控、拼接、拆样本、切除引物、比对等步骤&#xff0c;此软件居然分析宏基因组这么方便…