Rust学习-构建命令行程序

Rust 的运行速度、安全性、单二进制文件输出和跨平台支持使其成为创建命令行程序的绝佳选择
本文以实现一个minigrep为例,展开对之前学习的回归

初版

接收命令行参数并打印文件内容

// 当所需函数嵌套了多于一层模块时,通常将父模块引入作用域
// std::env::args 在其任何参数包含无效 Unicode 字符时会 panic
// 如果需要接受包含无效 Unicode 字符的参数,使用 std::env::args_os
// 它返回 OsString 值,且OsString 值每个平台都不一样
use std::env;
use std::fs;

fn main() {
	// env::args()返回一个传递给程序的命令行参数的 迭代器(iterator)
    let args: Vec = env::args().collect();
    
    // 程序的名称占据了 vector 的第一个值 args[0],和C的命令行参数相同
    
    let query = &args[1];
    let filename = &args[2];

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

    let contents = fs::read_to_string(filename)
        .expect("Something went wrong reading the file");
    println!("With text:\n{}", contents);
}

问题

(1)main 进行了两个任务,函数功能不单一
(2)query 和 filename 是程序中的配置变量,和代码中其他变量混到一起
(3)打开文件失败使用 expect 来打印出错误信息,没有得到失败原因
(4)使用 expect 来处理不同的错误,如果用户没有指定足够的参数来运行程序,则展示的错误依旧无法让使用者阅读

解决方式-关注分离

main的职责:
(1)使用参数值调用命令行解析逻辑
(2)设置任何其他的配置
(3)调用 lib.rs 中的 run 函数
(4)如果 run 返回错误,则处理这个错误

main.rs 处理程序运行
lib.rs 处理所有的真正的任务逻辑
因为不能直接测试 main 函数,这个结构通过将所有的程序逻辑移动到 lib.rs 的函数中使得可以测试他们

重构

重构参数读取

方案一

use std::env;
use std::fs;

fn main() {
    let args: Vec = env::args().collect();
    let config = parse_config(&args);

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

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");
        
    println!("With text:\n{}", contents);
}

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

// 定义 Config 来包含拥有所有权的 String 值
// main 中的 args 变量是参数值的所有者并只允许 parse_config 函数借用他们
// 意味着如果 Config 尝试获取 args 中值的所有权将违反 Rust 的借用规则
fn parse_config(args: &[String]) -> Config {
    // 由于其运行时消耗,尽量避免使用 clone 来解决所有权问题
    let query = args[1].clone();
    let filename = args[2].clone();

    Config { query, filename }
}

更合理的参数读取

use std::env;
use std::fs;

fn main() {
    // args类型是:alloc::vec::Vec
    let args: Vec = env::args().collect();

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

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

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");
        
    println!("With text:\n{}", contents);
}

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

impl Config {
    // args类型是 &[alloc::string::String]
    // 使用这个方式也可以:fn new(args: &Vec) -> Config
    // 此时argos类型为:&alloc::vec::Vec
    // 这种转换有待后续深挖
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let filename = args[2].clone();
        Config { query, filename }
    }
}

改善错误信息

执行 cargo run test,直接panic

thread 'main' panicked at 'index out of bounds: the len is 2 but the index is 2', src/main.rs:31:24
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

糟糕的方案一

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

// panic信息如下
// thread 'main' panicked at 'not enough arguments', src/main.rs:26:13
}

方案二

返回一个 Result
成功时带有一个 Config 实例
出错时带有一个 &'static str:字符串字面量

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

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

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

        let query = args[1].clone();
        let filename = args[2].clone();
        Ok(Config { query, filename })
    }
}

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

    // unwrap_or_else 定义于标准库的 Result 
    // 使用它可以进行一些自定义的非 panic! 的错误处理
    // 当其值是 Err 时,该方法会调用一个 闭包(closure),即匿名函数
    // unwrap_or_else 会将 Err 的内部值,即静态字符串传递给|err|
    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        // process::exit 会立即停止程序并将传递给它的数字作为退出状态码
        process::exit(1);
    });
    
    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");
        
    println!("With text:\n{}", contents);
}

// 打印信息如下
// Problem parsing arguments: not enough arguments

main 函数处理 new 函数返回的 Result 值
并在出现错误的情况更明确的结束进程

精简main

main的修改

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

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

    // 其他处理逻辑全部放入run函数
    // if let 来检查 run 是否返回一个 Err 值
    // run 并不返回像 Config::new 返回的 Config 实例那样需要 unwrap 的值
    // 因为 run 在成功时返回 ()
    // 而只关心检测错误,所以并不需要 unwrap_or_else 来返回未封装的值
    // 因为它只会是 ()
    if let Err(e) = run(config) {
        println!("Application error: {}", e);
        process::exit(1);
    }
}

run的处理

// 引入 trait 对象 Box的路径
use std::error::Error;
// --snip--

// unit 类型 ():作为 Ok 时的返回值类型
// trait 对象 Box:
// 返回实现了 Error trait 的类型,无需指定具体将会返回的值的类型
// 因为在不同的错误场景可能有不同类型的错误返回值
fn run(config: Config) -> Result<(), Box> {
    // 不同于遇到错误就 panic!
    // ? 会从函数中返回错误值并让调用者来处理它
    let contents = fs::read_to_string(config.filename)?;

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

    // 调用 run 函数只是为了它的副作用;函数并没有返回什么有意义的值
    Ok(())
}

拆模块

拥有可以测试的公有 API 的库 crate

// src/lib.rs
use std::error::Error;
use std::fs;

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

impl Config {
    pub fn new(args: &[String]) -> Result {
        // --snip--
    }
}

pub fn run(config: Config) -> Result<(), Box> {
    // --snip--
}
// main.rs
use std::env;
use std::process;

use minigrep::Config;

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

错误信息打印到标准输出流

目前为止将所有的输出都 println! 到了终端
大部分终端都提供了两种输出:
标准输出(standard output,stdout)对应一般信息
标准错误(standard error,stderr)则用于错误信息

cargo run > output.txt
shell将所有信息存储到 output.txt
结果output.txt中存储了错误信息

将错误打印到标准错误

// 标准库提供了 eprintln! 宏来打印到标准错误流
 eprintln!("Problem parsing arguments: {}", err);

附录

打印类型

fn print_type_of(_: &T) {
    println!("{}", std::any::type_name::())
}

你可能感兴趣的:(rust,rust,学习)