0


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

💻博主现有专栏:

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

** **Y小夜-CSDN博客

学习推荐:

    在当今这个飞速发展的信息时代,人工智能(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 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.rslib.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::exit

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

来确保一切都正确的衔接在一起。


本文转载自: https://blog.csdn.net/shsjssnn/article/details/136565822
版权归原作者 Y小夜 所有, 如有侵权,请联系我们删除。

“【Rust】——项目实例:——命令行实例(一)”的评论:

还没有评论