【Rust】——项目实例:——命令行实例(一)

💻博主现有专栏:

                C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等
🥏主页链接:

                Y小夜-CSDN博客

目录

🎯接收命令行程序

🎃读取参数

🎃将参数值保存进变量

🎯读取文件

🎯重构

🎃二进制项目的关注分离

🎃提取参数解析器

🎃组合配置值

🎃修复错误处理

✨改善错误信息

🎃从new中返回一个Result

🎃调用config::build并处理错误

🎃从main提取逻辑

🎃从run函数中返回错误

🎃处理main中run返回的错误

🎃将代码拆分到库crate


🎯接收命令行程序

        使用 cargo new 新建一个项目,我们称之为 minigrep 以便与可能已经安装在系统上的 grep 工具相区别:

$ cargo new minigrep
     Created binary (application) `minigrep` project
$ cd minigrep

        第一个任务是让 minigrep 能够接受两个命令行参数:文件路径和要搜索的字符串。也就是说我们希望能够使用 cargo run、要搜索的字符串和被搜索的文件的路径来运行程序,像这样:

$ cargo run -- searchstring example-filename.txt

🎃读取参数

     为了确保 minigrep 能够获取传递给它的命令行参数的值,我们需要一个 Rust 标准库提供的函数 std::env::args。这个函数返回一个传递给程序的命令行参数的 迭代器iterator)。

use std::env;

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

        首先使用 use 语句来将 std::env 模块引入作用域以便可以使用它的 args 函数。注意 std::env::args 函数被嵌套进了两层模块中。

        提醒: std::env::args 在其任何参数包含无效 Unicode 字符时会 panic。如果你需要接受包含无效 Unicode 字符的参数,使用 std::env::args_os 代替。这个函数返回 OsString 值而不是 String 值。这里出于简单考虑使用了 std::env::args,因为 OsString 值每个平台都不一样而且比 String 值处理起来更为复杂。

🎃将参数值保存进变量

        目前程序可以访问指定为命令行参数的值。现在需要将这两个参数的值保存进变量这样就可以在程序的余下部分使用这些值了。

use std::env;

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

    let query = &args[1];
    let file_path = &args[2];

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

        正如之前打印出 vector 时所所看到的,程序的名称占据了 vector 的第一个值 args[0],所以我们从索引为 1 的参数开始。minigrep 获取的第一个参数是需要搜索的字符串,所以将其将第一个参数的引用存放在变量 query 中。第二个参数将是文件路径,所以将第二个参数的引用放入变量 file_path 中。

🎯读取文件

        现在我们要增加读取由 file_path 命令行参数指定的文件的功能。首先,需要一个用来测试的示例文件:我们会用一个拥有多行少量文本且有一些重复单词的文件。

I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
use std::env;
use std::fs;

fn main() {
    // --snip--
    println!("In file {}", file_path);

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

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

        首先,我们增加了一个 use 语句来引入标准库中的相关部分:我们需要 std::fs 来处理文件。

        在 main 中新增了一行语句:fs::read_to_string 接受 file_path,打开文件,接着返回包含其内容的 std::io::Result<String>

        在这些代码之后,我们再次增加了临时的 println! 打印出读取文件之后 contents 的值,这样就可以检查目前为止的程序能否工作。

🎯重构

        为了改善我们的程序这里有四个问题需要修复,而且它们都与程序的组织方式和如何处理潜在错误有关。

        第一,main 现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果 main 中的功能持续增加,main 函数处理的独立任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数就负责一个任务。

        这同时也关系到第二个问题:query 和 file_path 是程序中的配置变量,而像 contents 则用来执行程序逻辑。随着 main 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构,这样就能使它们的目的更明确了。

        第三个问题是如果打开文件失败我们使用 expect 来打印出错误信息,不过这个错误信息只是说 Should have been able to read the file。读取文件失败的原因有多种:例如文件不存在,或者没有打开此文件的权限。目前,无论处于何种情况,我们只是打印出“文件读取出现错误”的信息,这并没有给予使用者具体的信息!

        第四,我们不停地使用 expect 来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 index out of bounds 错误,而这并不能明确地解释问题。如果所有的错误处理都位于一处,这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。

🎃二进制项目的关注分离

   main 函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在 main 函数开始变得庞大时进行二进制程序的关注分离的指导。这些过程有如下步骤:

  • 将程序拆分成 main.rs 和 lib.rs 并将程序的逻辑放入 lib.rs 中。
  • 当命令行解析逻辑比较小时,可以保留在 main.rs 中。
  • 当命令行解析开始变得复杂时,也同样将其从 main.rs 提取到 lib.rs 中。

经过这些过程之后保留在 main 函数中的责任应该被限制为:

  • 使用参数值调用命令行解析逻辑
  • 设置任何其他的配置
  • 调用 lib.rs 中的 run 函数
  • 如果 run 返回错误,则处理这个错误

        这个模式的一切就是为了关注分离:main.rs 处理程序运行,而 lib.rs 处理所有的真正的任务逻辑。因为不能直接测试 main 函数,这个结构通过将所有的程序逻辑移动到 lib.rs 的函数中使得我们可以测试它们。仅仅保留在 main.rs 中的代码将足够小以便阅读就可以验证其正确性。

🎃提取参数解析器

        首先,我们将解析参数的功能提取到一个 main 将会调用的函数中,为将命令行解析逻辑移动到 src/lib.rs 中做准备。示例中展示了新 main 函数的开头,它调用了新函数 parse_config。目前它仍将定义在 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)
}

        我们仍然将命令行参数收集进一个 vector,不过不同于在 main 函数中将索引 1 的参数值赋值给变量 query 和将索引 2 的值赋值给变量 file_path,我们将整个 vector 传递给 parse_config 函数。接着 parse_config 函数将包含决定哪个参数该放入哪个变量的逻辑,并将这些值返回到 main。仍然在 main 中创建变量 query 和 file_path,不过 main 不再负责处理命令行参数与变量如何对应。

🎃组合配置值

        我们可以采取另一个小的步骤来进一步改善这个函数。现在函数返回一个元组,不过立刻又将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。

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 }
}

        新定义的结构体 Config 中包含字段 query 和 file_path。 parse_config 的签名表明它现在返回一个 Config 值。在之前的 parse_config 函数体中,我们返回了引用 args 中 String 值的字符串 slice,现在我们定义 Config 来包含拥有所有权的 String 值。main 中的 args 变量是参数值的所有者并只允许 parse_config 函数借用它们,这意味着如果 Config 尝试获取 args 中值的所有权将违反 Rust 的借用规则。

🎃修复错误处理

        现在我们开始修复错误处理。回忆一下之前提到过如果 args vector 包含少于 3 个项并尝试访问 vector 中索引 1 或索引 2 的值会造成程序 panic。尝试不带任何参数运行程序;这将看起来像这样:

$ 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

✨改善错误信息

        在 new 函数中增加了一个检查在访问索引 1 和 2 之前检查 slice 是否足够长。如果 slice 不够长,程序会打印一个更好的错误信息并 panic:

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

        这个输出就好多了,现在有了一个合理的错误信息。然而,还是有一堆额外的信息我们不希望提供给用户。

🎃从new中返回一个Result

        我们可以选择返回一个 Result 值,它在成功时会包含一个 Config 的实例,而在错误时会描述问题。我们还将把函数名从new改为build,因为许多程序员希望 new 函数永远不会失败。当 Config::new 与 main 交流时,可以使用 Result 类型来表明这里存在问题。接着修改 main 将 Err 成员转换为对用户更友好的错误,而不是 panic! 调用产生的关于 thread 'main' 和 RUST_BACKTRACE 的文本。

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 })
    }
}

   build 函数体中有两处修改:当没有足够参数时不再调用 panic!,而是返回 Err 值。同时我们将 Config 返回值包装进 Ok 成员中。这些修改使得函数符合其新的类型签名。

        通过让 Config::build 返回一个 Err 值,这就允许 main 函数处理 build 函数返回的 Result 值并在出现错误的情况更明确的结束进程。

🎃调用config::build并处理错误

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--

        在上面的示例中,使用了一个之前没有详细说明的方法:unwrap_or_else,它定义于标准库的 Result<T, E> 上。使用 unwrap_or_else 可以进行一些自定义的非 panic! 的错误处理。当 Result 是 Ok 时,这个方法的行为类似于 unwrap:它返回 Ok 内部封装的值。然而,当其值是 Err 时,该方法会调用一个 闭包closure),也就是一个我们定义的作为参数传递给 unwrap_or_else 的匿名函数。

        我们新增了一个 use 行来从标准库中导入 process。在错误的情况闭包中将被运行的代码只有两行:我们打印出了 err 值,接着调用了 std::process::exitprocess::exit 会立即停止程序并将传递给它的数字作为退出状态码。这类似于示例 12-8 中使用的基于 panic! 的错误处理,除了不会再得到所有的额外输出了。

🎃从main提取逻辑

现在我们完成了配置解析的重构:让我们转向程序的逻辑。

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--

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

🎃从run函数中返回错误

通过将剩余的逻辑分离进 run 函数而不是留在 main 中,

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(())
}

        这里我们做出了三个明显的修改。首先,将 run 函数的返回类型变为 Result<(), Box<dyn Error>>。之前这个函数返回 unit 类型 (),现在它仍然保持作为 Ok 时的返回值。

对于错误类型,使用了 trait 对象 Box<dyn Error>(在开头使用了 use 语句将 std::error::Error 引入作用域)。

        第二个改变是去掉了 expect 调用并替换为 第九章 讲到的 ?。不同于遇到错误就 panic!? 会从函数中返回错误值并让调用者来处理它。

        第三个修改是现在成功时这个函数会返回一个 Ok 值。因为 run 函数签名中声明成功类型返回值是 (),这意味着需要将 unit 类型值包装进 Ok 值中。Ok(()) 一开始看起来有点奇怪,不过这样使用 () 是惯用的做法,表明调用 run 函数只是为了它的副作用;函数并没有返回什么有意义的值。

🎃处理main中run返回的错误

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

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

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

        我们使用 if let 来检查 run 是否返回一个 Err 值,不同于 unwrap_or_else,并在出错时调用 process::exit(1)run 并不返回像 Config::build 返回的 Config 实例那样需要 unwrap 的值。因为 run 在成功时返回 (),而我们只关心检测错误,所以并不需要 unwrap_or_else 来返回未封装的值,因为它只会是 ()

🎃将代码拆分到库crate

现在我们的 minigrep 项目看起来好多了!现在我们将要拆分 src/main.rs 并将一些代码放入 src/lib.rs,这样就能测试它们并拥有一个含有更少功能的 main 函数。

让我们将所有不是 main 函数的代码从 src/main.rs 移动到新文件 src/lib.rs 中:

  • run 函数定义
  • 相关的 use 语句
  • Config 的定义
  • Config::build 函数定义

现在 src/lib.rs 的内容应该看起来像示例 12-13(为了简洁省略了函数体)。注意直到下一个示例修改完 src/main.rs 之后,代码还不能编译:

use std::error::Error;
use std::fs;

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

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        // --snip--
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    // --snip--
}

        这里使用了公有的 pub 关键字:在 Config、其字段和其 build 方法,以及 run 函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。

        现在需要在 src/main.rs 中将移动到 src/lib.rs 的代码引入二进制 crate 的作用域中

use std::env;
use std::process;

use minigrep::Config;

fn main() {
    // --snip--
    if let Err(e) = minigrep::run(config) {
        // --snip--
    }
}

        我们添加了一行 use minigrep::Config,它将 Config 类型引入作用域,并使用 crate 名称作为 run 函数的前缀。通过这些重构,所有功能应该能够联系在一起并运行了。运行 cargo run 来确保一切都正确的衔接在一起。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Vision Pro应用】分享一个收集Apple Vision Pro 应用的网站

您是否也觉得 Vision Pro 应用程序商店经常一遍又一遍地展示相同的几个 VisionOS 应用程序?许多有趣、好玩的应用程序似乎消失得无影无踪,让人很难发现它们。为了帮助大家更轻松地探索和体验最新、最有趣的 Vision Pro 应用程序,这里分享一个网站https://www.findvisionapp.…

IDEA @Autowired不显示红线

IDEA 中&#xff0c;Autowired 显示红线一般情况是注入 Mapper 或者 Dao 时出现的&#xff0c;如下图&#xff1a; 这个报错是因为 Mapper 或者 Dao 上没有加 Repository 或者 Mapper&#xff0c;Autowired 注入时就判断为这不是一个 Bean。 不建议通过加上面两个注解的方式解…

Java面试八股之hashCode()和equals()方法的重要性

hashCode()和equals()方法的重要性 逻辑判断&#xff1a;equals()方法用于定义对象逻辑上的相等标准&#xff0c;即当两个对象在业务意义上被视为“相同”时&#xff0c;equals()应返回true。 哈希表支持&#xff1a;hashCode()返回一个整数哈希码&#xff0c;用于在哈希表&a…

【电路笔记】-数字逻辑门总结

数字逻辑门总结 文章目录 数字逻辑门总结1、概述2、逻辑门真值表3、总结 数字逻辑门有三种基本类型&#xff1a;与门、或门和非门。 1、概述 我们还看到&#xff0c;数字逻辑门具有与其相反或互补的形式&#xff0c;分别为“与非门”、“或非门”和“缓冲器”&#xff0c;并且…

AIGC的崛起:定义未来内容创作的新纪元

&#x1f31f;文章目录 &#x1f31f;AIGC简介&#x1f31f; AIGC的相关技术与特点&#x1f31f;AIGC有哪些应用场景&#xff1f;&#x1f31f;AIGC对其他行业影响&#x1f31f;面临的挑战与问题&#x1f31f;AIGC未来发展 &#x1f31f;AIGC十大热门网站推荐&#xff1a; 文心…

常用的7个免费ChatGPT网站

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

2022天梯赛 L3_2 关于深度优先搜索和逆序对的题应该不会很难吧这件事 【树上逆序对计数】

传送门&#xff1a;https://pintia.cn/problem-sets/994805046380707840/exam/problems/1518582895035215872?type7&page1 思路 观察发现&#xff0c;逆序对可以分成两类&#xff1a; 节点 u u u 和 v v v 有明确的父子关系&#xff08;不一定是直属的直连边&#xff…

今天给大家推荐36套404页面模板

404页面是网站必备的一个页面&#xff0c;它承载着用户体验与SEO优化的重任。当用户访问不存在的页面时&#xff0c;服务器会返回404错误代码&#xff0c;并显示404页面。一个好的404页面可以帮助用户快速找到所需信息&#xff0c;并提升网站的用户体验。 以下是一些演示下载资…

Web3技术简介:重新定义互联网的未来

引言 在21世纪的数字时代&#xff0c;互联网已成为我们日常生活的不可或缺的一部分。然而&#xff0c;随着区块链和加密技术的快速发展&#xff0c;一个全新的互联网模型——Web3&#xff0c;正逐渐崭露头角。Web3不仅仅是技术的进步&#xff0c;它更是对传统互联网模型的挑战…

类和对象中阶3⃣️-默认成员函数(赋值运算符重载,取地址及 const取地址操作符重载等)

目录 5.赋值运算符重载 5.1 运算符重载 5.2 赋值运算符重载 5.3 前置和后置重载 5.4 重载流插入与流提取 流插入<<运算符重载 流提取运算符重载 6.日期类实现 7.const成员 8.取地址 及 const取地址操作符 重载 5.赋值运算符重载 5.1 运算符重载 C为了增强代码…

数仓建模—数据仓库初识

数仓建模—数据仓库初识 数据仓库之父Bill Inmon在1991年出版的"Building the Data Warehouse"一书中所提出的定义被广泛接受 数据仓库&#xff08;Data Warehouse&#xff09;是一个面向主题的&#xff08;Subject Oriented&#xff09;、集成的&#xff08;Integ…

Mysql索引专题

文章目录 1. 数据库索引结构1.1 Hash结构1.2 树结构1.3 Mysql索引怎么提升效率? 2. 执行计划 explainidselect_typetabletypepossible_keyskeykey_lenrefrowsfiteredextra 1. 数据库索引结构 我们都知道mysql数据库的常用存储结构是B树&#xff0c;为什么是B树&#xff1f;试…

邮件代发API发送邮件如何使用?操作指南?

邮件代发邮箱API发送邮件的步骤&#xff1f;代发有哪些注意事项&#xff1f; 在自动化办公、批量营销等场景中&#xff0c;手动发送邮件往往显得效率低下&#xff0c;这时候&#xff0c;邮件代发API就显得尤为重要。那么&#xff0c;邮件代发API发送邮件究竟如何使用呢&#x…

买婴儿洗衣机怎么选择?四大绝佳好用婴儿洗衣机分享

幼龄时期的宝宝的衣物&#xff0c;是比较需要注意的时候。可能一不注意宝宝穿在身上就会有不适宜症状发生。所以宝妈们真的要随时观察&#xff0c;然后在宝宝洗衣服的这上面多下点功夫&#xff0c;不要让宝宝受到这种无谓的伤害。小婴儿的抵抗力比我们差很多。有些细菌、病毒可…

Hadoop大数据处理技术-Linux相关命令

​7.Linux常用命令 1&#xff09;Windows中的dir&#xff1a;列出当前目录下所有的文件和目录 2&#xff09;cd&#xff1a;改变当前目录 cd命令并不能直接实现这种跳跃转换目录的功能 它只能让你在当前目录和其子目录之间来回切换 就像在一张平面地图上移动一样 如果想跨目录…

如何挑选护眼灯?分享护眼灯排行榜前十名

许多家长肯定都有这样的烦恼&#xff0c;家中的孩子自从上学后&#xff0c;每天回家后的学习作写阅读时总会在不知不觉间越来越贴近书本&#xff0c;后来还会时不时眯着眼睛看东西&#xff0c;但其实这种用眼习惯的最大原因是孩子没有足够光线和舒适的环境光线导致的&#xff0…

硬件设备杂记——12G SDI及 AES67/EBU

常见的 SDI线缆规格&#xff0c;HD-SDI又被称为1.5G-SDI&#xff0c;具体参数以秋叶原的参数为例 AES67/EBU 目前音频网络标准主要集中在OSI网络体系的第二层和第三层。 第二层音频标准的弊端在于构建音频网络时需要专用的交换机&#xff0c;无法利用现有的以太网络&#xff0c…

SpringBoot基于redis zset实现滑动窗口限流

通过Redis zset实现滑动窗口限流算法 在开发高并发系统时有三把利器用来保护系统&#xff1a;缓存、降级和限流。限流可以认为服务降级的一种&#xff0c;限流通过限制请求的流量以达到保护系统的目的。 一般来说&#xff0c;系统的吞吐量是可以计算出一个阈值的&#xff0c;…

【leetcode面试经典150题】59. 合并两个有序链表(C++)

【leetcode面试经典150题】专栏系列将为准备暑期实习生以及秋招的同学们提高在面试时的经典面试算法题的思路和想法。本专栏将以一题多解和精简算法思路为主&#xff0c;题解使用C语言。&#xff08;若有使用其他语言的同学也可了解题解思路&#xff0c;本质上语法内容一致&…

【Java框架】Spring框架(四)——Spring中的Bean的创建与生命周期

目录 SpringBean的创建步骤后置处理器(PostProcessor)BeanFactoryPostProcessorBeanPostProcessorInstantiationAwareBeanPostProcessorpostProcessBeforeInstantiationpostProcessAfterInstantiationpostProcessProperties SmartInstantiationAwareBeanPostProcessordetermine…
最新文章