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

Y小夜 2024-06-22 08:35:01 阅读 93

💻博主现有专栏:

                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


学习推荐:

        在当今这个飞速发展的信息时代,人工智能(AI)已经成为了一个不可或缺的技术力量,它正在逐步改变着我们的生活、工作乃至整个社会的运作方式。从智能语音助手到自动驾驶汽车,从精准医疗到智慧城市,人工智能的应用已经渗透到了我们生活的方方面面。因此,学习和掌握人工智能相关的知识和技能,对于任何希望在这个时代保持竞争力的个人来说,都已经变得至关重要。

        然而,人工智能是一个涉及数学、计算机科学、数据科学、机器学习、神经网络等多个领域的交叉学科,其学习曲线相对陡峭,对初学者来说可能会有一定的挑战性。幸运的是,随着互联网教育资源的丰富,现在有大量优秀的在线平台和网站提供了丰富的人工智能学习材料,包括视频教程、互动课程、实战项目等,这些资源无疑为学习者打开了一扇通往人工智能世界的大门。

        前些天发现了一个巨牛的人工智能学习网站:前言 – 人工智能教程通俗易懂,风趣幽默,忍不住分享一下给大家。

🎯接收命令行程序

        使用 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 frogTo tell your name the livelong dayTo 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:21note: 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 来确保一切都正确的衔接在一起。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。