上周做完了 CS140e 之后想着还是再多看看别人怎么写 Rust 代码的吧。正好浏览器开着 The Rust Programming Language,忽然想起来这本书的生成器 mdBook 就是 Rust 写的。既然是官方御用工具,应该代码质量不会差,而且功能也简单。用 cloc` 统计了一下,总共4918行代码,非常短。就决定是这个了。

IntelliJ IDEA

头一次读代码,还是需要配置一下读代码的工具的。因为之前用 IntelliJ IDEA 写代码还挺顺手的,所以决定就用它来读代码了。下了个最新版的,感觉几个月不用,界面变得更好看了呢。官方有一个 IntelliJ Rust 插件,装上之后就可以用 Rust 了。导入项目很顺利,能够自动识别 Rust 项目。

我特别喜欢用的几个功能和快捷键是这样的:

  • cmd+B Go to Declaration
  • cmd+[ Go Back
  • opt+F7 Find Usage
  • F1 Quick Documentation

有趣的库

在看 mdBook 源代码的时候我发现了一些蛮实用的库,在这里跟大家分享一下。

首先,强烈推荐 error-chain。错误处理一直是大多数人写程序的时候最讨厌的事情了,处理的话很容易会代码膨胀,不处理的话又拿不出台面。Rust 的?操作符给错误处理带来了非常大的便利。但是在用的时候,我就遇到过不同错误类型的 Result 无法自动转换的情况,需要手动编写类型转换。另外一点就是,用 ? 把一个错误一路往上抛,最上层并不能知道具体是在哪里出错,也没法给出一个对用户友好的错误信息。

error-chain 就能很好的解决这两个问题。error-chain 会定义一个错误类型。经过非常简单的配置之后,不同类型的错误就可以自动转换到 error-chain 定义的错误类型上。在用 ? 往上抛错误的时候,也能够顺带指定原因。举个例子:

fn load_chapter(/*some args*/) -> Result<Chapter>
{
    // skip...
    let mut f = File::open(&location)
            .chain_err(|| format!("Chapter file not found, {}",
                link.location.display()))?;
    // skip...
}

这里 Result 的错误类型是用 error-chain 生成的错误类型。可以看到,File::open 产生的错误也能够直接抛出,而且还能通过 chain_err 附带上有意义的错误提示。看起来非常简单实用,真的强烈推荐。

另外还有几个看起来不错的库:

  • serde 序列化和反序列化工具,支持各种不同的数据格式,用起来也很方便,可以直接 #[derive(Serialize, Deserialize)]
  • clap 用来解析命令行参数
  • env_logger 简单地输出日志信息
  • lazy_static 能够定义运行时的常量,比方说 mdBook 里面就拿来存放编译过的正则表达式
  • elasticlunr 现在才知道这种东西叫做 full-text search engine

防止 Nested Match

我之前在写 Rust 程序的时候就有这个疑惑,因为很多数据都是用了 Option 之类的结构体包着的,要用的时候要用 match 或者 if 解开。数据多了,就会形成 nested match,或者我更愿意叫做 match hell,举个简单的例子:

match foo() {
    Ok(v1) => match bar(v1) {
        Some(v2) => baz(v2),
        None => {
            // handling None
            return;
        }
    },
    Err(e) => {
        // handling e
        return;
    }
}

一般来说主执行路径以外的都是简单处理一下错误,都是可以提前返回的。我之前的做法是沿袭其他语言的写法,先判断一下如果产生了错误就提前返回,然后 unwrap(),像这样:

let v1 = foo();
if let Err(e) = v1 {
    // handling e
    return;
}
let v1 = v1.ok().unwrap();
let v2 = bar(v1);
if v2.is_none() {
    // handling None
    return;
}
let v2 = v2.unwrap();
baz(v2)

但是一想到 unwarp() 会产生 panic! 调用,而且明明这个 unwrap() 毫无必要,我就觉得非常郁闷。这次在代码里面看到了新的写法,终于解决了我的问题:

let v1 = match foo() {
    Ok(v1) => v1,
    Err(e) => {
        // handling e
        return;
    }
};
let v2 = match bar(v1) {
    Some(v2) => v2,
    None => {
        // handling None
        return;
    }
}
baz(v2)

其实非常简单,我就是忘掉了 match 也是个表达式,也是可以有值的。

AsRef vs. Into

我看到了代码里面有 AsRef<PathBuf> 又有 Into<PathBuf>,但是我之前只见过前者,有点不知道是为什么。后来看了看代码,我觉得是这样的:如果只是想用一下的话就 AsRef,但如果想要自己拥有那就 Into

impl MDBook {
    pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
        MDBook {
            root: book_root.into()  // ownership
        }
    }
}

fn load_chapter<P: AsRef<Path>>(
    link: &Link,
    src_dir: P,
    parent_names: Vec<String>,
) -> Result<Chapter>
{
    let src_dir = src_dir.as_ref();
    let location = src_dir.join(&link.location);
    let mut f = File::open(&location)?;  // use
    // skip...
}