mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-14 21:11:31 +08:00
563 lines
46 KiB
HTML
563 lines
46 KiB
HTML
<!DOCTYPE HTML>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>增强错误处理和模块化 - Rust 程序设计语言 简体中文版</title>
|
||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||
<meta name="description" content="Rust 程序设计语言 简体中文版">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
|
||
<base href="">
|
||
|
||
<link rel="stylesheet" href="book.css">
|
||
<link href='https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/css'>
|
||
|
||
<link rel="shortcut icon" href="favicon.png">
|
||
|
||
<!-- Font Awesome -->
|
||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
|
||
|
||
<link rel="stylesheet" href="highlight.css">
|
||
<link rel="stylesheet" href="tomorrow-night.css">
|
||
|
||
<!-- MathJax -->
|
||
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||
|
||
<!-- Fetch JQuery from CDN but have a local fallback -->
|
||
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
|
||
<script>
|
||
if (typeof jQuery == 'undefined') {
|
||
document.write(unescape("%3Cscript src='jquery.js'%3E%3C/script%3E"));
|
||
}
|
||
</script>
|
||
</head>
|
||
<body class="light">
|
||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||
<script type="text/javascript">
|
||
var theme = localStorage.getItem('theme');
|
||
if (theme == null) { theme = 'light'; }
|
||
$('body').removeClass().addClass(theme);
|
||
</script>
|
||
|
||
<!-- Hide / unhide sidebar before it is displayed -->
|
||
<script type="text/javascript">
|
||
var sidebar = localStorage.getItem('sidebar');
|
||
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
|
||
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
|
||
</script>
|
||
|
||
<div id="sidebar" class="sidebar">
|
||
<ul class="chapter"><li><a href="ch01-00-introduction.html"><strong>1.</strong> 介绍</a></li><li><ul class="section"><li><a href="ch01-01-installation.html"><strong>1.1.</strong> 安装</a></li><li><a href="ch01-02-hello-world.html"><strong>1.2.</strong> Hello, World!</a></li></ul></li><li><a href="ch02-00-guessing-game-tutorial.html"><strong>2.</strong> 猜猜看教程</a></li><li><a href="ch03-00-common-programming-concepts.html"><strong>3.</strong> 通用编程概念</a></li><li><ul class="section"><li><a href="ch03-01-variables-and-mutability.html"><strong>3.1.</strong> 变量和可变性</a></li><li><a href="ch03-02-data-types.html"><strong>3.2.</strong> 数据类型</a></li><li><a href="ch03-03-how-functions-work.html"><strong>3.3.</strong> 函数如何工作</a></li><li><a href="ch03-04-comments.html"><strong>3.4.</strong> 注释</a></li><li><a href="ch03-05-control-flow.html"><strong>3.5.</strong> 控制流</a></li></ul></li><li><a href="ch04-00-understanding-ownership.html"><strong>4.</strong> 认识所有权</a></li><li><ul class="section"><li><a href="ch04-01-what-is-ownership.html"><strong>4.1.</strong> 什么是所有权</a></li><li><a href="ch04-02-references-and-borrowing.html"><strong>4.2.</strong> 引用 & 借用</a></li><li><a href="ch04-03-slices.html"><strong>4.3.</strong> Slices</a></li></ul></li><li><a href="ch05-00-structs.html"><strong>5.</strong> 结构体</a></li><li><ul class="section"><li><a href="ch05-01-method-syntax.html"><strong>5.1.</strong> 方法语法</a></li></ul></li><li><a href="ch06-00-enums.html"><strong>6.</strong> 枚举和模式匹配</a></li><li><ul class="section"><li><a href="ch06-01-defining-an-enum.html"><strong>6.1.</strong> 定义枚举</a></li><li><a href="ch06-02-match.html"><strong>6.2.</strong> <code>match</code>控制流运算符</a></li><li><a href="ch06-03-if-let.html"><strong>6.3.</strong> <code>if let</code>简单控制流</a></li></ul></li><li><a href="ch07-00-modules.html"><strong>7.</strong> 模块</a></li><li><ul class="section"><li><a href="ch07-01-mod-and-the-filesystem.html"><strong>7.1.</strong> <code>mod</code>和文件系统</a></li><li><a href="ch07-02-controlling-visibility-with-pub.html"><strong>7.2.</strong> 使用<code>pub</code>控制可见性</a></li><li><a href="ch07-03-importing-names-with-use.html"><strong>7.3.</strong> 使用<code>use</code>导入命名</a></li></ul></li><li><a href="ch08-00-common-collections.html"><strong>8.</strong> 通用集合类型</a></li><li><ul class="section"><li><a href="ch08-01-vectors.html"><strong>8.1.</strong> vector</a></li><li><a href="ch08-02-strings.html"><strong>8.2.</strong> 字符串</a></li><li><a href="ch08-03-hash-maps.html"><strong>8.3.</strong> 哈希 map</a></li></ul></li><li><a href="ch09-00-error-handling.html"><strong>9.</strong> 错误处理</a></li><li><ul class="section"><li><a href="ch09-01-unrecoverable-errors-with-panic.html"><strong>9.1.</strong> <code>panic!</code>与不可恢复的错误</a></li><li><a href="ch09-02-recoverable-errors-with-result.html"><strong>9.2.</strong> <code>Result</code>与可恢复的错误</a></li><li><a href="ch09-03-to-panic-or-not-to-panic.html"><strong>9.3.</strong> <code>panic!</code>还是不<code>panic!</code></a></li></ul></li><li><a href="ch10-00-generics.html"><strong>10.</strong> 泛型、trait 和生命周期</a></li><li><ul class="section"><li><a href="ch10-01-syntax.html"><strong>10.1.</strong> 泛型数据类型</a></li><li><a href="ch10-02-traits.html"><strong>10.2.</strong> trait:定义共享的行为</a></li><li><a href="ch10-03-lifetime-syntax.html"><strong>10.3.</strong> 生命周期与引用有效性</a></li></ul></li><li><a href="ch11-00-testing.html"><strong>11.</strong> 测试</a></li><li><ul class="section"><li><a href="ch11-01-writing-tests.html"><strong>11.1.</strong> 编写测试</a></li><li><a href="ch11-02-running-tests.html"><strong>11.2.</strong> 运行测试</a></li><li><a href="ch11-03-test-organization.html"><strong>11.3.</strong> 测试的组织结构</a></li></ul></li><li><a href="ch12-00-an-io-project.html"><strong>12.</strong> 一个 I/O 项目</a></li><li><ul class="section"><li><a href="ch12-01-accepting-command-line-arguments.html"><strong>12.1.</strong> 接受命令行参数</a></li><li><a href="ch12-02-reading-a-file.html"><strong>12.2.</strong> 读取文件</a></li><li><a href="ch12-03-improving-error-handling-and-modularity.html" class="active"><strong>12.3.</strong> 增强错误处理和模块化</a></li><li><a href="ch12-04-testing-the-librarys-functionality.html"><strong>12.4.</strong> 测试库的功能</a></li><li><a href="ch12-05-working-with-environment-variables.html"><strong>12.5.</strong> 处理环境变量</a></li><li><a href="ch12-06-writing-to-stderr-instead-of-stdout.html"><strong>12.6.</strong> 输出到<code>stderr</code>而不是<code>stdout</code></a></li></ul></li><li><a href="ch13-00-functional-features.html"><strong>13.</strong> Rust 中的函数式语言功能</a></li><li><ul class="section"><li><a href="ch13-01-closures.html"><strong>13.1.</strong> 闭包</a></li><li><a href="ch13-02-iterators.html"><strong>13.2.</strong> 迭代器</a></li><li><a href="ch13-03-improving-our-io-project.html"><strong>13.3.</strong> 改进 I/O 项目</a></li><li><a href="ch13-04-performance.html"><strong>13.4.</strong> 性能</a></li></ul></li><li><a href="ch14-00-more-about-cargo.html"><strong>14.</strong> 更多关于 Cargo 和 Crates.io</a></li><li><ul class="section"><li><a href="ch14-01-release-profiles.html"><strong>14.1.</strong> 发布配置</a></li><li><a href="ch14-02-publishing-to-crates-io.html"><strong>14.2.</strong> 将 crate 发布到 Crates.io</a></li><li><a href="ch14-03-cargo-workspaces.html"><strong>14.3.</strong> Cargo 工作空间</a></li><li><a href="ch14-04-installing-binaries.html"><strong>14.4.</strong> 使用<code>cargo install</code>从 Crates.io 安装文件</a></li><li><a href="ch14-05-extending-cargo.html"><strong>14.5.</strong> Cargo 自定义扩展命令</a></li></ul></li><li><a href="ch15-00-smart-pointers.html"><strong>15.</strong> 智能指针</a></li><li><ul class="section"><li><a href="ch15-01-box.html"><strong>15.1.</strong> <code>Box<T></code>用于已知大小的堆上数据</a></li><li><a href="ch15-02-deref.html"><strong>15.2.</strong> <code>Deref</code> Trait 允许通过引用访问数据</a></li><li><a href="ch15-03-drop.html"><strong>15.3.</strong> <code>Drop</code> Trait 运行清理代码</a></li><li><a href="ch15-04-rc.html"><strong>15.4.</strong> <code>Rc<T></code> 引用计数智能指针</a></li><li><a href="ch15-05-interior-mutability.html"><strong>15.5.</strong> <code>RefCell<T></code>和内部可变性模式</a></li><li><a href="ch15-06-reference-cycles.html"><strong>15.6.</strong> 引用循环和内存泄漏是安全的</a></li></ul></li><li><a href="ch16-00-concurrency.html"><strong>16.</strong> 无畏并发</a></li><li><ul class="section"><li><a href="ch16-01-threads.html"><strong>16.1.</strong> 线程</a></li><li><a href="ch16-02-message-passing.html"><strong>16.2.</strong> 消息传递</a></li><li><a href="ch16-03-shared-state.html"><strong>16.3.</strong> 共享状态</a></li><li><a href="ch16-04-extensible-concurrency-sync-and-send.html"><strong>16.4.</strong> 可扩展的并发:<code>Sync</code>和<code>Send</code></a></li></ul></li><li><a href="ch17-00-oop.html"><strong>17.</strong> 面向对象</a></li><li><ul class="section"><li><a href="ch17-01-what-is-oo.html"><strong>17.1.</strong> 什么是面向对象?</a></li><li><a href="ch17-02-trait-objects.html"><strong>17.2.</strong> 为使用不同类型的值而设计的 trait 对象</a></li><li><a href="ch17-03-oo-design-patterns.html"><strong>17.3.</strong> 面向对象设计模式的实现</a></li></ul></li></ul>
|
||
</div>
|
||
|
||
<div id="page-wrapper" class="page-wrapper">
|
||
|
||
<div class="page">
|
||
<div id="menu-bar" class="menu-bar">
|
||
<div class="left-buttons">
|
||
<i id="sidebar-toggle" class="fa fa-bars"></i>
|
||
<i id="theme-toggle" class="fa fa-paint-brush"></i>
|
||
</div>
|
||
|
||
<h1 class="menu-title">Rust 程序设计语言 简体中文版</h1>
|
||
|
||
<div class="right-buttons">
|
||
<i id="print-button" class="fa fa-print" title="Print this book"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="content" class="content">
|
||
<a class="header" href="#重构改进模块性和错误处理" name="重构改进模块性和错误处理"><h2>重构改进模块性和错误处理</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-03-improving-error-handling-and-modularity.md">ch12-03-improving-error-handling-and-modularity.md</a>
|
||
<br>
|
||
commit b8e4fcbf289b82c12121b282747ce05180afb1fb</p>
|
||
</blockquote>
|
||
<p>为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。</p>
|
||
<p>第一,<code>main</code>现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果<code>main</code>中的功能持续增加,<code>main</code>函数处理的单独的任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能这样每个函数就负责一个任务。</p>
|
||
<p>这同时也关系到第二个问题:<code>search</code>和<code>filename</code>是程序中的配置变量,而像<code>f</code>和<code>contents</code>则用来执行程序逻辑。随着<code>main</code>函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将将配置变量组织进一个结构这样就能使他们的目的更明确了。</p>
|
||
<p>第三个问题是如果打开文件失败我们使用<code>expect</code>来打印出错误信息,不过这个错误信息只是说<code>file not found</code>。除了缺少文件之外还有很多打开文件可能失败的方式:例如,文件可能存在,不过可能没有打开它的权限。如果我们现在就出于这种情况,打印出的<code>file not found</code>错误信息就给了用户一个不符合事实的建议!</p>
|
||
<p>第四,我们不停的使用<code>expect</code>来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 "index out of bounds" 错误而这并不能明确的解释问题。如果所有的错误处理都位于一处这样将来的维护者在需要修改错误处理逻辑时就只需要咨询一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。</p>
|
||
<p>让我们通过重构项目来解决这些问题。</p>
|
||
<a class="header" href="#二进制项目的关注分离" name="二进制项目的关注分离"><h3>二进制项目的关注分离</h3></a>
|
||
<p><code>main</code>函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一个类在<code>main</code>函数开始变得庞大时进行二进制程序的关注分离的指导性过程。这些过程有如下步骤:</p>
|
||
<ol>
|
||
<li>将程序拆分成 <em>main.rs</em> 和 <em>lib.rs</em> 并将程序的逻辑放入 <em>lib.rs</em> 中。</li>
|
||
<li>当命令行解析逻辑比较小时,可以保留在 <em>main.rs</em> 中。</li>
|
||
<li>当命令行解析开始变得复杂时,也同样将其从 <em>main.rs</em> 提取到 <em>lib.rs</em>中。</li>
|
||
<li>经过这些过程之后保留在<code>main</code>函数中的责任是:
|
||
<ul>
|
||
<li>使用参数值调用命令行解析逻辑</li>
|
||
<li>设置任何其他的配置</li>
|
||
<li>调用 <em>lib.rs</em> 中的<code>run</code>函数</li>
|
||
<li>如果<code>run</code>返回错误,则处理这个错误</li>
|
||
</ul>
|
||
</li>
|
||
</ol>
|
||
<p>这个模式的一切就是为了关注分离:<em>main.rs</em> 处理程序运行,而 <em>lib.rs</em> 处理所有的真正的任务逻辑。因为不能直接测试<code>main</code>函数,这个结构通过将所有的程序逻辑移动到 <em>lib.rs</em> 的函数中使得我们可以测试他们。仅仅保留在 <em>main.rs</em> 中的代码将足够小以便阅读就可以验证其正确性。</p>
|
||
<!--Since main is already handling the parsing of arguments, why do we need to
|
||
add a new function for it, can you say how that improves things? -->
|
||
<!-- Sorry, the steps we had were unclear. We've tried rewording. /Carol -->
|
||
<a class="header" href="#提取参数解析器" name="提取参数解析器"><h3>提取参数解析器</h3></a>
|
||
<p>首先,我们将提取解析参数的功能。列表 12-5 中展示了新<code>main</code>函数的开头,它调用了新函数<code>parse_config</code>。目前它仍将定义在 <em>src/main.rs</em> 中:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let args: Vec<String> = env::args().collect();
|
||
|
||
let (query, filename) = parse_config(&args);
|
||
|
||
// ...snip...
|
||
}
|
||
|
||
fn parse_config(args: &[String]) -> (&str, &str) {
|
||
let query = &args[1];
|
||
let filename = &args[2];
|
||
|
||
(query, filename)
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-5: Extract a <code>parse_config</code> function from
|
||
<code>main</code></span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>我们仍然将命令行参数收集进一个 vector,不过不同于在<code>main</code>函数中将索引 1 的参数值赋值给变量<code>query</code>和将索引 2 的值赋值给变量<code>filename</code>,我们将整个 vector 传递给<code>parse_config</code>函数。接着<code>parse_config</code>函数将包含知道哪个参数该放入哪个变量的逻辑,并将这些值返回到<code>main</code>。仍然在<code>main</code>中创建变量<code>query</code>和<code>filename</code>,不过<code>main</code>不再负责处理命令行参数与变量如何对应。</p>
|
||
<p>这对我们这小程序可能有点大材小用,不过我们将采用小的、增量的步骤进行重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时能帮助你定位问题的成因。</p>
|
||
<a class="header" href="#组合配置值" name="组合配置值"><h3>组合配置值</h3></a>
|
||
<p>我们可以采取另一个小的步骤来进一步改善这个函数。现在函数返回一个元组,不过立刻又就将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。</p>
|
||
<p>另一个表明还有改进空间的迹象是<code>parse_config</code>的<code>config</code>部分,它暗示了我们返回的两个值是相关的并都是一个配置值的一部分。目前除了将这两个值组合进元组之外并没有表达这个数据结构的意义:我们可以将这两个值放入一个结构体并给每个字段一个有意义的名字。这会让未来的维护者更容易理解不同的值如何相互关联以及他们的目的。</p>
|
||
<!-- above -- I'm not sure why this is a problem --- because they aren't
|
||
currently bound together? And why does it imply that -->
|
||
<blockquote>
|
||
<p>注意:一些同学将这种拒绝使用相对而言更为合适的复合类型而使用基本类型的模式称为<strong>基本类型偏执</strong>(<em>primitive obsession</em>)。</p>
|
||
</blockquote>
|
||
<!-- Ah, I see, so the problems here stem from using simple types to do tasks
|
||
inefficiently, when a more complex task could handle it in ways that improve...
|
||
behavior? Readability? Can you say as much? -->
|
||
<!-- I've tried to clarify above. Note that when Rust programmers talk about
|
||
"efficiency", they usually mean "run-time performance", whereas here we're
|
||
talking about code design and maintainability and not addressing performance
|
||
at all. /Carol -->
|
||
<p>列表 12-6 展示了新定义的结构体<code>Config</code>,它有字段<code>query</code>和<code>filename</code>。我们也改变了<code>parse_config</code>函数来返回一个<code>Config</code>结构体的实例,并更新<code>main</code>来使用结构体字段而不是单独的变量:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,should_panic"># use std::env;
|
||
# use std::fs::File;
|
||
#
|
||
fn main() {
|
||
let args: Vec<String> = env::args().collect();
|
||
|
||
let config = parse_config(&args);
|
||
|
||
println!("Searching for {}", config.query);
|
||
println!("In file {}", config.filename);
|
||
|
||
let mut f = File::open(config.filename).expect("file not found");
|
||
|
||
// ...snip...
|
||
}
|
||
|
||
struct Config {
|
||
query: String,
|
||
filename: String,
|
||
}
|
||
|
||
fn parse_config(args: &[String]) -> Config {
|
||
let query = args[1].clone();
|
||
let filename = args[2].clone();
|
||
|
||
Config {
|
||
query: query,
|
||
filename: filename,
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>Listing 12-6: Refactoring <code>parse_config</code> to return an instance of a <code>Config</code>
|
||
struct</p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p><code>parse_config</code>的签名现在表明它返回一个<code>Config</code>值。在<code>parse_config</code>的函数体中,之前返回了<code>args</code>中<code>String</code>值引用的字符串 slice,现在我们选择定义<code>Config</code>来使用拥有所有权的<code>String</code>值。<code>main</code>中的<code>args</code>变量是参数值的所有者并只允许<code>parse_config</code>函数借用他们,这意味着如果<code>Config</code>尝试获取<code>args</code>中值的所有权将违反 Rust 的借用规则。</p>
|
||
<p>还有许多不同的方式可以处理<code>String</code>的数据,而最简单但有些不太高效的方式是调用这些值的<code>clone</code>方法。这会生成<code>Config</code>实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。</p>
|
||
<!-- PROD: START BOX -->
|
||
<blockquote>
|
||
<a class="header" href="#使用clone权衡取舍" name="使用clone权衡取舍"><h4>使用<code>clone</code>权衡取舍</h4></a>
|
||
<p>由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于避免使用<code>clone</code>来解决所有权问题。在关于迭代器的第十三章中,我们将会学习如何更有效率的处理这种情况,不过现在,复制一些字符串来取得进展是没有问题的,因为只会进行一次这样的拷贝,而且文件名和要搜索的字符串都比较短。在第一轮编写时拥有一个可以工作但有点低效的程序要比尝试过度优化代码更好一些。随着你对 Rust 更加熟练,将能更轻松的直奔合适的方法,不过现在调用<code>clone</code>是完全可以接受的。</p>
|
||
</blockquote>
|
||
<!-- PROD: END BOX -->
|
||
<p>我们更新<code>main</code>将<code>parse_config</code>返回的<code>Config</code>实例放入变量<code>config</code>中,并更新之前分别使用<code>search</code>和<code>filename</code>变量的代码为现在的使用<code>Config</code>结构体的字段。</p>
|
||
<p>现在代码更明确的表现了我们的意图,<code>query</code>和<code>filename</code>是相关联的并且他们的目的是配置程序如何工作的。任何使用这些值的代码就知道在<code>config</code>实例中对应目的的字段名中寻找他们。</p>
|
||
<a class="header" href="#创建一个config构造函数" name="创建一个config构造函数"><h3>创建一个<code>Config</code>构造函数</h3></a>
|
||
<!-- Can you lay out what we intend to do in this section? I wasn't sure even
|
||
at the end what we did and why --- why did we create it as parse_config to then
|
||
change it to new? -->
|
||
<!-- We're making small, incremental changes. In addition to being good
|
||
software development practice, we were hoping that by changing one thing at a
|
||
time, the process of improving code's design would be easier to follow rather
|
||
than just jumping to the best solution. We extracted code into a function, then
|
||
it was clearer that we should introduce a struct, then it was clear that the
|
||
function we extracted is really a constructor of `Config` and should be written
|
||
as such. This refactoring process should be familiar to software developers.
|
||
I've tried to add a little recap to the start of this section, I hope that
|
||
helps. /Carol -->
|
||
<p>目前为止,我们将负责解析命令行参数的逻辑从<code>main</code>提取到了<code>parse_config</code>函数中,这帮助我们看清值<code>query</code>和<code>filename</code>是相互关联的并应该在代码中表现这种关系。接着我们增加了<code>Config</code>结构体来命名<code>query</code>和<code>filename</code>的相关目的,并能够从<code>parse_config</code>函数中将这些值的名称作为结构体字段名称返回。</p>
|
||
<p>所以现在<code>parse_config</code>函数的目的是创建一个<code>Config</code>实例,我们可以将<code>parse_config</code>从一个普通函数变为一个叫做<code>new</code>的与结构体关联的函数。做出这个改变使得代码更符合习惯:可以像标准库中的<code>String</code>调用<code>String::new</code>来创建一个该类型的实例那样,将<code>parse_config</code>变为一个与<code>Config</code>关联的<code>new</code>函数。列表 12-7 展示了需要做出的修改:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,should_panic"># use std::env;
|
||
#
|
||
fn main() {
|
||
let args: Vec<String> = env::args().collect();
|
||
|
||
let config = Config::new(&args);
|
||
|
||
// ...snip...
|
||
}
|
||
|
||
# struct Config {
|
||
# query: String,
|
||
# filename: String,
|
||
# }
|
||
#
|
||
// ...snip...
|
||
|
||
impl Config {
|
||
fn new(args: &[String]) -> Config {
|
||
let query = args[1].clone();
|
||
let filename = args[2].clone();
|
||
|
||
Config {
|
||
query: query,
|
||
filename: filename,
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-7: Changing <code>parse_config</code> into
|
||
<code>Config::new</code></span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>这里将<code>main</code>中调用<code>parse_config</code>的地方更新为调用<code>Config::new</code>。我们将<code>parse_config</code>的名字改为<code>new</code>并将其移动到<code>impl</code>块中,这使得<code>new</code>函数与<code>Config</code>相关联。再次尝试编译并确保它可以工作。</p>
|
||
<a class="header" href="#修复错误处理" name="修复错误处理"><h3>修复错误处理</h3></a>
|
||
<p>现在我们开始修复错误处理。回忆一下之前提到过如果<code>args</code> vector 包含少于 3 个项并尝试访问 vector 中索引 1 或 索引 2 的值会造成程序 panic。尝试不带任何参数运行程序;这将看起来像这样:</p>
|
||
<pre><code>$ cargo run
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running `target/debug/greprs`
|
||
thread 'main' panicked at 'index out of bounds: the len is 1
|
||
but the index is 1', /stable-dist-rustc/build/src/libcollections/vec.rs:1307
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
</code></pre>
|
||
<p><code>index out of bounds: the len is 1 but the index is 1</code>是一个针对程序员的错误信息,然而这并不能真正帮助终端用户理解发生了什么和他们应该做什么。现在就让我们修复它吧。</p>
|
||
<a class="header" href="#改善错误信息" name="改善错误信息"><h3>改善错误信息</h3></a>
|
||
<p>在列表 12-8 中,在<code>new</code>函数中增加了一个检查在访问索引 1 和 2 之前检查 slice 是否足够长。如果 slice 不够长,我们使用一个更好的错误信息 panic 而不是<code>index out of bounds</code>信息:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">// ...snip...
|
||
fn new(args: &[String]) -> Config {
|
||
if args.len() < 3 {
|
||
panic!("not enough arguments");
|
||
}
|
||
// ...snip...
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-8: Adding a check for the number of
|
||
arguments</span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>这类似于列表 9-8 中的<code>Guess::new</code>函数,那里如果<code>value</code>参数超出了有效值的范围就调用<code>panic!</code>。不同于检查值的范围,这里检查<code>args</code>的长度至少是 3,而函数的剩余部分则可以假设这个条件成立的基础上运行。如果
|
||
<code>args</code>少于 3 个项,这个条件将为真,并调用<code>panic!</code>立即终止程序。</p>
|
||
<p>有了<code>new</code>中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么:</p>
|
||
<pre><code>$ cargo run
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running `target/debug/greprs`
|
||
thread 'main' panicked at 'not enough arguments', src/main.rs:29
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
</code></pre>
|
||
<p>这个输出就好多了,现在有了一个合理的错误信息。然而,我们还有一堆额外的信息不希望提供给用户。所以在这里使用列表 9-8 中的技术可能不是最好的;无论如何<code>panic!</code>调用更适合程序问题而不是使用问题,正如第九章所讲到的。相反我们可以使用那一章学习的另一个技术:返回一个可以表明成功或错误的<code>Result</code>。</p>
|
||
<!-- Below -- how does using new fix this, can you lay that our up front? -->
|
||
<!-- I'm not sure what you mean, we're already using `new` and the fix continues
|
||
to use `new`... /Carol -->
|
||
<a class="header" href="#从new中返回result而不是调用panic" name="从new中返回result而不是调用panic"><h4>从<code>new</code>中返回<code>Result</code>而不是调用<code>panic!</code></h4></a>
|
||
<p>我们可以选择返回一个<code>Result</code>值,它在成功时会包含一个<code>Config</code>的实例,而在错误时会描述问题。当<code>Config::new</code>与<code>main</code>交流时,在使用<code>Result</code>类型存在问题时可以使用 Rust 的信号方式。接着修改<code>main</code>将<code>Err</code>成员转换为对用户更友好的错误,而不是<code>panic!</code>调用产生的关于<code>thread 'main'</code>和<code>RUST_BACKTRACE</code>的文本。</p>
|
||
<p>列表 12-9 展示了<code>Config::new</code>返回值和函数体中返回<code>Result</code>所需的改变:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">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: query,
|
||
filename: filename,
|
||
})
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-9: Return a <code>Result</code> from <code>Config::new</code></span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<!-- what does returning a Result rather than a Config do? -->
|
||
<!-- This is what Chapter 9 was about, I've added a few more references
|
||
to that chapter to reinforce the connection /Carol -->
|
||
<p>现在<code>new</code>函数返回一个<code>Result</code>,在成功时带有一个<code>Config</code>实例而在出现错误时带有一个<code>&'static str</code>。回忆一下第十章“静态声明周期”中讲到<code>&'static str</code>是一个字符串字面值,也是目前的错误信息。</p>
|
||
<p><code>new</code>函数体中有两处修改:当没有足够参数时不再调用<code>panic!</code>,而是返回<code>Err</code>值。同时我们将<code>Config</code>返回值包装进<code>Ok</code>成员中。这些修改使得函数符合其新的类型签名。</p>
|
||
<p>通过让<code>Config::new</code>返回一个<code>Err</code>值,这就允许<code>main</code>函数处理<code>new</code>函数返回的<code>Result</code>值并在出现错误的情况更明确的结束进程。</p>
|
||
<a class="header" href="#confignew调用并处理错误" name="confignew调用并处理错误"><h3><code>Config::new</code>调用并处理错误</h3></a>
|
||
<p>为了处理错误情况并打印一个对用户友好的信息,我们需要像列表 12-10 那样更新<code>main</code>函数来处理现在<code>Config::new</code>返回的<code>Result</code>。另外还需要实现一些<code>panic!</code>替我们处理的问题:使用错误码 1 退出命令行工具。非零的退出状态是一个告诉调用程序的进程我们的程序以错误状态退出的惯例信号。</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">use std::process;
|
||
|
||
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);
|
||
});
|
||
|
||
// ...snip...
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-10: Exiting with an error code if creating a
|
||
new <code>Config</code> fails</span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<!-- In the `main` function itself, we'll handle the `Result` value returned
|
||
from the `new` function and exit the process in a cleaner way if `Config::new`
|
||
returns an `Err` value.-->
|
||
<!-- I moved this line above to the previous section, it seems to at least
|
||
partially answer some of my earlier confusions, though I'm not following this
|
||
as well as I'd like so not sure if I have this right, can you confirm either
|
||
way whether that move makes sense? -->
|
||
<!-- That's fine /Carol -->
|
||
<p>在上面的列表中,使用了一个之前没有涉及到的方法:<code>unwrap_or_else</code>,它定义于标准库的<code>Result<T, E></code>上。使用<code>unwrap_or_else</code>可以进行一些自定义的非<code>panic!</code>的错误处理。当<code>Result</code>是<code>Ok</code>时,这个方法的行为类似于<code>unwrap</code>:它返回<code>Ok</code>内部封装的值。然而,当<code>Result</code>是<code>Err</code>时,它调用一个<strong>闭包</strong>(<em>closure</em>),也就是一个我们定义的作为参数传递给<code>unwrap_or_else</code>的匿名函数。第十三章会更详细的介绍闭包。现在你需要理解的是<code>unwrap_or_else</code>会将<code>Err</code>的内部值,也就是列表 12-9 中增加的<code>not enough arguments</code>静态字符串的情况,传递给闭包中位于两道竖线间的参数<code>err</code>。闭包中的代码在其运行时可以使用这个<code>err</code>值。</p>
|
||
<!--Can you give a high-level idea of what the closure does with it? -->
|
||
<!-- Does with what? I've tried to elaborate in the above and below paragraphs,
|
||
but I'm not sure exactly what's confusing /Carol -->
|
||
<p>我们新增了一个<code>use</code>行来从标准库中导入<code>process</code>。在错误的情况闭包中将被运行的代码只有两行:我们打印出了<code>err</code>值,接着调用了<code>std::process::exit</code>(在开头增加了新的<code>use</code>行从标准库中导入了<code>process</code>)。<code>process::exit</code>会立即停止程序并将传递给它的数字作为返回状态码。这类似于列表 12-8 中使用的基于<code>panic!</code>的错误处理,除了不会在得到所有的额外输出了。让我们试试:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling greprs v0.1.0 (file:///projects/greprs)
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.48 secs
|
||
Running `target/debug/greprs`
|
||
Problem parsing arguments: not enough arguments
|
||
</code></pre>
|
||
<p>非常好!现在输出对于用户来说就友好多了。</p>
|
||
<a class="header" href="#提取run函数" name="提取run函数"><h3>提取<code>run</code>函数</h3></a>
|
||
<p>现在我们完成了配置解析的重构:让我们转向程序的逻辑。正如“二进制项目的关注分离”部分的讨论所留下的过程,我们将提取一个叫做<code>run</code>的函数来存放目前<code>main</code>函数中不属于设置配置或处理错误的所有逻辑。一旦完成这些,<code>main</code>函数将简明的足以通过观察来验证,而我们将能够为所有其他逻辑编写测试。</p>
|
||
<!-- it contains ALL the function from main? Can you say why we're doing this,
|
||
hw this improves it? What is the run function doing? I'm afraid I feel a bit in
|
||
the dark here-->
|
||
<!-- This is the pattern that we explained in the Separation of Concerns for
|
||
Binary Projects section. I've added a reference back to that and reiterated
|
||
some of the reasoning from there, but this section isn't introducing the
|
||
concept of the `run` function holding the logic that was in `main` /Carol -->
|
||
<p>列表 12-11 展示了提取出来的<code>run</code>函数。目前我们只进行小的增量式的提取函数的改进并仍将在 <em>src/main.rs</em> 中定义这个函数:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
// ...snip...
|
||
|
||
println!("Searching for {}", config.query);
|
||
println!("In file {}", config.filename);
|
||
|
||
run(config);
|
||
}
|
||
|
||
fn run(config: Config) {
|
||
let mut f = File::open(config.filename).expect("file not found");
|
||
|
||
let mut contents = String::new();
|
||
f.read_to_string(&mut contents).expect("something went wrong reading the file");
|
||
|
||
println!("With text:\n{}", contents);
|
||
}
|
||
|
||
// ...snip...
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-11: Extracting a <code>run</code> function containing the
|
||
rest of the program logic</span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>现在<code>run</code>函数包含了<code>main</code>中从读取文件开始的剩余的所有逻辑。<code>run</code>函数获取一个<code>Config</code>实例作为参数。</p>
|
||
<a class="header" href="#从run函数中返回错误" name="从run函数中返回错误"><h4>从<code>run</code>函数中返回错误</h4></a>
|
||
<p>通过将剩余的逻辑分离进<code>run</code>函数而不是留在<code>main</code>中,就可以像列表 12-9 中的<code>Config::new</code>那样改进错误处理。不再通过通过<code>expect</code>允许程序 panic,<code>run</code>函数将会在出错时返回一个<code>Result<T, E></code>。这让我们进一步以一种对用户友好的方式统一<code>main</code>中的错误处理。列表 12-12 展示了<code>run</code>签名和函数体中的变化:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">use std::error::Error;
|
||
|
||
// ...snip...
|
||
|
||
fn run(config: Config) -> Result<(), Box<Error>> {
|
||
let mut f = File::open(config.filename)?;
|
||
|
||
let mut contents = String::new();
|
||
f.read_to_string(&mut contents)?;
|
||
|
||
println!("With text:\n{}", contents);
|
||
|
||
Ok(())
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-12: Changing the <code>run</code> function to return
|
||
<code>Result</code></span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>这里做出了三个大的改变。第一,改变了<code>run</code>函数的返回值为<code>Result<(), Box<Error>></code>。之前这个函数返回 unit 类型<code>()</code>,现在它仍然保持作为<code>Ok</code>时的返回值。</p>
|
||
<!-- is just the `Box` bit the trait object, or the whole `Box<Error>`
|
||
syntax?-->
|
||
<!-- The whole `Box<Error>` /Carol -->
|
||
<p>对于错误类型,使用了<strong>trait 对象</strong><code>Box<Error></code>(在开头使用了<code>use</code>语句将<code>std::error::Error</code>引入作用域)。第十七章会涉及 trait 对象。目前只需知道<code>Box<Error></code>意味着函数会返回实现了<code>Error</code> trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。</p>
|
||
<p>第二个改变是去掉了<code>expect</code>调用并替换为第九章讲到的<code>?</code>。不同于遇到错误就<code>panic!</code>,这会从函数中返回错误值并让调用者来处理它。</p>
|
||
<p>第三个修改是现在成功时这个函数会返回一个<code>Ok</code>值。因为<code>run</code>函数签名中声明成功类型返回值是<code>()</code>,这意味着需要将 unit 类型值包装进<code>Ok</code>值中。<code>Ok(())</code>一开始看起来有点奇怪,不过这样使用<code>()</code>是表明我们调用<code>run</code>只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。</p>
|
||
<p>上述代码能够编译,不过会有一个警告:</p>
|
||
<pre><code>warning: unused result which must be used, #[warn(unused_must_use)] on by default
|
||
--> src/main.rs:39:5
|
||
|
|
||
39 | run(config);
|
||
| ^^^^^^^^^^^^
|
||
</code></pre>
|
||
<p>Rust 提示我们的代码忽略了<code>Result</code>值,它可能表明这里存在一个错误。虽然我们没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正他们。</p>
|
||
<a class="header" href="#处理main中run返回的错误" name="处理main中run返回的错误"><h4>处理<code>main</code>中<code>run</code>返回的错误</h4></a>
|
||
<p>我们将检查错误并使用与列表 12-10 中处理错误类似的技术来优雅的处理他们,不过有一些细微的不同:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
// ...snip...
|
||
|
||
println!("Searching for {}", config.query);
|
||
println!("In file {}", config.filename);
|
||
|
||
if let Err(e) = run(config) {
|
||
println!("Application error: {}", e);
|
||
|
||
process::exit(1);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>我们使用<code>if let</code>来检查<code>run</code>是否返回一个<code>Err</code>值,不同于<code>unwrap_or_else</code>,并在出错时调用<code>process::exit(1)</code>。<code>run</code>并不返回像<code>Config::new</code>返回的<code>Config</code>实例那样需要<code>unwrap</code>的值。因为<code>run</code>在成功时返回<code>()</code>,而我们只关心发现一个错误,所以并不需要<code>unwrap_or_else</code>来返回未封装的值,因为它只会是<code>()</code>。</p>
|
||
<p>不过两个例子中<code>if let</code>和<code>unwrap_or_else</code>的函数体都一样:打印出错误并退出。</p>
|
||
<a class="header" href="#将代码拆分到库-crate" name="将代码拆分到库-crate"><h3>将代码拆分到库 crate</h3></a>
|
||
<p>现在项目看起来好多了!现在我们将要拆分 <em>src/main.rs</em> 并将一些代码放入 <em>src/lib.rs</em>,这样就能测试他们并拥有一个小的<code>main</code>函数。</p>
|
||
<p>让我们将如下代码片段从 <em>src/main.rs</em> 移动到新文件 <em>src/lib.rs</em> 中:</p>
|
||
<ul>
|
||
<li><code>run</code>函数定义</li>
|
||
<li>相关的<code>use</code>语句</li>
|
||
<li><code>Config</code>的定义</li>
|
||
<li><code>Config::new</code>函数定义</li>
|
||
</ul>
|
||
<p>现在 <em>src/lib.rs</em> 的内容应该看起来像列表 12-13:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">use std::error::Error;
|
||
use std::fs::File;
|
||
use std::io::prelude::*;
|
||
|
||
pub struct Config {
|
||
pub query: String,
|
||
pub filename: String,
|
||
}
|
||
|
||
impl Config {
|
||
pub 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: query,
|
||
filename: filename,
|
||
})
|
||
}
|
||
}
|
||
|
||
pub fn run(config: Config) -> Result<(), Box<Error>>{
|
||
let mut f = File::open(config.filename)?;
|
||
|
||
let mut contents = String::new();
|
||
f.read_to_string(&mut contents)?;
|
||
|
||
println!("With text:\n{}", contents);
|
||
|
||
Ok(())
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-13: Moving <code>Config</code> and <code>run</code> into
|
||
<em>src/lib.rs</em></span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>这里使用了公有的<code>pub</code>:在<code>Config</code>、其字段和其<code>new</code>方法,以及<code>run</code>函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。</p>
|
||
<a class="header" href="#从二进制-crate-中调用库-crate" name="从二进制-crate-中调用库-crate"><h4>从二进制 crate 中调用库 crate</h4></a>
|
||
<p>现在需要在 <em>src/main.rs</em> 中使用<code>extern crate greprs</code>将移动到 <em>src/lib.rs</em> 的代码引入二进制 crate 的作用域。接着我们将增加一个<code>use greprs::Config</code>行将<code>Config</code>类型引入作用域,并使用库 crate 的名称作为<code>run</code>函数的前缀,如列表 12-14 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">extern crate greprs;
|
||
|
||
use std::env;
|
||
use std::process;
|
||
|
||
use greprs::Config;
|
||
|
||
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);
|
||
});
|
||
|
||
println!("Searching for {}", config.query);
|
||
println!("In file {}", config.filename);
|
||
|
||
if let Err(e) = greprs::run(config) {
|
||
println!("Application error: {}", e);
|
||
|
||
process::exit(1);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-14: Bringing the <code>greprs</code> crate into the scope
|
||
of <em>src/main.rs</em></span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>通过这些重构,所有功能应该抖联系在一起并可以运行了。运行<code>cargo run</code>来确保一切都正确的衔接在一起。</p>
|
||
<!-- any tips for if they do find something is broken, main places to check? Or
|
||
just "diff your file against the XXX file in the book's resources to check
|
||
where it went wrong"? -->
|
||
<!-- We think general troubleshooting tips should be something we cover in
|
||
Chapter 1; the tips should apply to any example in the book /Carol -->
|
||
<p>哇哦!这可有很多的工作,不过我们为将来成功打下了基础。现在处理错误将更容易,同时代码也更模块化。从现在开始几乎所有的工作都将在 <em>src/lib.rs</em> 中进行。</p>
|
||
<p>让我们利用这些新创建的模块的优势来进行一些在旧代码中难以展开的工作,他们在新代码中却很简单:编写测试!</p>
|
||
|
||
</div>
|
||
|
||
<!-- Mobile navigation buttons -->
|
||
|
||
<a href="ch12-02-reading-a-file.html" class="mobile-nav-chapters previous">
|
||
<i class="fa fa-angle-left"></i>
|
||
</a>
|
||
|
||
|
||
|
||
<a href="ch12-04-testing-the-librarys-functionality.html" class="mobile-nav-chapters next">
|
||
<i class="fa fa-angle-right"></i>
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
|
||
<a href="ch12-02-reading-a-file.html" class="nav-chapters previous" title="You can navigate through the chapters using the arrow keys">
|
||
<i class="fa fa-angle-left"></i>
|
||
</a>
|
||
|
||
|
||
|
||
<a href="ch12-04-testing-the-librarys-functionality.html" class="nav-chapters next" title="You can navigate through the chapters using the arrow keys">
|
||
<i class="fa fa-angle-right"></i>
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
|
||
<!-- Local fallback for Font Awesome -->
|
||
<script>
|
||
if ($(".fa").css("font-family") !== "FontAwesome") {
|
||
$('<link rel="stylesheet" type="text/css" href="_FontAwesome/css/font-awesome.css">').prependTo('head');
|
||
}
|
||
</script>
|
||
|
||
<!-- Livereload script (if served using the cli tool) -->
|
||
|
||
|
||
<script src="highlight.js"></script>
|
||
<script src="book.js"></script>
|
||
</body>
|
||
</html>
|