Rust之构建命令行程序(三):重构改进模块化和错误处理

news2024/11/27 19:49:06

开发环境

  • Windows 10
  • Rust 1.74.1

 

  • VS Code 1.85.1

项目工程

这次创建了新的工程minigrep.

重构改进模块化和错误处理 

为了改进我们的程序,我们将修复与程序结构及其处理潜在错误的方式有关的四个问题。首先,我们的main函数现在执行两项任务:解析参数和读取文件。随着我们程序的增长,main处理的独立任务的数量也会增加。随着一个功能获得更多的职责,它变得更难推理、更难测试、更难在不破坏其某个部分的情况下进行更改。最好将功能分开,每个功能负责一项任务。

这个问题还与第二个问题有关:尽管queryfile_path是程序的配置变量,但contents等变量也用于执行程序的逻辑。main越长,我们需要纳入范围的变量就越多;范围内的变量越多,就越难跟踪每个变量的用途。最好将配置变量分组到一个结构中,以使其目的明确。 

第三个问题是,我们已经使用expect在读取文件失败时打印了一条错误消息,但错误消息只是打印了Should have been able to read the file。读取文件失败的原因有很多:例如,文件可能丢失,或者我们可能没有权限打开它。现在,不管情况如何,我们都会为所有内容打印相同的错误消息,这不会给用户任何信息! 

第四,我们重复使用expect来处理不同的错误,如果用户在没有指定足够参数的情况下运行我们的程序,他们将从Rust获得一个索引越界错误,该错误无法清楚地解释问题。如果所有的错误处理代码都在一个地方是最好的,这样将来的维护人员在需要更改错误处理逻辑时就只有一个地方可以查阅代码。将所有错误处理代码放在一个地方还将确保我们打印的消息对最终用户有意义。 

让我们通过重构项目来解决这四个问题。 

二进制项目的关注点分离

将多项任务的责任分配给main功能的组织问题在许多二元项目中很常见。因此,Rust社区开发了当main开始变大时分割二进制程序的独立关注点的指导方针。该过程包括以下步骤: 

  • 将你的程序分成一个main.rs和一个lib.rs,并将你的程序逻辑转移到lib.rs。 
  • 只要您的命令行解析逻辑很小,它就可以保留在main.rs中。
  • 当命令行解析逻辑开始变得复杂时,将其从main.rs中提取出来并移动到lib.rs中。

在此流程之后,main函数的职责应仅限于以下方面: 

  • 使用参数值调用命令行解析逻辑
  • 设置其他配置
  • lib.rs中调用运行函数 
  • run返回错误时处理错误

这种模式是关于分离关注点的:main.rs处理程序的运行,lib.rs处理手头任务的所有逻辑。因为您不能直接测试main函数,所以这种结构允许您通过将程序的逻辑移入lib.rs中的函数来测试程序的所有逻辑。保留在main.rs中的代码将足够小,可以通过读取它来验证其正确性。让我们按照这个过程重新编写我们的程序。 

提取参数解析器

我们将解析参数的功能提取到一个函数中,main将调用该函数来准备将命令行解析逻辑移动到src/lib.rs .示例12-5显示了main调用新函数parse_config的新开始,我们暂时将在src/main.rs中定义该函数。 

文件名:src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();

    let (query, file_path) = parse_config(&args);

    // --snip--
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let file_path = &args[2];

    (query, file_path)
}

 示例12-5 从main中提取parse_config函数

 我们仍然将命令行参数收集到一个向量中,但是我们没有将索引1处的参数值分配给变量query,也没有将索引2处的参数值分配给主函数中的变量file_path,而是将整个向量传递给parse_config函数。parse_config函数然后保存逻辑,该逻辑确定哪个参数进入哪个变量并将值传递回main。我们仍然在main中创建queryfile_path变量,但是main不再负责确定命令行参数和变量如何对应。

对于我们的小程序来说,这种返工似乎有些过头了,但我们正在以小而渐进的步骤进行重构。在做出这一更改后,再次运行程序以验证参数解析是否仍然有效。经常检查你的进展是有好处的,有助于在问题出现时找出问题的原因。

对配置值进行分组

我们可以采取另一个小步骤来进一步改进parse_config函数。目前,我们正在返回一个元组,但随后我们立即再次将该元组拆分为单独的部分。这表明我们可能还没有正确的抽象概念。

另一个显示还有改进空间的指标是parse_configconfig部分,这意味着我们返回的两个值是相关的,并且都是一个配置值的一部分。除了将这两个值分组到一个元组中之外,我们目前没有在数据结构中传达这种含义;相反,我们将把这两个值放入一个结构中,并为每个结构字段赋予一个有意义的名称。这样做将使该代码的未来维护者更容易理解不同值之间的关系以及它们的用途。 

示例12-6显示了对parse_config函数的改进。 

文件名:src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    // --snip--
}

struct Config {
    query: String,
    file_path: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let file_path = args[2].clone();

    Config { query, file_path }
}

 示例12-6:重构parse_config以返回一个Config结构的实例

我们添加了一个名为Config的结构,该结构定义了名为queryfile_path的字段。parse_config的签名现在表明它返回一个配置值。在parse_config的主体中,我们曾经返回引用args中的字符串值的字符串片段,现在我们将Config定义为包含拥有的字符串值。main中的args变量是参数值的所有者,并且只让parse_config函数借用它们,这意味着如果Config试图获取args中的值的所有权,我们将违反Rust的借用规则。 

我们可以通过多种方式管理String数据;最简单的方法是对值调用clone方法,尽管这种方法有些低效。这将为Config实例创建数据的完整副本,这比存储对字符串数据的引用花费更多的时间和内存。然而,克隆数据也使我们的代码非常简单,因为我们不必管理引用的生存期;在这种情况下,牺牲一点性能来获得简单性是值得的。 

使用克隆的利弊

许多Rust开发者倾向于避免使用clone来解决所有权问题,因为它的运行时成本很高。在后续章节中,你将学习如何在这种情况下使用更有效的方法。但是现在,复制几个字符串以继续取得进展是可以的,因为您将只复制这些副本一次,并且您的文件路径和查询字符串非常小。最好有一个效率稍低的工作程序,而不是第一次就试图过度优化代码。随着您对Rust的经验越来越丰富,开始使用最有效的解决方案将变得更加容易,但就目前而言,调用clone是完全可以接受的。 

我们已经更新了main,因此它将parse_config返回的config实例放入名为config的变量中,并且我们更新了以前使用单独的queryfile_path变量的代码,因此它现在改为使用Config结构上的字段。 

现在我们的代码更清楚地传达了queryfile_path是相关的,它们的目的是配置程序将如何工作。任何使用这些值的代码都知道在config实例中为其目的命名的字段中找到它们。

为配置创建构造函数

到目前为止,我们已经从main中提取了负责解析命令行参数的逻辑,并将其放在parse_config函数中。这样做有助于我们看到查询和file_path值是相关的,并且这种关系应该在我们的代码中传达。然后,我们添加了一个配置结构来命名queryfile_path的相关用途,并能够从parse_config函数中将值的名称作为结构字段名返回。 

既然parse_config函数的目的是创建一个配置实例,我们可以将parse_config从一个普通函数更改为一个名为new的函数,该函数与配置结构相关联。进行这一更改将使代码更符合习惯。我们可以通过调用String::new在标准库中创建类型的实例,例如String。类似地,通过将parse_config更改为与config相关联的新函数,我们将能够通过调用Config::new来创建Config的实例。示例12-7显示了我们需要进行的更改。 

文件名:src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();

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

    // --snip--
}

// --snip--

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

        Config { query, file_path }
    }
}

 示例12-7:将parse_config更改为Config::new

我们更新了main中调用parse_config的地方,改为调用Config::new。我们已经将parse_config的名称改为new,并将其移动到impl块中,该块将新函数与config相关联。请再次尝试编译此代码以确保它正常工作。

修复错误处理

现在我们将修复我们的错误处理。回想一下,如果args向量包含的项少于三项,则尝试访问索引1或索引2处的args向量中的值将导致程序崩溃。尝试不带任何参数运行程序;它将看起来像这样:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

 index out of bounds: the len is 1 but the index is 1,是一条针对程序员的错误消息。它不会帮助我们的最终用户理解他们应该做什么。让我们现在解决这个问题。

改进错误信息

在示例12-8中,我们在new函数中添加了一个检查,它将在访问索引1和2之前验证切片是否足够长。如果切片不够长,程序会惊慌并显示更好的错误消息。 

文件名:src/main.rs

    // --snip--
    fn new(args: &[String]) -> Config {
        if args.len() < 3 {
            panic!("not enough arguments");
        }
        // --snip--

 示例12-8:添加对参数数量的检查

value参数超出有效值范围时。在这里,我们不是检查值的范围,而是检查args的长度至少为3,并且函数的其余部分可以在满足该条件的假设下运行。如果args的项目少于三个,则该条件将为真,我们称之为panic!宏立即结束程序。 

有了new中的这几行额外代码,让我们再次不带任何参数运行程序,看看错误现在是什么样子的: 

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at 'not enough arguments', src/main.rs:26:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

 这个输出更好:我们现在有了一个合理的错误消息。然而,我们也有不想给用户的无关信息。也许使用我们在之前的示例9-13中使用的技术在这里不是最好的:恐慌的呼唤!更适合于编程问题而不是使用问题。

返回结果而不是调用panic!

相反,我们可以返回一个Result,该值在成功的情况下将包含一个Config实例,并在错误的情况下描述问题。我们还将把函数名从new改为build,因为许多程序员希望新函数永远不会失败。当Config::buildmain通信时,我们可以使用结果类型来表示有问题。然后,我们可以更改main,将Err变体转换为更实用的错误,这样用户就不用担心周围的关于线程“main”和RUST_BACKTRACEpanic!原因。

示例12-9显示了我们需要对我们现在调用的Config::build函数的返回值以及返回结果所需的函数体进行的更改。请注意,直到我们更新main之后才会编译,我们将在下一个清单中更新main。 

文件名:src/main.rs

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

 示例12-9:从Config::build返回Result

我们的build函数在成功情况下返回一个带有Config实例的Result,在错误情况下返回一个&‘static str。我们的错误值将总是具有“static生存期”的字符串文字。 我们在函数体中做了两处修改:不再调用panic!当用户没有传递足够的参数时,我们现在返回一个Err值,并且我们已经将配置返回值包装在Ok中。这些更改使函数符合其新的类型签名。 

Config::build返回一个Err值允许main函数处理从build函数返回的Result,并在出错时更干净地退出进程。 

调用Config::build并处理错误

为了处理错误情况并打印用户友好的消息,我们需要更新main来处理Config::build返回的Result,如示例12-10所示。我们还将负责退出命令行工具,并给出一个非零错误代码,以免引起panic!而是手动实现。非零退出状态是一种惯例,它向调用我们程序的进程发出信号,表明程序以错误状态退出。

文件名:src/main.rs

use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

示例12-10:如果构建Config失败,退出并显示错误代码

在这个示例中,我们使用了一个尚未详细介绍的方法:unwrap_or_else,它是由标准库在Result<T, E>上定义的。使用unwrap_or_else允许我们定义一些自定义的、非紧急的!错误处理。如果结果是一个Ok值,该方法的行为类似于unwrap:它返回Ok正在换行的内部值。但是,如果该值是一个Err值,该方法将调用闭包中的代码,这是一个我们定义的匿名函数,并作为参数传递给unwrap_or_else。我们将在后续章节更详细地讨论闭包。现在,您只需要知道unwrap_or_else将把err的内部值传递给出现在竖线之间的参数Err中的闭包,在本例中,该值是我们在示例12-9中添加的静态字符串“not follow arguments”。闭包中的代码可以在运行时使用err值。 

我们添加了一个新的use行,将标准库中的process纳入范围。在错误情况下运行的闭包中的代码只有两行:我们打印err值,然后调用process::exitprocess::exit函数将立即停止程序并返回作为退出状态代码传递的数字。这类似于panic!我们在示例12-8中使用的基于的处理,但是我们不再得到所有额外的输出。让我们来试试:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments

太好了!这个输出对我们的用户更友好。

从main提取逻辑

现在我们已经完成了配置解析的重构,让我们转向程序的逻辑。正如我们在“二进制项目的关注点分离”中所述,我们将提取一个名为run的函数,该函数将保存main函数中当前不涉及设置配置或处理错误的所有逻辑。当我们完成时,main将简洁并易于检查验证,我们将能够为所有其他逻辑编写测试。

示例12-11显示了提取的run函数。目前,我们只是对提取函数进行了微小的、渐进的改进。我们仍在定义src/main.rs中的函数。

文件名:src/main.rs

fn main() {
    // --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) {
    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

// --snip--

示例12-11:提取包含剩余程序逻辑的run函数

run函数现在包含main中所有剩余的逻辑,从读取文件开始。run函数将Config实例作为参数。 

从运行函数返回错误

通过将剩余的程序逻辑分离到run函数中,我们可以改进错误处理,就像我们在示例12-9中对Config::build所做的那样。当出现问题时,run函数将返回Result<T, E>,而不是通过调用expect让程序崩溃。这将让我们以用户友好的方式进一步巩固main中处理错误的逻辑。示例12-12显示了我们需要对run的签名和主体进行的更改。 

文件名:src/main.rs

use std::error::Error;

// --snip--

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    println!("With text:\n{contents}");

    Ok(())
}

示例12-12:将run函数更改为返回Result

我们在这里做了三个重大改变。首先,我们将run函数的返回类型更改为Result<(),Box<dyn Error>>。该函数先前返回了单元类型(),我们将其作为Ok情况下的返回值。 

对于错误类型,我们使用了trait对象框《dyn Error》(并且我们将std::error::Error纳入了范围,并在顶部使用了一条use语句)。我们将在第17章讨论特征对象。现在,只需知道Box《dyn Error》意味着函数将返回实现错误特征的类型,但我们不必指定返回值将是什么特定类型。这给了我们在不同错误情况下返回不同类型的错误值的灵活性。dyn关键字是“动态”的缩写 

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

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

相关文章

Find My相机|苹果Find My技术与相机结合,智能防丢,全球定位

相机是一种利用光学成像原理形成影像并使用底片记录影像的设备&#xff0c;是用于摄影的光学器械。相机让我们能够记录下美丽的风景和珍贵的时刻。当我们到达一个迷人的地方,或者经历了一个特别难忘的时刻时,我们可以使用照相机来拍摄照片,记录下这些美好的回忆。照相机可以帮助…

最新 生成pdf文字和表格

生成pdf文字和表格 先看效果 介绍 java项目&#xff0c;使用apache的pdfbox工具&#xff0c;可分页&#xff0c;自定义列 依赖 <dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>2.0.22<…

鸿蒙开发(四)UIAbility和Page交互

通过上一篇的学习&#xff0c;相信大家对UIAbility已经有了初步的认知。在上篇中&#xff0c;我们最后实现了一个小demo&#xff0c;从一个UIAbility调起了另外一个UIAbility。当时我提到过&#xff0c;暂不实现比如点击EntryAbility中的控件去触发跳转&#xff0c;而是在Entry…

Qt编程之仿gnome-terminal终端样式 +颜色文字显示

Qt仿linux 终端样式 颜色文字 1.说再多废话不如直接show code2.实现效果 本文采用QTextBrowser作为文本显示窗口&#xff0c;进行文本的显示。本文实例实现的效果并没有终端的输入效果&#xff0c;这里只是提供一些仿终端样式思路。 1.说再多废话不如直接show code 1.ui文件…

Openwrt 下动态路由协议(quagga-OSPF)配置与验证

文章目录 前言网络拓扑静态路由方式动态路由方式Openwrt下 Quagga 安装Quagga 配置R1路由器zebra配置R1路由器ospf配置R2路由器zebra配置R2路由器ospf配置OSPF协议分析REF本文将在两台openwrt系统上安装配置quagga, 搭建一套完整环境,来验证OSPF动态路由的基本功能和实际效果,…

关于变量在多个.C文件中使用(Undefined symbol tempbuff (referred from main.o).问题解决)

1、如图结构&#xff0c;想在multimenu.C和usart1.c中使用变量tempbuff 于是&#xff0c;就将使用的代码移动了过来&#xff0c;为SetTxData1_toLCD(0x00,0x01);和UserUart1Send( tempbuff1,sizeof(tempbuff1));&#xff0c;编译后提示错误如下&#xff1a; communication_prot…

从零开始学习Python基础语法:打开编程大门的钥匙

文章目录 一、引言1.1 Python作为一种流行的编程语言的介绍1.2 Python的应用领域和适用性 二、为什么选择Python2.1 Python的简洁性和易读性2.2 Python的跨平台特性 三、Python在数据科学和人工智能领域的应用3.1 第一个Python程序3.1.1 Hello, World!&#xff1a;编写并运行你…

XXL-Job的搭建接入Springboot项目(详细)

一、XXL-Job介绍 XXL-Job 是一款开源的分布式任务调度平台&#xff0c;由 Xuxueli&#xff08;徐雪里&#xff09;开发。它基于 Java 技术栈&#xff0c;提供了一套简单易用、高可靠性的任务调度解决方案。 XXL-Job 的主要作用是帮助开发者实现定时任务的调度和执行。它可以用…

安装脚手架Vue CLI详解!!!

Vue CLI基本介绍&#xff1a; Vue CLI是Vue官方提供的一个全局命令工具。可以帮助我们快速创建一个开发Vue项目的标准化基础架子【集成了webpack配置】 安装脚手架好处&#xff1a; 开箱即用&#xff0c;零配置&#xff1b;内置babel等工具&#xff1b;标准化 安装步骤&#…

vscode中关于python的一些常用配置

文章目录 python cv2 提示配置第一步 配置提示信息第二部 重启vs 可能还不行&#xff0c;那就重新安装以下opencv-python 配置pytest还是如上&#xff0c;将下入的位置打开编写测试用例 配置跨文件import在工作目录中新建一个.env文件输入内容如下打开.vscode中的setting.json …

react umi/max 封装页签组件

1. models/tabs // 全局共享数据示例 import { useState } from react;const useUser () > {const [items, setItems] useState<any[]>([]); // 页签的全局Item数据const [key, setKey] useState<string>(/home); // 页签的高亮Keyreturn {items,setItems…

leetcode热题100.路径总和 III

Problem: 437. 路径总和 III 文章目录 题目思路1复杂度1Code1思路2复杂度2Code2 题目 给定一个二叉树的根节点 root &#xff0c;和一个整数 targetSum &#xff0c;求该二叉树里节点值之和等于 targetSum 的 路径 的数目。 路径 不需要从根节点开始&#xff0c;也不需要在叶…

Leetcode:128. 最长连续序列

128. 最长连续序列 乍一看感觉很简单&#xff0c;一看要用O(n)??? 因为我觉得题目很难而且题目看起来很简单&#xff0c;感觉以后会用到&#x1f606;&#xff0c;做个记录 1.朴素做法 思路 答:任何一段连续的数都有一个左端点&#xff1a;比如&#xff08;1&#xff0c;…

TCP 拥塞控制对数据延迟的影响

哈喽大家好&#xff0c;我是咸鱼 今天分享一篇文章&#xff0c;是关于 TCP 拥塞控制对数据延迟产生的影响的。作者在服务延迟变高之后进行抓包分析&#xff0c;结果发现时间花在了 TCP 本身的机制上面&#xff1a;客户端并不是将请求一股脑发送给服务端&#xff0c;而是只发送…

web架构师编辑器内容-编辑器组件图层面板功能开发01-锁定隐藏功能的开发

我们这一部分主要是对最右侧图层面板功能进行剖析&#xff0c;完成对应的功能的开发: 每个图层都对应编辑器上面的元素&#xff0c;有多少个元素就对应多少个图层&#xff0c;主要的功能如下&#xff1a; 锁定功能&#xff1a;点击锁定&#xff0c;在编辑器中没法编辑对应的组…

git 常规操作及设置

git 常规操作及设置 Git是一个分布式版本控制系统&#xff0c;可以用来跟踪文件的修改历史并与其他人进行协作开发。下面是一些常见的Git操作及设置&#xff1a; 初始化仓库&#xff1a;使用命令git init在当前目录创建一个新的Git仓库。 克隆仓库&#xff1a;使用命令git clo…

web terminal - 如何在mac os上运行gotty

gotty可以让你使用web terminal的方式与环境进行交互&#xff0c;实现终端效果 假设你已经配置好了go环境&#xff0c;首先使用go get github.com/yudai/gotty命令获取可执行文件&#xff0c;默认会安装在$GOPATH/bin这个目录下&#xff0c;注意如果你的go版本比较高&#xff…

C++设计模式(李建忠)笔记3

C设计模式&#xff08;李建忠&#xff09; 本文是学习笔记&#xff0c;如有侵权&#xff0c;请联系删除。 参考链接 Youtube: C设计模式 Gtihub源码与PPT&#xff1a;https://github.com/ZachL1/Bilibili-plus 豆瓣: 设计模式–可复用面向对象软件的基础 文章目录 C设计模…

线性代数的学习和整理23:用EXCEL计算 向量/向量组的点乘 (内积) (建设ing)

目录 前言&#xff1a;EXCEL里的的向量相关计算公式 0.1 EXCEL里相关公式 0.2 先说结论&#xff1a;向量组的点乘公式和 向量组的点乘公式不一样 1 向量的点乘 (内积) 1.1 向量的点乘公式 1.2 EXCEL里向量点乘的计算 ​编辑 1.3 向量点乘的性质 1.3.1 内积的公式…

docker环境下mongo副本集的部署及异常修复

最近更换了办公地点。部署在本地docker环境里的mongo数据库不能使用了。原因是本地的ip地址变更。以前的mongo副本集的配置需要更新。处理完后&#xff0c;索性重新记录一下mongo副本集在docker中的部署流程。 mongo的事务及副本集 我们先了解一下什么是事务&#xff0c;事务…