Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

12.4 重构 Pt.2:错误处理

12.4.0. 写在正文之前

第12章要做一个实例的项目——一个命令行程序。这个程序是一个grep(Global Regular Expression Print),是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。

这个项目分为这么几步:

12.4.1. 回顾

上一节中为了模块化我们为变量创建了结构体,还把读取指令的函数独立出去改成了结构体的方法。以下是截止到上一篇文章所写出的所有代码:

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

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

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

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

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

12.4.2. 意料之外的输入

这个程序能正确运行的前提是用户输入的输入无误,那我们试试不带参数的输入会引发什么:

$ cargo run
   Compiling minigrep v0.1.0 (/tmp/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/minigrep`

thread 'main' (469490) panicked at src/main.rs:20:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

它提示"Index out of bounds"索引越界,作为程序编写者的我们明白这是因为参数不够导致程序在使用索引获取参数时越界触发恐慌。但是作为用户就不可能看懂这个报错信息,无法纠正错误。

这一篇文章要做的就是让程序产生的错误信息易于理解。

12.4.3. 指定报错信息

让用户理解报错信息的方式就是自己指定一个报错信息。刚刚的例子是在运行Config::new时索引越界,所以我们就修改这个地方:

#![allow(unused)]
fn main() {
impl Config {
    fn new(args: &[String]) -> Config {
        if args.len() < 3 {
            panic!("not enough arguments");
        }
        let query = args[1].clone();
        let filename = args[2].clone();
        Config {
            query,
            filename,
        }
    }
}
}

如果args的元素数量小于三就发生恐慌打印“not enough arguments“来提示用户输入的参数太少了。

再试试不带参数的输入:

$ cargo run
   Compiling minigrep v0.1.0 (/tmp/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
     Running `target/debug/minigrep`

thread 'main' (465520) panicked at src/main.rs:21:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

这一次的错误信息比上一次就好很多了。

但是它仍然残留了一些其他的信息,比如“thread ‘main’ (465520) panicked at src/main.rs:21:13:“和“note: run with RUST_BACKTRACE=1 environment variable to display a backtrace”,这些内容是给程序员看的不是给用户看的。所以这些信息也得去掉。

12.4.4. 使用Result类型

panic!适用于程序本身出现问题时的恐慌,而这里缺少参数的输入是程序使用时的问题,针对这种问题,使用Result类型来传播错误(这部分的内容详见 9.2. Result枚举与可恢复的错误 Pt.1 & Pt.2)才是最优解:

#![allow(unused)]
fn main() {
impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }
        let query = args[1].clone();
        let filename = args[2].clone();
        Ok(Config { query, filename})
    }
}
}
  • 报错的信息需要用Err包裹,成功的返回值需要用Ok包裹
  • Result类型的Ok返回Config实例,Err返回&str字符串字面值,但是编译器不知道这个&str是从哪里来的以及它的生命周期有多长,所以得带生命周期,我们需要它在程序运行时始终保持有效,写成&'static str这个静态生命周期。

new函数的返回值都变了,main函数里接收值的逻辑也得变:

#![allow(unused)]
fn main() {
let config = Config::new(&args).unwrap_or_else(|err| {
    println!("Problem parsing arguments: {}", err);
    process::exit(1);
});
}

unwrap_or_else这个方法会接收Result类型,如果是Ok,就会把Ok附带的值直接返回赋给变量,类似于unwrap;如果是Err,那么这个方法会调用一个闭包(closure)。

闭包是我们定义的匿名函数,并将其作为参数传递给unwrap_or_else。其写法是两个管道符||,在中间放变量名,相当于一个参数,这里就放了err,这个err可以在闭包的函数体内被调用,比如在打印错误时就使用了err

然后使用标准库的process::exit这个函数,使用前记得先导入一下:use std::process;,如果调用exit函数,程序的执行就会立即终止,而其参数,也就是示例代码中的1就作为程序退出时的状态码,这样显示到println!("Problem parsing arguments: {}", err);之后程序就会终止,自然就不会有比如“thread ‘main’ (465520) panicked at src/main.rs:21:13:“和“note: run with RUST_BACKTRACE=1 environment variable to display a backtrace“这些内容。

试一下:

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

闭包这个概念在 13.1. 闭包 Pt.1 才会讲到,这里没看懂也没关系,只要了解个大概即可。

12.4.5. 整体代码

以下是截止到这篇文章写出的所有代码:

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

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

fn main() {
    let args:Vec<String> = env::args().collect();
    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

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

impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }
        let query = args[1].clone();
        let filename = args[2].clone();
        Ok(Config { query, filename})
    }
}