mdBook 源码阅读
上周做完了 CS140e 之后想着还是再多看看别人怎么写 Rust 代码的吧。正好浏览器开着 The Rust Programming Language,忽然想起来这本书的生成器 mdBook
就是 Rust 写的。既然是官方御用工具,应该代码质量不会差,而且功能也简单。用 cloc
` 统计了一下,总共4918行代码,非常短。就决定是这个了。
IntelliJ IDEA
头一次读代码,还是需要配置一下读代码的工具的。因为之前用 IntelliJ IDEA 写代码还挺顺手的,所以决定就用它来读代码了。下了个最新版的,感觉几个月不用,界面变得更好看了呢。官方有一个 IntelliJ Rust 插件,装上之后就可以用 Rust 了。导入项目很顺利,能够自动识别 Rust 项目。
我特别喜欢用的几个功能和快捷键是这样的:
cmd+B
Go to Declarationcmd+[
Go Backopt+F7
Find UsageF1
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...
}