mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-14 04:41:49 +08:00
10499 lines
839 KiB
HTML
10499 lines
839 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"><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><li><a href="ch18-00-patterns.html"><strong>18.</strong> 模式用来匹配值的结构</a></li><li><ul class="section"><li><a href="ch18-01-all-the-places-for-patterns.html"><strong>18.1.</strong> 所有可能会用到模式的位置</a></li><li><a href="ch18-02-refutability.html"><strong>18.2.</strong> refutable:何时模式可能会匹配失败</a></li><li><a href="ch18-03-pattern-syntax.html"><strong>18.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="介绍"><h1>介绍</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch01-00-introduction.md">ch01-00-introduction.md</a>
|
||
<br>
|
||
commit 62f78bb3f7c222b574ff547d0161c2533691f9b4</p>
|
||
</blockquote>
|
||
<p>欢迎阅读“Rust 程序设计语言”,一本介绍 Rust 的书。Rust 是一门着眼于安全、速度和并发的编程语言。它的设计兼顾性能与底层控制,以及高级语言强大的抽象能力。适合那些有类 C 语言经验,正在寻找更安全的替代品的开发者;以及有着类 Python 语言背景,寻求在不牺牲表现力的前提下,编写性能更好的代码的开发者。</p>
|
||
<p>Rust 主要在编译时执行安全检查和内存管理决策,对运行时性能的影响微不足道。这使其在许多语言不擅长的应用场景中得以大显身手:空间和时间需求可预测的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package
|
||
registry site),<a href="https://crates.io/">crates.io</a>!我们期待<strong>你</strong>使用 Rust 进行创作。</p>
|
||
<p>本书的目标读者至少应了解一门其它编程语言。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过短小精干、前后呼应的例子来学习 Rust,并展示其多样功能的使用方法,同时了解幕后如何运行。</p>
|
||
<a class="header" href="#为本书做出贡献" name="为本书做出贡献"><h2>为本书做出贡献</h2></a>
|
||
<p>本书是开源的。如果你发现任何错误,不要犹豫,<a href="https://github.com/rust-lang/book">在 GitHub 上</a>发起 issue 或提交 pull request。请查看 <a href="https://github.com/rust-lang/book/blob/master/CONTRIBUTING.md">CONTRIBUTING.md</a> 获取更多信息。</p>
|
||
<blockquote>
|
||
<p>译者注:译本的 <a href="https://github.com/KaiserY/trpl-zh-cn">GitHub 仓库</a>,同样欢迎 Issue 和 PR :)</p>
|
||
</blockquote>
|
||
<a class="header" href="#安装" name="安装"><h2>安装</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch01-01-installation.md">ch01-01-installation.md</a>
|
||
<br>
|
||
commit c1b95a18dbcbb06aadf07c03759f27d88ccf62cf</p>
|
||
</blockquote>
|
||
<p>第一步是安装 Rust。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。</p>
|
||
<p>我们将会展示很多在终端中输入的命令,这些命令均以 <code>$</code> 开头。你不需要真的输入<code>$</code>,在这里它代表每行命令的起始。网上有很多教程和例子遵循这种惯例:<code>$</code> 代表以常规用户身份运行命令,<code>#</code> 代表需要用管理员身份运行命令。没有以 <code>$</code>(或 <code>#</code>)起始的行通常是之前命令的输出。</p>
|
||
<a class="header" href="#在-linux-或-mac-上安装" name="在-linux-或-mac-上安装"><h3>在 Linux 或 Mac 上安装</h3></a>
|
||
<p>如果你使用 Linux 或 Mac,你需要做的全部,就是打开一个终端并输入:</p>
|
||
<pre><code>$ curl https://sh.rustup.rs -sSf | sh
|
||
</code></pre>
|
||
<p>这会下载一个脚本并开始安装。可能会提示你输入密码,如果一切顺利,将会出现如下内容:</p>
|
||
<pre><code>Rust is installed now. Great!
|
||
</code></pre>
|
||
<p>当然,如果你对于 <code>curl | sh</code> 心有疑虑,你可以随意下载、检查和运行这个脚本。</p>
|
||
<a class="header" href="#在-windows-上安装" name="在-windows-上安装"><h3>在 Windows 上安装</h3></a>
|
||
<p>如果你使用 Windows,前往 <a href="https://rustup.rs/">https://rustup.rs</a><!-- ignore -->,按说明下载 rustup-init.exe,运行并照其指示操作。</p>
|
||
<p>本书中其余 Windows 相关的命令,假设你使用 <code>cmd</code> 作为 shell。如果你使用其它 shell,也许可以执行与 Linux 和 Mac 用户相同的命令。如果不行,请查看该 shell 的文档。</p>
|
||
<a class="header" href="#自定义安装" name="自定义安装"><h3>自定义安装</h3></a>
|
||
<p>无论出于何种理由,如果不愿意使用 rustup.rs,请查看 <a href="https://www.rust-lang.org/install.html">Rust 安装页面</a> 获取其他选择。</p>
|
||
<a class="header" href="#更新" name="更新"><h3>更新</h3></a>
|
||
<p>一旦 Rust 安装完,更新到最新版本很简单。在 shell 中执行:</p>
|
||
<pre><code>$ rustup update
|
||
</code></pre>
|
||
<a class="header" href="#卸载" name="卸载"><h3>卸载</h3></a>
|
||
<p>卸载 Rust 同样简单。在 shell 中执行:</p>
|
||
<pre><code>$ rustup self uninstall
|
||
</code></pre>
|
||
<a class="header" href="#故障排除" name="故障排除"><h3>故障排除</h3></a>
|
||
<p>安装完 Rust 后,在 shell 中执行:</p>
|
||
<pre><code>$ rustc --version
|
||
</code></pre>
|
||
<p>应该能看到类似这样的版本号、提交哈希和提交日期,对应安装时的最新稳定版:</p>
|
||
<pre><code>rustc x.y.z (abcabcabc yyyy-mm-dd)
|
||
</code></pre>
|
||
<p>出现这些内容,Rust 就安装成功了!</p>
|
||
<p>恭喜入坑!(此处应该有掌声!)</p>
|
||
<p>如果在 Windows 中使用出现问题,检查 Rust(rustc,cargo 等)是否在 <code>%PATH%</code> 环境变量所包含的路径中。</p>
|
||
<p>如果还是不能解决,有许多地方可以求助。最简单的是 <a href="irc://irc.mozilla.org/#rust">irc.mozilla.org 上的 #rust IRC 频道</a><!-- ignore --> ,可以使用 <a href="http://chat.mibbit.com/?server=irc.mozilla.org&channel=%23rust">Mibbit</a> 来访问它。然后就能和其他 Rustacean(Rust 用户的称号,有自嘲意味)聊天并寻求帮助。其它给力的资源包括<a href="https://users.rust-lang.org/">用户论坛</a>和 <a href="http://stackoverflow.com/questions/tagged/rust">Stack Overflow</a>。</p>
|
||
<a class="header" href="#本地文档" name="本地文档"><h3>本地文档</h3></a>
|
||
<p>安装程序自带本地文档,可以离线阅读。输入 <code>rustup doc</code> 可以在浏览器中查看。</p>
|
||
<p>任何时候,如果你拿不准标准库中类型或函数,请查看 API 文档!</p>
|
||
<a class="header" href="#hello-world" name="hello-world"><h2>Hello, World!</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch01-02-hello-world.md">ch01-02-hello-world.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p>Rust 已安好,让我们来编写第一个程序。当学习一门新语言的时候,使用该语言在屏幕上打印 “Hello, world!” 是一项传统,我们将遵循这个传统。</p>
|
||
<blockquote>
|
||
<p>注意:本书假设你熟悉基本的命令行操作。对于你的编辑器、工具,以及你的代码存在何处,Rust 并没有特殊要求,如果你更喜欢 IDE,请随意。</p>
|
||
</blockquote>
|
||
<a class="header" href="#创建项目文件夹" name="创建项目文件夹"><h3>创建项目文件夹</h3></a>
|
||
<p>首先,创建一个存放代码的文件夹。Rust 并不关心它的位置,不过在本书中,我们建议你在 home 目录中创建一个 <em>projects</em> 目录,并把你的所有项目放在这。打开一个终端,输入如下命令来创建一个文件夹:</p>
|
||
<p>Linux 和 Mac:</p>
|
||
<pre><code>$ mkdir ~/projects
|
||
$ cd ~/projects
|
||
$ mkdir hello_world
|
||
$ cd hello_world
|
||
</code></pre>
|
||
<p>Windows:</p>
|
||
<pre><code class="language-cmd">> mkdir %USERPROFILE%\projects
|
||
> cd %USERPROFILE%\projects
|
||
> mkdir hello_world
|
||
> cd hello_world
|
||
</code></pre>
|
||
<a class="header" href="#编写并运行-rust-程序" name="编写并运行-rust-程序"><h3>编写并运行 Rust 程序</h3></a>
|
||
<p>接下来,新建一个叫做 <em>main.rs</em> 的文件。Rust 源代码总是以 <em>.rs</em> 后缀结尾。如果文件名包含多个单词,使用下划线分隔它们。例如 <em>my_program.rs</em>,而不是 <em>myprogram.rs</em>。</p>
|
||
<p>现在打开刚创建的 <em>main.rs</em> 文件,输入如下代码:</p>
|
||
<p><span class="filename">Filename: main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
println!("Hello, world!");
|
||
}
|
||
</code></pre>
|
||
<p>保存文件,并回到终端窗口。在 Linux 或 OSX 上,输入如下命令:</p>
|
||
<pre><code>$ rustc main.rs
|
||
$ ./main
|
||
Hello, world!
|
||
</code></pre>
|
||
<p>在 Windows 上,运行 <code>.\main.exe</code>,而不是<code>./main</code>。不管使用何种系统,你应该在终端看到 <code>Hello, world!</code> 字样。如果你做到了,恭喜你!你已经正式编写了一个 Rust 程序,成为一名 Rust 程序员!</p>
|
||
<a class="header" href="#分析-rust-程序" name="分析-rust-程序"><h3>分析 Rust 程序</h3></a>
|
||
<p>现在,让我们回过头来,仔细看看“Hello, world!”程序到底发生了什么。这是拼图的第一片:</p>
|
||
<pre><code class="language-rust">fn main() {
|
||
|
||
}
|
||
</code></pre>
|
||
<p>这几行定义了一个 Rust <strong>函数</strong>。一个叫 <code>main</code> 的函数,没有参数也没有返回值。如果有参数的话,它们应该出现在括弧中,<code>(</code>和<code>)</code>之间。<code>main</code> 函数是特殊的:它是每一个可执行的 Rust 程序的入口点。</p>
|
||
<p>还须注意函数体被包裹在花括号中,<code>{</code>和<code>}</code> 之间。所有函数体都要用花括号包裹起来(译者注:有些语言,当函数体只有一行时可以省略花括号,但 Rust 中是不行的)。一般来说,将左花括号与函数声明置于一行,并以空格分隔,是良好的代码风格。</p>
|
||
<p>在 <code>main()</code> 函数中:</p>
|
||
<pre><code class="language-rust"> println!("Hello, world!");
|
||
</code></pre>
|
||
<p>一行代码完成这个小程序的所有工作:在屏幕上打印文本。这里有很多细节需要注意。首先 Rust 使用 4 个空格的缩进风格,而不是 1 个制表符(tab)。</p>
|
||
<p>第二个重要的部分是<code>println!()</code>。这是 <strong>宏</strong>,Rust 元编程(metaprogramming)的关键所在。而调用一个函数,则要像这样:<code>println</code>(没有<code>!</code>)。我们将在 21 章 E 小节中更加详细的讨论宏,现在你只需记住,当看到符号 <code>!</code> 的时候,调用的是宏而不是普通函数。</p>
|
||
<p>接下来,<code>"Hello, world!"</code> 是一个 <strong>字符串</strong>。我们把这个字符串作为一个参数传递给<code>println!</code>,它负责在屏幕上打印这个字符串。轻松加愉快!(⊙o⊙)</p>
|
||
<p>该行以分号结尾(<code>;</code>)。<code>;</code> 代表一个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以 <code>;</code> 结尾。</p>
|
||
<a class="header" href="#编译和运行是两个步骤" name="编译和运行是两个步骤"><h3>编译和运行是两个步骤</h3></a>
|
||
<p>“编写并运行 Rust 程序”部分,展示了如何创建运行程序。现在我们将拆分并检查每一步操作。</p>
|
||
<p>运行一个 Rust 程序之前,必须先编译它。可以通过 <code>rustc</code> 命令来使用 Rust 编译器,并传递源文件的名字给它,如下:</p>
|
||
<pre><code>$ rustc main.rs
|
||
</code></pre>
|
||
<p>如果你有 C 或 C++ 背景,就会发现这与 <code>gcc</code> 和 <code>clang</code> 类似。编译成功后,Rust 应该会输出一个二进制可执行文件,在 Linux 或 OSX 上在 shell 中你可以通过<code>ls</code>命令看到如下:</p>
|
||
<pre><code>$ ls
|
||
main main.rs
|
||
</code></pre>
|
||
<p>在 Windows 上,输入:</p>
|
||
<pre><code class="language-cmd">> dir /B %= the /B option says to only show the file names =%
|
||
main.exe
|
||
main.rs
|
||
</code></pre>
|
||
<p>这表示我们有两个文件:<em>.rs</em> 后缀的源文件,和可执行文件(在 Windows下是 <em>main.exe</em>,其它平台是 <em>main</em>)。然后运行 <em>main</em> 或 <em>main.exe</em> 文件,像这样:</p>
|
||
<pre><code>$ ./main # or .\main.exe on Windows
|
||
</code></pre>
|
||
<p>如果 <em>main.rs</em> 是我们的“Hello, world!”程序,它将会在终端上打印<code>Hello, world!</code>。</p>
|
||
<p>来自 Ruby、Python 或 JavaScript 这样的动态类型语言背景的同学,可能不太习惯将编译和执行分为两个步骤。Rust 是一种 <strong>预编译静态类型语言</strong>(<em>ahead-of-time compiled language</em>),这意味着编译好程序后,把它给任何人,他们不需要安装 Rust 就可运行。如果你给他们一个 <code>.rb</code> , <code>.py</code> 或 <code>.js</code> 文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM),不过你只需要一句命令就可以编译和执行程序。这一切都是语言设计上的权衡取舍。</p>
|
||
<p>使用 <code>rustc</code> 编译简单程序是没问题的,不过随着项目的增长,你可能需要控制你项目的方方面面,并且更容易地将代码分享给其它人或项目。所以接下来,我们要介绍一个叫做 Cargo 的工具,它会帮助你编写真实世界中的 Rust 程序。</p>
|
||
<a class="header" href="#hello-cargo" name="hello-cargo"><h2>Hello, Cargo!</h2></a>
|
||
<p>Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载依赖库并编译。我们把代码需要的库叫做 <strong>依赖</strong>(<em>dependencies</em>)。</p>
|
||
<p>最简单的 Rust 程序,比如我们刚刚编写的,并没有任何依赖,所以我们只使用了 Cargo 构建代码的功能。随着更复杂程序的编写,你会想要添加依赖,如果你使用 Cargo 开始的话,这将会变得简单许多。</p>
|
||
<p>由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用之前介绍的官方安装包的话,它自带 Cargo。如果通过其他方式安装的话,可以在终端输入如下命令,检查是否安装了 Cargo:</p>
|
||
<pre><code>$ cargo --version
|
||
</code></pre>
|
||
<p>如果出现了版本号,一切 OK!如果出现类似“<code>command not found</code>”的错误,你应该查看安装文档以确定如何单独安装 Cargo。</p>
|
||
<a class="header" href="#使用-cargo-创建项目" name="使用-cargo-创建项目"><h3>使用 Cargo 创建项目</h3></a>
|
||
<p>让我们使用 Cargo 来创建一个新项目,然后看看与上面的<code>hello_world</code>项目有什么不同。回到 projects 目录(或者任何你放置代码的目录):</p>
|
||
<p>Linux 和 Mac:</p>
|
||
<pre><code>$ cd ~/projects
|
||
</code></pre>
|
||
<p>Windows:</p>
|
||
<pre><code class="language-cmd">> cd %USERPROFILE%\projects
|
||
</code></pre>
|
||
<p>并在任何操作系统运行:</p>
|
||
<pre><code>$ cargo new hello_cargo --bin
|
||
$ cd hello_cargo
|
||
</code></pre>
|
||
<p>我们向 <code>cargo new</code> 传递了 <code>--bin</code>,因为我们的目标是生成一个可执行程序,而不是一个库。可执行程序是二进制可执行文件,通常就叫做 <strong>二进制文件</strong>(<em>binaries</em>)。项目的名称被定为<code>hello_cargo</code>,同时 Cargo 在一个同名目录中创建它的文件,接着我们可以进入查看。</p>
|
||
<p>如果列出 <em>hello_cargo</em> 目录中的文件,将会看到 Cargo 生成了一个文件和一个目录:一个 <em>Cargo.toml</em> 文件和一个 <em>src</em> 目录,<em>main.rs</em> 文件位于目录中。它也在 <em>hello_cargo</em> 目录初始化了一个 git 仓库,以及一个 <em>.gitignore</em> 文件;你可以通过<code>--vcs</code>参数,切换到其它版本控制系统(VCS),或者不使用 VCS。</p>
|
||
<p>使用文本编辑器(IDE)打开 <em>Cargo.toml</em> 文件。它应该看起来像这样:</p>
|
||
<p><span class="filename">Filename: Cargo.toml</span></p>
|
||
<pre><code class="language-toml">[package]
|
||
name = "hello_cargo"
|
||
version = "0.1.0"
|
||
authors = ["Your Name <you@example.com>"]
|
||
|
||
[dependencies]
|
||
</code></pre>
|
||
<p>这个文件使用 <a href="https://github.com/toml-lang/toml"><em>TOML</em></a><!-- ignore --> (Tom's Obvious, Minimal Language) 格式。TOML 类似于 INI,不过有一些额外的改进之处,并且被用作 Cargo 的配置文件的格式。</p>
|
||
<p>第一行,<code>[package]</code>,是一个段落标题,表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他段落。</p>
|
||
<p>最后一行,<code>[dependencies]</code>,是项目依赖的 <em>crates</em>(Rust 代码包)的段落的开始,这样 Cargo 就知道下载和编译它们了。这个项目并不需要任何其他的 crate,不过在下一章猜猜看教程会需要。</p>
|
||
<p>现在看看 <em>src/main.rs</em>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
println!("Hello, world!");
|
||
}
|
||
</code></pre>
|
||
<p>Cargo 为你生成了一个“Hello World!”,正如我们之前编写的那个!目前为止,之前项目与 Cargo 生成项目区别有:</p>
|
||
<ul>
|
||
<li>代码位于 <em>src</em> 目录</li>
|
||
<li>项目根目录包含一个 <em>Cargo.toml</em> 配置文件</li>
|
||
</ul>
|
||
<p>Cargo 期望源文件位于 <em>src</em> 目录,将项目根目录留给 README、license 信息、配置文件和其他跟代码无关的文件。这样,Cargo 帮助你保持项目干净整洁,一切井井有条。</p>
|
||
<p>如果没有用 Cargo 创建项目,比如 <em>hello_world</em> 目录中的项目,可以通过将代码放入 <em>src</em> 目录,并创建一个合适的 <em>Cargo.toml</em>,将其转化为一个 Cargo 项目。</p>
|
||
<a class="header" href="#构建并运行-cargo-项目" name="构建并运行-cargo-项目"><h3>构建并运行 Cargo 项目</h3></a>
|
||
<p>现在让我们看看通过 Cargo 构建和运行 Hello World 程序有什么不同。为此,输入如下命令:</p>
|
||
<pre><code>$ cargo build
|
||
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
|
||
</code></pre>
|
||
<p>这应该创建 <em>target/debug/hello_cargo</em>(或者在 Windows 上是 <em>target\debug\hello_cargo.exe</em>)可执行文件,可以通过这个命令运行:</p>
|
||
<pre><code>$ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
|
||
Hello, world!
|
||
</code></pre>
|
||
<p>好的!如果一切顺利,<code>Hello, world!</code>应该再次打印在终端上。</p>
|
||
<p>首次运行 <code>cargo build</code> 的时候,Cargo 会在项目根目录创建一个新文件,<em>Cargo.lock</em>,它看起来像这样:</p>
|
||
<p><span class="filename">Filename: Cargo.lock</span></p>
|
||
<pre><code class="language-toml">[root]
|
||
name = "hello_cargo"
|
||
version = "0.1.0"
|
||
</code></pre>
|
||
<p>Cargo 使用 <em>Cargo.lock</em> 来记录程序的依赖。这个项目并没有依赖,所以内容比较少。事实上,你自己永远也不需要碰这个文件,让 Cargo 处理它就行了。</p>
|
||
<p>我们刚刚使用 <code>cargo build</code> 构建了项目并使用 <code>./target/debug/hello_cargo</code> 运行了它,也可以使用 <code>cargo run</code> 编译并运行:</p>
|
||
<pre><code>$ cargo run
|
||
Running `target/debug/hello_cargo`
|
||
Hello, world!
|
||
</code></pre>
|
||
<p>注意这一次并没有出现“正在编译 <code>hello_cargo</code>”的输出。Cargo 发现文件并没有被改变,直接运行了二进制文件。如果修改了源文件的话,将会出现像这样的输出:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
|
||
Running `target/debug/hello_cargo`
|
||
Hello, world!
|
||
</code></pre>
|
||
<p>所以现在又出现更多的不同:</p>
|
||
<ul>
|
||
<li>使用 <code>cargo build</code> 构建项目(或使用 <code>cargo run</code> 一步构建并运行),而不是使用<code>rustc</code></li>
|
||
<li>有别于将构建结果放在源码目录,Cargo 将它放到 <em>target/debug</em> 目录。</li>
|
||
</ul>
|
||
<p>Cargo 的另一个优点是,不管你使用什么操作系统,它的命令都是一样的,所以之后我们将不再为 Linux 和 Mac 以及 Windows 提供相应的命令。</p>
|
||
<a class="header" href="#发布构建" name="发布构建"><h3>发布构建</h3></a>
|
||
<p>当项目最终准备好发布了,可以使用 <code>cargo build --release</code> 来优化编译项目。这会在 <em>target/release</em> 下生成可执行文件,而不是 <em>target/debug</em>。优化可以让 Rust 代码运行的更快,然而也需要更长的编译时间。因此产生了两种不同的配置:一种为了开发,你需要快速重新构建;另一种构建给用户的最终程序,不会重新构建,并且程序运行得越快越好。如果你在测试代码的运行时间,请确保运行 <code>cargo build --release</code> 并使用 <em>target/release</em> 下的可执行文件。</p>
|
||
<a class="header" href="#把-cargo-当作习惯" name="把-cargo-当作习惯"><h3>把 Cargo 当作习惯</h3></a>
|
||
<p>对于简单项目, Cargo 并不比 <code>rustc</code> 更有价值,不过随着开发的进行终将体现它的价值。对于拥有多个 crate 的复杂项目,让 Cargo 来协调构建将更简单。有了 Cargo,只需运行<code>cargo build</code>,然后一切将有序运行。即便这个项目很简单,也它使用了很多你之后的 Rust 生涯将会用得上的实用工具。其实你可以开始任何你想要从事的项目,使用下面的命令:</p>
|
||
<pre><code>$ git clone someurl.com/someproject
|
||
$ cd someproject
|
||
$ cargo build
|
||
</code></pre>
|
||
<blockquote>
|
||
<p>注意:如果想要了解 Cargo 更多的细节,请阅读官方的 <a href="http://doc.crates.io/guide.html">Cargo guide</a>,它覆盖了所有的功能。</p>
|
||
</blockquote>
|
||
<a class="header" href="#猜猜看" name="猜猜看"><h1>猜猜看</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch02-00-guessing-game-tutorial.md">ch02-00-guessing-game-tutorial.md</a>
|
||
<br>
|
||
commit e6d6caab41471f7115a621029bd428a812c5260e</p>
|
||
</blockquote>
|
||
<p>让我们亲自动手,快速熟悉 Rust!本章将介绍 Rust 中常用的一些概念,并通过真实的程序来展示如何运用。你将会学到更多诸如 <code>let</code>、<code>match</code>、方法、关联函数、外部 crate 等知识!后继章节会深入探索这些概念的细节。在这一章,我们将练习基础。</p>
|
||
<p>我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会请玩家猜一个数并输入,然后提示猜测是大了还是小了。如果猜对了,它会在退出前祝贺你。</p>
|
||
<a class="header" href="#准备一个新项目" name="准备一个新项目"><h2>准备一个新项目</h2></a>
|
||
<p>要创建一个新项目,进入第一章创建的<strong>项目</strong>目录,使用 Cargo 创建它:</p>
|
||
<pre><code>$ cargo new guessing_game --bin
|
||
$ cd guessing_game
|
||
</code></pre>
|
||
<p>第一个命令,<code>cargo new</code>,获取项目的名称(<code>guessing_game</code>)作为第一个参数。<code>--bin</code>参数告诉 Cargo 创建一个二进制项目,与第一章类似。第二个命令进入到新创建的项目目录。</p>
|
||
<p>看看生成的 <em>Cargo.toml</em> 文件:</p>
|
||
<p><span class="filename">Filename: Cargo.toml</span></p>
|
||
<pre><code class="language-toml">[package]
|
||
name = "guessing_game"
|
||
version = "0.1.0"
|
||
authors = ["Your Name <you@example.com>"]
|
||
|
||
[dependencies]
|
||
</code></pre>
|
||
<p>如果 Cargo 从环境中获取的开发者信息不正确,修改这个文件并再次保存。</p>
|
||
<p>正如第一章那样,<code>cargo new</code> 生成了一个“Hello, world!”程序。查看 <em>src/main.rs</em> 文件:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
println!("Hello, world!");
|
||
}
|
||
</code></pre>
|
||
<p>现在让我们使用 <code>cargo run</code>,编译运行一步到位:</p>
|
||
<pre><code class="language-sh">$ cargo run
|
||
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
Running `target/debug/guessing_game`
|
||
Hello, world!
|
||
</code></pre>
|
||
<p><code>run</code> 命令适合用在需要快速迭代的项目,而这个游戏就是:我们需要在下一步迭代之前快速测试。</p>
|
||
<p>重新打开 <em>src/main.rs</em> 文件。我们将会在这个文件中编写全部代码。</p>
|
||
<a class="header" href="#处理一次猜测" name="处理一次猜测"><h2>处理一次猜测</h2></a>
|
||
<p>程序的第一部分请求和处理用户输入,并检查输入是否符合预期。首先,需要有一个让玩家输入猜测的地方。在 <em>src/main.rs</em> 中输入列表 2-1 中的代码。</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">use std::io;
|
||
|
||
fn main() {
|
||
println!("Guess the number!");
|
||
|
||
println!("Please input your guess.");
|
||
|
||
let mut guess = String::new();
|
||
|
||
io::stdin().read_line(&mut guess)
|
||
.expect("Failed to read line");
|
||
|
||
println!("You guessed: {}", guess);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 2-1: Code to get a guess from the user and print it out</span></p>
|
||
<p>这些代码包含很多信息,我们一点一点地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 <code>io</code>(输入/输出)库引入当前作用域。<code>io</code>库来自于标准库(也被称为<code>std</code>):</p>
|
||
<pre><code class="language-rust,ignore">use std::io;
|
||
</code></pre>
|
||
<p>Rust 默认只在每个程序的 <a href="https://doc.rust-lang.org/std/prelude/index.html"><em>prelude</em></a><!-- ignore --> 中引入少量类型。如果需要的类型不在 prelude 中,你必须使用一个 <code>use</code> 语句显式的将其引入作用域。<code>std::io</code> 库提供很多 <code>io</code> 相关的功能,比如接受用户输入。</p>
|
||
<p>如第一章所提及,<code>main</code> 函数是程序的入口点:</p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
</code></pre>
|
||
<p><code>fn</code> 语法声明了一个新函数,<code>()</code> 表明没有参数,<code>{</code> 作为函数体的开始。</p>
|
||
<p>第一章也提及,<code>println!</code> 是一个在屏幕上打印字符串的宏:</p>
|
||
<pre><code class="language-rust,ignore">println!("Guess the number!");
|
||
|
||
println!("Please input your guess.");
|
||
</code></pre>
|
||
<p>这些代码仅仅打印提示,介绍游戏的内容然后请用户输入。</p>
|
||
<a class="header" href="#用变量储存值" name="用变量储存值"><h3>用变量储存值</h3></a>
|
||
<p>接下来,创建一个地方储存用户输入,像这样:</p>
|
||
<pre><code class="language-rust">let mut guess = String::new();
|
||
</code></pre>
|
||
<p>现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个 <code>let</code> 语句,用来创建<strong>变量</strong>。这里是另外一个例子:</p>
|
||
<pre><code class="language-rust">let foo = bar;
|
||
</code></pre>
|
||
<p>这行代码会创建一个叫做 <code>foo</code> 的新变量并把它绑定到值 <code>bar</code> 上。在 Rust 中,变量默认是不可变的。下面的例子展示了如何在变量名前使用 <code>mut</code> 来使一个变量可变:</p>
|
||
<pre><code class="language-rust">let foo = 5; // immutable
|
||
let mut bar = 5; // mutable
|
||
</code></pre>
|
||
<blockquote>
|
||
<p>注意:<code>//</code> 开始一个注释,它持续到本行的结尾。Rust 忽略注释中的所有内容。</p>
|
||
</blockquote>
|
||
<p>现在我们知道了 <code>let mut guess</code> 会引入一个叫做 <code>guess</code> 的可变变量。等号(<code>=</code>)的右边是 <code>guess</code> 所绑定的值,它是 <code>String::new</code> 的结果,这个函数会返回一个 <code>String</code> 的新实例。<a href="https://doc.rust-lang.org/std/string/struct.String.html"><code>String</code></a><!-- ignore --> 是一个标准库提供的字符串类型,它是 UTF-8 编码的可增长文本块。</p>
|
||
<p><code>::new</code> 那一行的 <code>::</code> 语法表明 <code>new</code> 是 <code>String</code> 类型的一个 <strong>关联函数</strong>(<em>associated function</em>)。关联函数是针对类型实现的,在这个例子中是 <code>String</code>,而不是 <code>String</code> 的某个特定实例。一些语言中把它称为<strong>静态方法</strong>(<em>static method</em>)。</p>
|
||
<p><code>new</code> 函数创建了一个新的空 <code>String</code>,你会在很多类型上发现<code>new</code> 函数,这是创建类型实例的惯用函数名。</p>
|
||
<p>总结一下,<code>let mut guess = String::new();</code> 这一行创建了一个可变变量,绑定到一个新的 <code>String</code> 空实例上。</p>
|
||
<p>回忆一下,我们在程序的第一行使用 <code>use std::io;</code> 从标准库中引入“输入输出”。现在调用 <code>io</code> 的关联函数 <code>stdin</code>:</p>
|
||
<pre><code class="language-rust,ignore">io::stdin().read_line(&mut guess)
|
||
.expect("Failed to read line");
|
||
</code></pre>
|
||
<p>如果程序的开头没有 <code>use std::io</code> 这一行,我们可以把函数调用写成 <code>std::io::stdin</code>。<code>stdin</code> 函数返回一个 <a href="https://doc.rust-lang.org/std/io/struct.Stdin.html"><code>std::io::Stdin</code></a><!-- ignore --> 的实例,这代表终端标准输入句柄的类型。</p>
|
||
<p>代码的下一部分,<code>.read_line(&mut guess)</code>,调用 <a href="https://doc.rust-lang.org/std/io/struct.Stdin.html#method.read_line"><code>read_line</code></a><!-- ignore --> 方法从标准输入句柄获取用户输入。我们还向 <code>read_line()</code> 传递了一个参数:<code>&mut guess</code>。</p>
|
||
<p><code>read_line</code> 的工作是,无论用户在标准输入中键入什么内容,都将其存入一个字符串中,因此它需要字符串作为参数。这个字符串参数应该是可变的,以便 <code>read_line</code> 将用户输入附加上去。</p>
|
||
<p><code>&</code> 表示这个参数是一个<strong>引用</strong>(<em>reference</em>),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的操纵引用。完成当前程序并不需要了解如此多细节:第四章会更全面的解释引用。现在,我们只需知道它像变量一样,默认是不可变的,需要写成 <code>&mut guess</code> 而不是 <code>&guess</code> 来使其可变。</p>
|
||
<p>我们还没有分析完这行代码。虽然这是单独一行代码,但它是一个逻辑行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法:</p>
|
||
<pre><code class="language-rust">.expect("Failed to read line");
|
||
</code></pre>
|
||
<p>当使用 <code>.expect()</code> 语法调用方法时,通过‘换行并缩进’来把长行拆开,是明智的。我们完全可以这样写:</p>
|
||
<pre><code class="language-rust">io::stdin().read_line(&mut guess).expect("Failed to read line");
|
||
</code></pre>
|
||
<p>不过,过长的行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。</p>
|
||
<a class="header" href="#使用-result-类型来处理潜在的错误" name="使用-result-类型来处理潜在的错误"><h3>使用 <code>Result</code> 类型来处理潜在的错误</h3></a>
|
||
<p>之前提到,<code>read_line</code> 将用户输入附加到传递给它字符串中,不过它也返回一个值——在这个例子中是 <a href="https://doc.rust-lang.org/std/io/type.Result.html"><code>io::Result</code></a><!-- ignore -->。Rust 标准库中有很多叫做 <code>Result</code> 的类型。一个 <a href="https://doc.rust-lang.org/std/result/enum.Result.html"><code>Result</code></a><!-- ignore --> 泛型以及对应子模块的特定版本,比如 <code>io::Result</code>。</p>
|
||
<p><code>Result</code> 类型是 <a href="ch06-00-enums.html"><em>枚举</em>(<em>enumerations</em>)</a><!-- ignore -->,通常也写作 <em>enums</em>。枚举类型持有固定集合的值,这些值被称为枚举的<strong>成员</strong>(<em>variants</em>)。第六章将介绍枚举的更多细节。</p>
|
||
<p>对于 <code>Result</code>,它的成员是 <code>Ok</code> 或 <code>Err</code>,<code>Ok</code> 表示操作成功,内部包含产生的值。<code>Err</code> 意味着操作失败,包含失败的前因后果。</p>
|
||
<p><code>Result</code> 类型的作用是编码错误处理信息。<code>Result</code> 类型的值,像其他类型一样,拥有定义于其上的方法。<code>io::Result</code> 的实例拥有<a href="https://doc.rust-lang.org/std/result/enum.Result.html#method.expect"><code>expect</code>方法</a><!-- ignore -->,如果实例的值是 <code>Err</code>,<code>expect</code> 会导致程序崩溃,并显示当做参数传递给 <code>expect</code> 的信息;如果实例的值是 <code>Ok</code>,<code>expect</code> 会获取 <code>Ok</code> 中的值并原样返回。在本例中,这个值是用户输入到标准输入中的字节的数量。</p>
|
||
<p>如果不使用 <code>expect</code>,程序也能编译,不过会出现一个警告:</p>
|
||
<pre><code>$ cargo build
|
||
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
src/main.rs:10:5: 10:39 warning: unused result which must be used,
|
||
#[warn(unused_must_use)] on by default
|
||
src/main.rs:10 io::stdin().read_line(&mut guess);
|
||
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
</code></pre>
|
||
<p>Rust 警告我们没有使用 <code>read_line</code> 的返回值 <code>Result</code>,说明有一个可能的错误没处理。想消除警告,就老实的写错误处理,不过我们就是希望程序在出现问题时立即崩溃,所以直接使用 <code>expect</code>。第九章会学习如何从错误中恢复。</p>
|
||
<a class="header" href="#使用-println-占位符打印值" name="使用-println-占位符打印值"><h3>使用 <code>println!</code> 占位符打印值</h3></a>
|
||
<p>除了位于结尾的大括号,目前为止就只有一行代码值得讨论一下了,就是这一行:</p>
|
||
<pre><code class="language-rust">println!("You guessed: {}", guess);
|
||
</code></pre>
|
||
<p>这行代码打印存储用户输入的字符串。第一个参数是格式化字符串,里面的 <code>{}</code> 是预留在特定位置的占位符。使用占位符也可以打印多个值:格式化字符串中第一个占位符对应第二个参数值,第二个占位符对应第三个参数值,以此类推(第一个参数是格式化字符串本身)。调用一次 <code>println!</code> 打印多个值看起来像这样:</p>
|
||
<pre><code class="language-rust">let x = 5;
|
||
let y = 10;
|
||
|
||
println!("x = {} and y = {}", x, y);
|
||
</code></pre>
|
||
<p>这行代码会打印出 <code>x = 5 and y = 10</code>。</p>
|
||
<a class="header" href="#测试第一部分代码" name="测试第一部分代码"><h3>测试第一部分代码</h3></a>
|
||
<p>让我们来测试下猜猜看游戏的第一部分。使用<code>cargo run</code>运行它:</p>
|
||
<pre><code class="language-sh">$ cargo run
|
||
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
Running `target/debug/guessing_game`
|
||
Guess the number!
|
||
Please input your guess.
|
||
6
|
||
You guessed: 6
|
||
</code></pre>
|
||
<p>至此为止,游戏的第一部分已经完成:我们从键盘获取输入并打印了出来。</p>
|
||
<a class="header" href="#生成一个秘密数字" name="生成一个秘密数字"><h2>生成一个秘密数字</h2></a>
|
||
<p>接下来,需要生成一个秘密数字,好让用户来猜。秘密数字应该每次都不同,这样重复玩才不会乏味;范围应该在 1 到 100 之间,这样才不会太困难。Rust 标准库中尚未包含随机数功能。然而,Rust 团队还是提供了一个 <a href="https://crates.io/crates/rand"><code>rand</code> crate</a>。</p>
|
||
<a class="header" href="#使用-crate-来增加更多功能" name="使用-crate-来增加更多功能"><h3>使用 crate 来增加更多功能</h3></a>
|
||
<p>记住 <em>crate</em> 是一个 Rust 代码的包。我们正在构建的项目是一个<strong>二进制 crate</strong>,它生成一个可执行文件。 <code>rand</code> crate 是一个 <strong>库 crate</strong>,库 crate 可以包含任意能被其他程序使用的代码。</p>
|
||
<p>Cargo 对外部 crate 的运用是亮点。在我们使用 <code>rand</code> 编写代码之前,需要编辑 <em>Cargo.toml</em> ,声明 <code>rand</code> 作为一个依赖。现在打开这个文件并在 <code>[dependencies]</code> 标题(Cargo 为你创建了它)之下添加:</p>
|
||
<p><span class="filename">Filename: Cargo.toml</span></p>
|
||
<pre><code class="language-toml">[dependencies]
|
||
|
||
rand = "0.3.14"
|
||
</code></pre>
|
||
<p>在 <em>Cargo.toml</em> 文件中,标题以及之后的内容属同一个段落,遇到下一个标题则开始新的段落。<code>[dependencies]</code> 部分告诉 Cargo 本项目依赖了哪些外部 crate 及其版本。本例中,我们使用语义化版本 <code>0.3.14</code> 来指定 <code>rand</code> crate。Cargo 理解<a href="http://semver.org">语义化版本(Semantic Versioning)</a><!-- ignore -->(有时也称为 <em>SemVer</em>),是一种定义版本号的标准。<code>0.3.14</code> 事实上是 <code>^0.3.14</code> 的简写,它表示“任何与 0.3.14 版本公有 API 相兼容的版本”。</p>
|
||
<p>现在,不修改任何代码,构建项目,如列表 2-2 所示:</p>
|
||
<pre><code>$ cargo build
|
||
Updating registry `https://github.com/rust-lang/crates.io-index`
|
||
Downloading rand v0.3.14
|
||
Downloading libc v0.2.14
|
||
Compiling libc v0.2.14
|
||
Compiling rand v0.3.14
|
||
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
</code></pre>
|
||
<p><span class="caption">Listing 2-2: The output from running <code>cargo build</code> after
|
||
adding the rand crate as a dependency</span></p>
|
||
<p>可能会出现不同的版本号(多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。</p>
|
||
<p>现在我们有了一个外部依赖,Cargo 从 <em>registry</em> (<a href="https://crates.io">Crates.io</a>)上获取了一份(兼容的)最新版本的代码。Crates.io 是 Rust 生态环境中的开发者们向他人贡献 Rust 开源项目的地方。</p>
|
||
<p>在更新完 registry (索引)后,Cargo 检查 <code>[dependencies]</code> 段落并下载缺失的部分。本例中,只声明了 <code>rand</code> 一个依赖,然而 Cargo 还是额外获取了 <code>libc</code>,因为 <code>rand</code> 依赖 <code>libc</code> 来正常工作。下载完成后,Rust 编译依赖,然后使用这些依赖编译项目。</p>
|
||
<p>如果不做任何修改,立刻再次运行 <code>cargo build</code>,则不会有任何输出。Cargo 知道它已经下载并编译了依赖,同时 <em>Cargo.toml</em> 文件也没有变动,并且代码也没有任何修改,所以它不会重新编译代码。因为无事可做,它简单的退出了。如果打开 <em>src/main.rs</em> 文件,做一些普通的修改,保存并再次构建,只会出现一行输出:</p>
|
||
<pre><code>$ cargo build
|
||
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
</code></pre>
|
||
<p>这一行表示 Cargo 只针对 <em>src/main.rs</em> 文件的微小修改而构建。依赖没有变化,所以 Cargo 会复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码。</p>
|
||
<a class="header" href="#cargolock-文件确保构建是可重现的" name="cargolock-文件确保构建是可重现的"><h4><em>Cargo.lock</em> 文件确保构建是可重现的</h4></a>
|
||
<p>Cargo 有一个机制来确保任何人在任何时候重新构建代码,都会产生相同的结果:Cargo 只会使用你指定的依赖的版本,除非你又手动指定了别的。例如,如果下周 <code>rand</code> crate 的 <code>v0.3.15</code> 版本出来了,它修复了一个重要的 bug,同时也含有一个缺陷,会破坏代码的运行,这时会发生什么呢?</p>
|
||
<p>答案是 <em>Cargo.lock</em> 文件。它在第一次运行 <code>cargo build</code> 时创建,并放在 <em>guessing_game</em> 目录,Cargo 计算出所有符合要求的依赖版本并写入 <em>Cargo.lock</em> 文件。当将来构建项目时,如果 <em>Cargo.lock</em> 存在,Cargo 就使用里面指定的版本,不会重新计算。自动使你拥有了一个可重现的构建。换句话说,项目会继续使用 <code>0.3.14</code> 直到你显式升级,感谢 <em>Cargo.lock</em>。</p>
|
||
<a class="header" href="#更新-crate-到一个新版本" name="更新-crate-到一个新版本"><h4>更新 crate 到一个新版本</h4></a>
|
||
<p>当你<strong>确实</strong>需要升级 crate 时,Cargo 提供了另一个命令,<code>update</code>,他会:</p>
|
||
<ol>
|
||
<li>忽略 <em>Cargo.lock</em> 文件,并计算出所有符合 <em>Cargo.toml</em> 声明的最新版本。</li>
|
||
<li>如果成功了,Cargo 会把这些版本写入 <em>Cargo.lock</em> 文件。</li>
|
||
</ol>
|
||
<p>不过,Cargo 默认只会寻找大于 <code>0.3.0</code> 而小于 <code>0.4.0</code> 的版本。如果 <code>rand</code> crate 发布了两个新版本,<code>0.3.15</code> 和 <code>0.4.0</code>,在运行 <code>cargo update</code> 时会出现如下内容:</p>
|
||
<pre><code>$ cargo update
|
||
Updating registry `https://github.com/rust-lang/crates.io-index`
|
||
Updating rand v0.3.14 -> v0.3.15
|
||
</code></pre>
|
||
<p>这时,值得注意的是 <em>Cargo.lock</em> 文件中的一个改变,<code>rand</code> crate 现在使用的版本是<code>0.3.15</code>。</p>
|
||
<p>如果想要使用 <code>0.4.0</code> 版本的 <code>rand</code> 或是任何 <code>0.4.x</code> 系列的版本,必须像这样更新 <em>Cargo.toml</em> 文件:</p>
|
||
<pre><code class="language-toml">[dependencies]
|
||
|
||
rand = "0.4.0"
|
||
</code></pre>
|
||
<p>下一次运行 <code>cargo build</code> 时,Cargo 会从 registry 更新,并根据你指定的新版本重新计算。</p>
|
||
<p>第十四章会讲到 <a href="http://doc.crates.io">Cargo</a><!-- ignore --> 及其<a href="http://doc.crates.io/crates-io.html">生态系统</a><!-- ignore -->的更多内容,不过目前你只需要了解这么多。通过 Cargo 复用库文件非常容易,因此 Rustacean 能够编写出由很多包组装而成的更轻巧的项目。</p>
|
||
<a class="header" href="#生成一个随机数" name="生成一个随机数"><h3>生成一个随机数</h3></a>
|
||
<p>让我们开始<strong>使用</strong> <code>rand</code>。下一步是更新 <em>src/main.rs</em>,如列表 2-3 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">extern crate rand;
|
||
|
||
use std::io;
|
||
use rand::Rng;
|
||
|
||
fn main() {
|
||
println!("Guess the number!");
|
||
|
||
let secret_number = rand::thread_rng().gen_range(1, 101);
|
||
|
||
println!("The secret number is: {}", secret_number);
|
||
|
||
println!("Please input your guess.");
|
||
|
||
let mut guess = String::new();
|
||
|
||
io::stdin().read_line(&mut guess)
|
||
.expect("Failed to read line");
|
||
|
||
println!("You guessed: {}", guess);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 2-3: Code changes needed in order to generate a
|
||
random number</span></p>
|
||
<p>我们在顶部增加一行 <code>extern crate rand;</code> 通知 Rust 我们要使用外部依赖。这也会调用相应的 <code>use rand</code>,所以现在可以使用 <code>rand::</code> 前缀来调用 <code>rand</code> 中的内容。</p>
|
||
<p>接下来,我们增加了一行 <code>use</code>:<code>use rand::Rng</code>。<code>Rng</code> 是一个 trait,它定义了随机数生成器应实现的方法 ,想使用这些方法的话此 trait 必须在作用域中。第十章会详细介绍 trait。</p>
|
||
<p>另外,中间还新增加了两行。<code>rand::thread_rng</code> 函数提供实际使用的随机数生成器:它位于当前执行线程,并从操作系统获取 seed。接下来,调用随机数生成器的 <code>gen_range</code> 方法。这个方法由刚才引入到作用域的 <code>Rng</code> trait 定义。<code>gen_range</code> 方法获取两个数字作为参数,并生成一个范围在两者之间的随机数。它包含下限但不包含上限,所以需要指定<code>1</code>和<code>101</code>来请求一个<code>1</code>和<code>100</code>之间的数。</p>
|
||
<p><strong>知道</strong> use 哪个 trait 和该从 crate 中调用哪个方法并不是全部,crate 的说明位于其文档中,Cargo 有一个很棒的功能是:运行 <code>cargo doc --open</code> 命令来构建所有本地依赖提供的文档,并在浏览器中打开。例如,假设你对 <code>rand</code> crate 中的其他功能感兴趣,<code>cargo doc --open</code> 并点击左侧导航栏中的 <code>rand</code>。</p>
|
||
<p>新增加的第二行代码打印出了秘密数字。这在开发程序时很有用,因为我们可以去测试它,不过在最终版本我们会删掉它。游戏一开始就打印出结果就没什么可玩的了!</p>
|
||
<p>尝试运行程序几次:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
Running `target/debug/guessing_game`
|
||
Guess the number!
|
||
The secret number is: 7
|
||
Please input your guess.
|
||
4
|
||
You guessed: 4
|
||
$ cargo run
|
||
Running `target/debug/guessing_game`
|
||
Guess the number!
|
||
The secret number is: 83
|
||
Please input your guess.
|
||
5
|
||
You guessed: 5
|
||
</code></pre>
|
||
<p>你应该能得到不同的随机数,同时他们应该都是在 1 和 100 之间的。干得漂亮!</p>
|
||
<a class="header" href="#比较猜测与秘密数字" name="比较猜测与秘密数字"><h2>比较猜测与秘密数字</h2></a>
|
||
<p>现在有了用户输入和一个随机数,我们可以比较他们。这个步骤如列表 2-4 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">extern crate rand;
|
||
|
||
use std::io;
|
||
use std::cmp::Ordering;
|
||
use rand::Rng;
|
||
|
||
fn main() {
|
||
println!("Guess the number!");
|
||
|
||
let secret_number = rand::thread_rng().gen_range(1, 101);
|
||
|
||
println!("The secret number is: {}", secret_number);
|
||
|
||
println!("Please input your guess.");
|
||
|
||
let mut guess = String::new();
|
||
|
||
io::stdin().read_line(&mut guess)
|
||
.expect("Failed to read line");
|
||
|
||
println!("You guessed: {}", guess);
|
||
|
||
match guess.cmp(&secret_number) {
|
||
Ordering::Less => println!("Too small!"),
|
||
Ordering::Greater => println!("Too big!"),
|
||
Ordering::Equal => println!("You win!"),
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 2-4: Handling the possible return values of
|
||
comparing two numbers</span></p>
|
||
<p>新代码的第一行是另一个 <code>use</code>,从标准库引入了一个叫做 <code>std::cmp::Ordering</code> 的类型。<code>Ordering</code> 是一个像 <code>Result</code> 一样的枚举,不过它的成员是 <code>Less</code>、<code>Greater</code> 和 <code>Equal</code>。这是你做比较时可能出现的三种结果。</p>
|
||
<p>接着,底部的五行新代码使用了 <code>Ordering</code> 类型:</p>
|
||
<pre><code class="language-rust">match guess.cmp(&secret_number) {
|
||
Ordering::Less => println!("Too small!"),
|
||
Ordering::Greater => println!("Too big!"),
|
||
Ordering::Equal => println!("You win!"),
|
||
}
|
||
</code></pre>
|
||
<p><code>cmp</code> 方法用来比较两个值。在任何可比较的值上调用,然后获取另一个被比较值的引用:这里是把 <code>guess</code> 与 <code>secret_number</code> 做比较,返回一个 <code>Ordering</code> 枚举的成员。再使用一个 <a href="ch06-02-match.html"><code>match</code></a><!-- ignore --> 表达式,根据枚举成员来决定接下来干什么。</p>
|
||
<p>一个 <code>match</code> 表达式由 <strong>分支(arms)</strong> 构成。一个分支包含一个 <strong>模式</strong>(<em>pattern</em>)和动作,表达式头的求值结果符合分支的模式时将执行对应的动作。Rust 获取提供给 <code>match</code> 的值并挨个检查每个分支的模式。<code>match</code> 结构和模式是 Rust 的强大功能,它体现了代码可能遇到的多种情形,并帮助你没有遗漏的处理。这些功能将分别在第六章和第十八章详细介绍。</p>
|
||
<p>让我们看看使用 <code>match</code> 表达式的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。比较 50 与 38 时,因为 50 比 38 要大,<code>cmp</code> 方法会返回 <code>Ordering::Greater</code>。<code>match</code> 表达式得到该值,然后检查第一个分支的模式,<code>Ordering::Less</code> 与 <code>Ordering::Greater</code>并不匹配,所以它忽略了这个分支的动作并来到下一个分支。下一个分支的模式是 <code>Ordering::Greater</code>,<strong>正确</strong>匹配!这个分支关联的动作被执行,在屏幕打印出 <code>Too big!</code>。<code>match</code> 表达式就此终止,因为该场景下没有检查最后一个分支的必要。</p>
|
||
<p>然而,列表 2-4 的代码并不能编译,可以尝试一下:</p>
|
||
<pre><code>$ cargo build
|
||
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
error[E0308]: mismatched types
|
||
--> src/main.rs:23:21
|
||
|
|
||
23 | match guess.cmp(&secret_number) {
|
||
| ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integral variable
|
||
|
|
||
= note: expected type `&std::string::String`
|
||
= note: found type `&{integer}`
|
||
|
||
error: aborting due to previous error
|
||
Could not compile `guessing_game`.
|
||
</code></pre>
|
||
<p>错误的核心表明这里有<strong>不匹配的类型</strong>(<em>mismatched types</em>)。Rust 有一个静态强类型系统,同时也有类型推断。当我们写出 <code>let guess = String::new()</code> 时,Rust 推断出 <code>guess</code> 应该是一个<code>String</code>,不需要我们写出类型。另一方面,<code>secret_number</code>,是一个数字类型。多种数字类型拥有 1 到 100 之间的值:32 位数字 <code>i32</code>;32 位无符号数字 <code>u32</code>;64 位数字 <code>i64</code> 等等。Rust 默认使用 <code>i32</code>,所以它是 <code>secret_number</code> 的类型,除非增加类型信息,或任何能让 Rust 推断出不同数值类型的信息。这里错误的原因在于 Rust 不会比较字符串类型和数字类型。</p>
|
||
<p>所以我们必须把从输入中读取到的 <code>String</code> 转换为一个真正的数字类型,才好与秘密数字进行比较。可以通过在 <code>main</code> 函数体中增加如下两行代码来实现:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">extern crate rand;
|
||
|
||
use std::io;
|
||
use std::cmp::Ordering;
|
||
use rand::Rng;
|
||
|
||
fn main() {
|
||
println!("Guess the number!");
|
||
|
||
let secret_number = rand::thread_rng().gen_range(1, 101);
|
||
|
||
println!("The secret number is: {}", secret_number);
|
||
|
||
println!("Please input your guess.");
|
||
|
||
let mut guess = String::new();
|
||
|
||
io::stdin().read_line(&mut guess)
|
||
.expect("Failed to read line");
|
||
|
||
let guess: u32 = guess.trim().parse()
|
||
.expect("Please type a number!");
|
||
|
||
println!("You guessed: {}", guess);
|
||
|
||
match guess.cmp(&secret_number) {
|
||
Ordering::Less => println!("Too small!"),
|
||
Ordering::Greater => println!("Too big!"),
|
||
Ordering::Equal => println!("You win!"),
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>这两行代码是:</p>
|
||
<pre><code class="language-rust">let guess: u32 = guess.trim().parse()
|
||
.expect("Please type a number!");
|
||
</code></pre>
|
||
<p>这里创建了一个叫做 <code>guess</code> 的变量。不过等等,不是已经有了一个叫做<code>guess</code>的变量了吗?确实如此,不过 Rust 允许<strong>遮盖</strong>(<em>shadow</em>),用一个新值来遮盖 <code>guess</code> 之前的值。这个功能常用在需要转换值类型之类的场景,它允许我们复用 <code>guess</code> 变量的名字,而不是被迫创建两个不同变量,诸如 <code>guess_str</code> 和 <code>guess</code> 之类。(第三章会介绍 shadowing 的更多细节。)</p>
|
||
<p><code>guess</code> 被绑定到 <code>guess.trim().parse()</code> 表达式。表达式中的 <code>guess</code> 是包含输入的 <code>String</code> 类型。<code>String</code> 实例的 <code>trim</code> 方法会去除字符串开头和结尾的空白。<code>u32</code> 只能由数字字符转换,不过用户必须输入回车键才能让 <code>read_line</code> 返回,然而用户按下回车键时,会在字符串中增加一个换行(newline)符。例如,用户输入 5 并回车,<code>guess</code> 看起来像这样:<code>5\n</code>。<code>\n</code> 代表“换行”,回车键。<code>trim</code> 方法消除 <code>\n</code>,只留下<code>5</code>。</p>
|
||
<p><a href="https://doc.rust-lang.org/std/primitive.str.html#method.parse">字符串的<code>parse</code>方法</a><!-- ignore --> 将字符串解析成数字。这个方法可以解析多种数字类型,因此需要告诉 Rust 具体的数字类型,这里通过 <code>let guess: u32</code> 指定。<code>guess</code> 后面的冒号(<code>:</code>)告诉 Rust 我们指定了变量的类型。Rust 有一些内建的数字类型;<code>u32</code> 是一个无符号的 32 位整型。对于不大的正整数来说,它是不错的类型,第三章还会讲到其他数字类型。另外,程序中的 <code>u32</code> 注解以及与 <code>secret_number</code> 的比较,意味着 Rust 会推断出 <code>secret_number</code> 也是 <code>u32</code> 类型。现在可以使用相同类型比较两个值了!</p>
|
||
<p><code>parse</code> 调用可能产生错误。例如,字符串中包含 <code>A👍%</code>,就无法将其转换为一个数字。因此,<code>parse</code> 方法返回一个 <code>Result</code> 类型。像之前讨论的 <code>read_line</code> 方法,按部就班的用 <code>expect</code> 方法处理即可。如果 <code>parse</code> 不能从字符串生成一个数字,返回一个 <code>Result::Err</code> 时,<code>expect</code> 会使游戏崩溃并打印附带的信息。如果 <code>parse</code> 成功地将字符串转换为一个数字,它会返回 <code>Result::Ok</code>,然后 <code>expect</code> 会返回 <code>Ok</code> 中的数字。</p>
|
||
<p>现在让我们运行程序!</p>
|
||
<pre><code>$ cargo run
|
||
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
Running `target/guessing_game`
|
||
Guess the number!
|
||
The secret number is: 58
|
||
Please input your guess.
|
||
76
|
||
You guessed: 76
|
||
Too big!
|
||
</code></pre>
|
||
<p>漂亮!即便是在猜测之前添加了空格,程序依然能判断出用户猜测了 76。多运行程序几次来检验不同类型输入的相应行为:猜一个正确的数字,猜一个过大的数字和猜一个过小的数字。</p>
|
||
<p>现在游戏已经大体上能玩了,不过用户只能猜一次。增加一个循环来改变它吧!</p>
|
||
<a class="header" href="#使用循环来允许多次猜测" name="使用循环来允许多次猜测"><h2>使用循环来允许多次猜测</h2></a>
|
||
<p><code>loop</code> 关键字提供了一个无限循环。将其加入后,用户可以反复猜测:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">extern crate rand;
|
||
|
||
use std::io;
|
||
use std::cmp::Ordering;
|
||
use rand::Rng;
|
||
|
||
fn main() {
|
||
println!("Guess the number!");
|
||
|
||
let secret_number = rand::thread_rng().gen_range(1, 101);
|
||
|
||
println!("The secret number is: {}", secret_number);
|
||
|
||
loop {
|
||
println!("Please input your guess.");
|
||
|
||
let mut guess = String::new();
|
||
|
||
io::stdin().read_line(&mut guess)
|
||
.expect("Failed to read line");
|
||
|
||
let guess: u32 = guess.trim().parse()
|
||
.expect("Please type a number!");
|
||
|
||
println!("You guessed: {}", guess);
|
||
|
||
match guess.cmp(&secret_number) {
|
||
Ordering::Less => println!("Too small!"),
|
||
Ordering::Greater => println!("Too big!"),
|
||
Ordering::Equal => println!("You win!"),
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保这些代码额外缩进了一层,再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们的要求:永远地请求另一个猜测,用户没法退出!</p>
|
||
<p>用户总能使用 <code>Ctrl-C</code> 终止程序。不过还有另一个方法跳出无限循环,就是“比较猜测”部分提到的 <code>parse</code>:如果用户输入一个非数字答案,程序会崩溃。用户可以利用这一点来退出,如下所示:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
Running `target/guessing_game`
|
||
Guess the number!
|
||
The secret number is: 59
|
||
Please input your guess.
|
||
45
|
||
You guessed: 45
|
||
Too small!
|
||
Please input your guess.
|
||
60
|
||
You guessed: 60
|
||
Too big!
|
||
Please input your guess.
|
||
59
|
||
You guessed: 59
|
||
You win!
|
||
Please input your guess.
|
||
quit
|
||
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:785
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
error: Process didn't exit successfully: `target/debug/guess` (exit code: 101)
|
||
</code></pre>
|
||
<p>输入 <code>quit</code> 确实退出了程序,同时其他任何非数字输入也一样。然而,这并不理想,我们想要当猜测正确的数字时游戏能自动退出。</p>
|
||
<a class="header" href="#猜测正确后退出" name="猜测正确后退出"><h3>猜测正确后退出</h3></a>
|
||
<p>让我们增加一个 <code>break</code>,在用户猜对时退出游戏:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">extern crate rand;
|
||
|
||
use std::io;
|
||
use std::cmp::Ordering;
|
||
use rand::Rng;
|
||
|
||
fn main() {
|
||
println!("Guess the number!");
|
||
|
||
let secret_number = rand::thread_rng().gen_range(1, 101);
|
||
|
||
println!("The secret number is: {}", secret_number);
|
||
|
||
loop {
|
||
println!("Please input your guess.");
|
||
|
||
let mut guess = String::new();
|
||
|
||
io::stdin().read_line(&mut guess)
|
||
.expect("Failed to read line");
|
||
|
||
let guess: u32 = guess.trim().parse()
|
||
.expect("Please type a number!");
|
||
|
||
println!("You guessed: {}", guess);
|
||
|
||
match guess.cmp(&secret_number) {
|
||
Ordering::Less => println!("Too small!"),
|
||
Ordering::Greater => println!("Too big!"),
|
||
Ordering::Equal => {
|
||
println!("You win!");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>通过在 <code>You win!</code> 之后增加一行 <code>break</code>,用户猜对了神秘数字后会退出循环。退出循环也意味着退出程序,因为循环是 <code>main</code> 的最后一部分。</p>
|
||
<a class="header" href="#处理无效输入" name="处理无效输入"><h3>处理无效输入</h3></a>
|
||
<p>为了进一步改善游戏性,不要在用户输入非数字时崩溃,需要忽略非数字,让用户可以继续猜测。可以通过修改 <code>guess</code> 将 <code>String</code> 转化为 <code>u32</code> 那部分代码来实现:</p>
|
||
<pre><code class="language-rust">let guess: u32 = match guess.trim().parse() {
|
||
Ok(num) => num,
|
||
Err(_) => continue,
|
||
};
|
||
</code></pre>
|
||
<p>将 <code>expect</code> 调用换成 <code>match</code> 语句,是从“立即崩溃”转到真正处理错误的惯用方法。须知 <code>parse</code> 返回一个 <code>Result</code> 类型,而 <code>Result</code> 是一个拥有 <code>Ok</code> 或 <code>Err</code> 成员的枚举。这里使用的 <code>match</code> 表达式,和之前处理 <code>cmp</code> 方法返回 <code>Ordering</code> 时用的一样。</p>
|
||
<p>如果 <code>parse</code> 能够成功的将字符串转换为一个数字,它会返回一个包含结果数字的 <code>Ok</code>。这个 <code>Ok</code> 值与<code>match</code> 第一个分支的模式相匹配,该分支对应的动作返回 <code>Ok</code> 值中的数字 <code>num</code>,最后如愿变成新创建的 <code>guess</code> 变量。</p>
|
||
<p>如果 <code>parse</code> <em>不</em>能将字符串转换为一个数字,它会返回一个包含更多错误信息的 <code>Err</code>。<code>Err</code> 值不能匹配第一个 <code>match</code> 分支的 <code>Ok(num)</code> 模式,但是会匹配第二个分支的 <code>Err(_)</code> 模式:<code>_</code> 是一个兜底值,用来匹配所有 <code>Err</code> 值,不管其中有何种信息。所以程序会执行第二个分支的动作,<code>continue</code> 意味着进入 <code>loop</code> 的下一次循环,请求另一个猜测。这样程序就忽略了 <code>parse</code> 可能遇到的所有错误!</p>
|
||
<p>现在万事俱备,只需运行 <code>cargo run</code>:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
Running `target/guessing_game`
|
||
Guess the number!
|
||
The secret number is: 61
|
||
Please input your guess.
|
||
10
|
||
You guessed: 10
|
||
Too small!
|
||
Please input your guess.
|
||
99
|
||
You guessed: 99
|
||
Too big!
|
||
Please input your guess.
|
||
foo
|
||
Please input your guess.
|
||
61
|
||
You guessed: 61
|
||
You win!
|
||
</code></pre>
|
||
<p>太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。在测试时还好,但正式发布时会毁了游戏。删掉打印秘密数字的 <code>println!</code>。列表 2-5 为最终代码:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">extern crate rand;
|
||
|
||
use std::io;
|
||
use std::cmp::Ordering;
|
||
use rand::Rng;
|
||
|
||
fn main() {
|
||
println!("Guess the number!");
|
||
|
||
let secret_number = rand::thread_rng().gen_range(1, 101);
|
||
|
||
loop {
|
||
println!("Please input your guess.");
|
||
|
||
let mut guess = String::new();
|
||
|
||
io::stdin().read_line(&mut guess)
|
||
.expect("Failed to read line");
|
||
|
||
let guess: u32 = match guess.trim().parse() {
|
||
Ok(num) => num,
|
||
Err(_) => continue,
|
||
};
|
||
|
||
println!("You guessed: {}", guess);
|
||
|
||
match guess.cmp(&secret_number) {
|
||
Ordering::Less => println!("Too small!"),
|
||
Ordering::Greater => println!("Too big!"),
|
||
Ordering::Equal => {
|
||
println!("You win!");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 2-5: Complete code of the guessing game</span></p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>此时此刻,你顺利完成了猜猜看游戏!恭喜!</p>
|
||
<p>这是一个通过动手实践学习 Rust 新概念的项目:<code>let</code>、<code>match</code>、方法、关联函数、使用外部 crate 等等,接下来的几章,我们将会继续深入。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法的语法,而第六章侧重解释枚举。</p>
|
||
<a class="header" href="#通用编程概念" name="通用编程概念"><h1>通用编程概念</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-00-common-programming-concepts.md">ch03-00-common-programming-concepts.md</a>
|
||
<br>
|
||
commit 04aa3a45eb72855b34213703718f50a12a3eeec8</p>
|
||
</blockquote>
|
||
<p>本章涉及一些几乎所有编程语言都有的概念,以及他们在 Rust 中是如何工作的。很多编程语言的核心概念都是共通的,本章中展示的概念都不是 Rust 特有的,不过我们会在 Rust 环境中讨论他们,解释他们的使用习惯。</p>
|
||
<p>具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你拥有坚实的起步。</p>
|
||
<blockquote>
|
||
<a class="header" href="#关键字" name="关键字"><h3>关键字</h3></a>
|
||
<p>Rust 语言有一系列保留的<strong>关键字</strong>(<em>keywords</em>),只能由语言本身使用,像大部分语言一样。你不能使用这些关键字作为变量或函数的名称,大部分关键字有特殊的意义,并被用来完成 Rust 程序中的各种任务;一些关键字目前没有分配,是为将来可能添加的功能保留的。可以在附录 A 中找到关键字的列表。</p>
|
||
</blockquote>
|
||
<a class="header" href="#变量和可变性" name="变量和可变性"><h2>变量和可变性</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-01-variables-and-mutability.md">ch03-01-variables-and-mutability.md</a>
|
||
<br>
|
||
commit 04aa3a45eb72855b34213703718f50a12a3eeec8</p>
|
||
</blockquote>
|
||
<p>第二章中提到过,变量默认是<strong>不可变</strong>(<em>immutable</em>)的。这是利用 Rust 安全和简单并发的优势编写代码一大助力。不过,变量仍然有可变的选项。让我们探讨一下,拥抱不可变性的原因及方法,以及何时你不想拥抱。</p>
|
||
<p>当变量不可变时,意味着一旦值被绑定上一个名称,你就不能改变这个值。作为说明,通过<code>cargo new --bin variables</code>在 <em>projects</em> 目录生成一个叫做 <em>variables</em> 的新项目。</p>
|
||
<p>接着,在新建的 <em>variables</em> 目录,打开 <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 x = 5;
|
||
println!("The value of x is: {}", x);
|
||
x = 6;
|
||
println!("The value of x is: {}", x);
|
||
}
|
||
</code></pre>
|
||
<p>保存并使用<code>cargo run</code>运行程序。应该会看到一个错误信息,如下输出所示:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling variables v0.0.1 (file:///projects/variables)
|
||
error[E0384]: re-assignment of immutable variable `x`
|
||
--> src/main.rs:4:5
|
||
|
|
||
2 | let x = 5;
|
||
| - first assignment to `x`
|
||
3 | println!("The value of x is: {}", x);
|
||
4 | x = 6;
|
||
| ^^^^^ re-assignment of immutable variable
|
||
</code></pre>
|
||
<p>这个例子展示了编译器如何帮助你找出程序中的错误。即便编译错误令人沮丧,那也不过是说程序不能安全的完成你想让它完成的工作;而<strong>不能</strong>说明你是不是一个好程序员!有经验的 Rustacean 们一样会遇到编译错误。这些错误给出的原因是<code>对不可变变量重新赋值</code>(<code>re-assignment of immutable variable</code>),因为我们尝试对不可变变量<code>x</code>赋第二个值。</p>
|
||
<p>尝试去改变预设为不可变的值,产生编译错误是很重要的,因为这种情况可能导致 bug:如果代码的一部分假设一个值永远也不会改变,而另一部分代码改变了它,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 难以跟踪,尤其是第二部分代码只是<strong>有时</strong>改变其值。</p>
|
||
<p>Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要厘清如何以及哪里可能会被改变,从而使得代码易于推导。</p>
|
||
<p>不过可变性也是非常有用的。变量只是默认不可变;可以通过在变量名之前加 <code>mut</code> 来使其可变。它向读者表明了其他代码将会改变这个变量的意图。</p>
|
||
<p>例如,改变 <em>src/main.rs</em> 并替换其代码为如下:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let mut x = 5;
|
||
println!("The value of x is: {}", x);
|
||
x = 6;
|
||
println!("The value of x is: {}", x);
|
||
}
|
||
</code></pre>
|
||
<p>当运行这个程序,出现如下:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling variables v0.1.0 (file:///projects/variables)
|
||
Running `target/debug/variables`
|
||
The value of x is: 5
|
||
The value of x is: 6
|
||
</code></pre>
|
||
<p>通过 <code>mut</code>,允许把绑定到 <code>x</code> 的值从 <code>5</code> 改成 <code>6</code>。在一些情况下,你会想要一个变量可变,因为相对不可变的风格更容易写。</p>
|
||
<p>除了避免 bug 外,还有多处需要权衡取舍。例如,使用大型数据结构时,适当地使变量可变,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的风格编程,可能会使代码更易理解,为可读性而遭受性能惩罚或许值得。</p>
|
||
<a class="header" href="#变量和常量的区别" name="变量和常量的区别"><h3>变量和常量的区别</h3></a>
|
||
<p>不允许改变值的变量,可能会使你想起另一个大部分编程语言都有的概念:<strong>常量</strong>(<em>constants</em>)。类似于不可变变量,常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。</p>
|
||
<p>首先,不允许对常量使用 <code>mut</code>:常量不光默认不能变,它总是不能变。</p>
|
||
<p>声明常量使用 <code>const</code> 关键字而不是 <code>let</code>,而且<em>必须</em>注明值的类型。在下一部分,“数据类型”,涉及到类型和类型注解,现在无需关心这些细节,记住总是标注类型即可。</p>
|
||
<p>常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。</p>
|
||
<p>最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果,或任何其他只在运行时计算的值。</p>
|
||
<p>这是一个常量声明的例子,它的名称是 <code>MAX_POINTS</code>,值是 100,000。(常量使用下划线分隔的大写字母命名):</p>
|
||
<pre><code class="language-rust">const MAX_POINTS: u32 = 100_000;
|
||
</code></pre>
|
||
<p>常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以作为多处代码使用的全局范围的值,例如一个游戏中所有玩家可以获取的最高分或者光速。</p>
|
||
<p>将作用于整个程序的值,由硬编码改为常量(并编写文档),对后来的维护者了解值的意义很用帮助。它也能将硬编码的值汇总一处,为将来可能的修改提供方便。</p>
|
||
<a class="header" href="#遮盖shadowing" name="遮盖shadowing"><h3>遮盖(Shadowing)</h3></a>
|
||
<p>如第二章“猜猜看游戏”所讲的,我们可以定义一个与之前变量重名的新变量,而新变量会<strong>遮盖</strong>之前的变量。Rustacean 称之为“第一个变量被第二个<strong>遮盖</strong>了”,这意味着使用这个变量时会看第二个值。可以用相同变量名称来遮盖它自己,以及重复使用 <code>let</code> 关键字来多次遮盖,如下所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let x = 5;
|
||
|
||
let x = x + 1;
|
||
|
||
let x = x * 2;
|
||
|
||
println!("The value of x is: {}", x);
|
||
}
|
||
</code></pre>
|
||
<p>这个程序首先将 <code>x</code> 绑定到值 <code>5</code> 上。接着通过 <code>let x =</code> 遮盖 <code>x</code>,获取原始值并加 <code>1</code> 这样 <code>x</code> 的值就变成 <code>6</code> 了。第三个 <code>let</code> 语句也覆盖了 <code>x</code>,获取之前的值并乘以 <code>2</code>,<code>x</code> 最终的值是 <code>12</code>。运行这个程序,它会有如下输出:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling variables v0.1.0 (file:///projects/variables)
|
||
Running `target/debug/variables`
|
||
The value of x is: 12
|
||
</code></pre>
|
||
<p>这与将变量声明为 <code>mut</code> 是有区别的。因为除非再次使用 <code>let</code> 关键字,不小心尝试对变量重新赋值会导致编译时错误。我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。</p>
|
||
<p><code>mut</code> 与遮盖的另一个区别是,当再次使用 <code>let</code> 时,实际上创建了一个新变量,我们可以改变值的类型。例如,假设程序请求用户输入空格来提供文本间隔,然而我们真正需要的是将输入存储成数字(多少个空格):</p>
|
||
<pre><code class="language-rust">let spaces = " ";
|
||
let spaces = spaces.len();
|
||
</code></pre>
|
||
<p>这里允许第一个 <code>spaces</code> 变量是字符串类型,而第二个 <code>spaces</code> 变量,它是一个恰巧与第一个变量同名的崭新变量,是数字类型。遮盖使我们不必使用不同的名字,如 <code>spaces_str</code> 和 <code>spaces_num</code>;相反,我们可以复用 <code>spaces</code> 这个更简单的名字。然而,如果尝试使用<code>mut</code>,如下所示:</p>
|
||
<pre><code class="language-rust,ignore">let mut spaces = " ";
|
||
spaces = spaces.len();
|
||
</code></pre>
|
||
<p>会导致一个编译错误,因为改变一个变量的类型是不被允许的:</p>
|
||
<pre><code>error[E0308]: mismatched types
|
||
--> src/main.rs:3:14
|
||
|
|
||
3 | spaces = spaces.len();
|
||
| ^^^^^^^^^^^^ expected &str, found usize
|
||
|
|
||
= note: expected type `&str`
|
||
= note: found type `usize`
|
||
</code></pre>
|
||
<p>现在我们探索了变量如何工作,让我们看看更多的数据类型。</p>
|
||
<a class="header" href="#数据类型" name="数据类型"><h2>数据类型</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-02-data-types.md">ch03-02-data-types.md</a>
|
||
<br>
|
||
commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32</p>
|
||
</blockquote>
|
||
<p>在 Rust 中,任何值都属于一种明确的<strong>类型</strong>(<em>type</em>),声明它被指定了何种数据,以便明确其处理方式。我们将分两部分探讨一些内建类型:标量(scalar)和复合(compound)。</p>
|
||
<p>Rust 是<strong>静态类型</strong>(<em>statically typed</em>)语言,也就是说在编译时就需要知道所有变量的类型,这一认知将贯穿整个章节,请在头脑中明确。通过值的形式及其使用方式,编译器通常可以推断出我们想要用的类型。多种类型均有可能时,比如第二章中使用 <code>parse</code> 将 <code>String</code> 转换为数字,必须增加类型注解,像这样:</p>
|
||
<pre><code class="language-rust">let guess: u32 = "42".parse().expect("Not a number!");
|
||
</code></pre>
|
||
<p>如果不添加类型注解,Rust 会显示如下错误。这说明编译器需要更多信息,来了解我们想要的类型:</p>
|
||
<pre><code>error[E0282]: unable to infer enough type information about `_`
|
||
--> src/main.rs:2:9
|
||
|
|
||
2 | let guess = "42".parse().expect("Not a number!");
|
||
| ^^^^^ cannot infer type for `_`
|
||
|
|
||
= note: type annotations or generic parameter binding required
|
||
</code></pre>
|
||
<p>在我们讨论各种数据类型时,你会看到多样的类型注解。</p>
|
||
<a class="header" href="#标量类型" name="标量类型"><h3>标量类型</h3></a>
|
||
<p><strong>标量</strong>类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过他们,不过让我们深入了解他们在 Rust 中时如何工作的。</p>
|
||
<a class="header" href="#整型" name="整型"><h4>整型</h4></a>
|
||
<p><strong>整数</strong>是一个没有小数部分的数字。我们在这一章的前面使用过 <code>i32</code> 类型。该类型声明指示,i32 关联的值应该是一个占据32比特位的有符号整数(因为这个<code>i</code>,与<code>u</code>代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一种变体的有符号和无符号列(例如,<em>i32</em>)可以用来声明对应的整数值。</p>
|
||
<p><span class="caption">Table 3-1: Integer Types in Rust</span></p>
|
||
<table><thead><tr><td> Length </td><td> Signed </td><td> Unsigned </td></tr></thead>
|
||
<tr><td> 8-bit </td><td> i8 </td><td> u8 </td></tr>
|
||
<tr><td> 16-bit </td><td> i16 </td><td> u16 </td></tr>
|
||
<tr><td> 32-bit </td><td> i32 </td><td> u32 </td></tr>
|
||
<tr><td> 64-bit </td><td> i64 </td><td> u64 </td></tr>
|
||
<tr><td> arch </td><td> isize </td><td> usize </td></tr>
|
||
</table>
|
||
<p>每一种变体都可以是有符号或无符号的,并有一个明确的大小。有符号和无符号代表数字能否为负值;换句话说,数字是否需要有一个符号(有符号数),或者永远为正而不需要符号(无符号数)。这有点像在纸上书写数字:当需要考虑符号的时候,数字以加号或减号作为前缀;然而,可以安全地假设为正数时,加号前缀通常省略。有符号数以二进制补码形式(two’s complement representation)存储(如果你不清楚这是什么,可以在网上搜索;对其的解释超出了本书的范畴)。</p>
|
||
<p>每一个有符号的变体可以储存包含从 -(2<sup>n - 1</sup>) 到 2<sup>n - 1</sup> - 1 在内的数字,这里 <code>n</code> 是变体使用的位数。所以 <code>i8</code> 可以储存从 -(2<sup>7</sup>) 到 2<sup>7</sup> - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2<sup>n</sup> - 1 的数字,所以 <code>u8</code> 可以储存从 0 到 2<sup>8</sup> - 1 的数字,也就是从 0 到 255。</p>
|
||
<p>另外,<code>isize</code> 和 <code>usize</code> 类型依赖运行程序的计算机架构:64 位架构上他们是 64 位的, 32 位架构上他们是 32 位的。</p>
|
||
<p>可以使用表格 3-2 中的任何一种形式编写数字字面值。除字节以外的其它字面值允许使用类型后缀,例如 <code>57u8</code>,允许使用 <code>_</code> 做为分隔符以方便读数,例如 <code>1_000</code> (分隔符的数量与位置并不影响实际的数字)。</p>
|
||
<p><span class="caption">Table 3-2: Integer Literals in Rust</span></p>
|
||
<table><thead><tr><td> Number literals </td><td> Example </td></tr></thead>
|
||
<tr><td> Decimal </td><td> <code>98_222</code> </td></tr>
|
||
<tr><td> Hex </td><td> <code>0xff</code> </td></tr>
|
||
<tr><td> Octal </td><td> <code>0o77</code> </td></tr>
|
||
<tr><td> Binary </td><td> <code>0b1111_0000</code> </td></tr>
|
||
<tr><td> Byte (<code>u8</code> only) </td><td> <code>b'A'</code> </td></tr>
|
||
</table>
|
||
<p>那么该使用哪种类型的数字呢?如果拿不定主意,Rust 的默认类型通常就很好,数字类型默认是 <code>i32</code>:它通常是最快的,甚至在 64 位系统上也是。<code>isize</code> 或 <code>usize</code> 的主要作为集合的索引。</p>
|
||
<a class="header" href="#浮点型" name="浮点型"><h4>浮点型</h4></a>
|
||
<p>Rust 同样有两个主要的<strong>浮点数</strong>类型,<code>f32</code> 和 <code>f64</code>,它们是带小数点的数字,分别占 32 位和 64 位比特。默认类型是 <code>f64</code>,因为它与 <code>f32</code> 速度差不多,然而精度更高。在 32 位系统上也能够使用 <code>f64</code>,不过比使用 <code>f32</code> 要慢。多数情况下,以潜在的性能损耗换取更高的精度是合理的;如果觉得浮点数的大小是个麻烦,你应该以性能测试作为决策依据。</p>
|
||
<p>这是一个展示浮点数的实例:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let x = 2.0; // f64
|
||
|
||
let y: f32 = 3.0; // f32
|
||
}
|
||
</code></pre>
|
||
<p>浮点数采用 IEEE-754 标准表示。<code>f32</code>是单精度浮点数,<code>f64</code>是双精度浮点数。</p>
|
||
<a class="header" href="#数字运算符" name="数字运算符"><h4>数字运算符</h4></a>
|
||
<p>Rust 支持所有数字类型常见的基本数学运算操作:加法、减法、乘法、除法以及余数。如下代码展示了如何使用一个<code>let</code>语句来使用他们:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
// addition
|
||
let sum = 5 + 10;
|
||
|
||
// subtraction
|
||
let difference = 95.5 - 4.3;
|
||
|
||
// multiplication
|
||
let product = 4 * 30;
|
||
|
||
// division
|
||
let quotient = 56.7 / 32.2;
|
||
|
||
// remainder
|
||
let remainder = 43 % 5;
|
||
}
|
||
</code></pre>
|
||
<p>这些语句中的每个表达式使用了一个数学运算符并计算出了一个值,他们绑定到了一个变量。附录 B 包含了一个 Rust 提供的所有运算符的列表。</p>
|
||
<a class="header" href="#布尔型" name="布尔型"><h4>布尔型</h4></a>
|
||
<p>正如其他大部分编程语言一样,Rust 中的布尔类型有两个可能的值:<code>true</code>和<code>false</code>。Rust 中的布尔类型使用<code>bool</code>表示。例如:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let t = true;
|
||
|
||
let f: bool = false; // with explicit type annotation
|
||
}
|
||
</code></pre>
|
||
<p>使用布尔值的主要场景是条件表达式,例如<code>if</code>。在“控制流”(“Control Flow”)部分将讲到<code>if</code>表达式在 Rust 中如何工作。</p>
|
||
<a class="header" href="#字符类型" name="字符类型"><h4>字符类型</h4></a>
|
||
<p>目前为止只使用到了数字,不过 Rust 也支持字符。Rust 的<code>char</code>类型是大部分语言中基本字母字符类型,如下代码展示了如何使用它:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let c = 'z';
|
||
let z = 'ℤ';
|
||
let heart_eyed_cat = '😻';
|
||
}
|
||
</code></pre>
|
||
<p>Rust 的<code>char</code>类型代表了一个 Unicode 变量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。拼音字母(Accented letters),中文/日文/汉语等象形文字,emoji(絵文字)以及零长度的空白字符对于 Rust <code>char</code>类型都是有效的。Unicode 标量值包含从 <code>U+0000</code> 到 <code>U+D7FF</code> 和 <code>U+E000</code> 到 <code>U+10FFFF</code> 之间的值。不过,“字符”并不是一个 Unicode 中的概念,所以人直觉上的“字符”可能与 Rust 中的<code>char</code>并不符合。第八章的“字符串”部分将详细讨论这个主题。</p>
|
||
<a class="header" href="#复合类型" name="复合类型"><h3>复合类型</h3></a>
|
||
<p><strong>复合类型</strong>可以将多个其他类型的值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。</p>
|
||
<a class="header" href="#将值组合进元组" name="将值组合进元组"><h4>将值组合进元组</h4></a>
|
||
<p>元组是一个将多个其他类型的值组合进一个复合类型的主要方式。</p>
|
||
<p>我们使用一个括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了额外的可选类型注解:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let tup: (i32, f64, u8) = (500, 6.4, 1);
|
||
}
|
||
</code></pre>
|
||
<p><code>tup</code>变量绑定了整个元组,因为元组被认为是一个单独的复合元素。为了从元组中获取单个的值,可以使用模式匹配(pattern matching)来解构(destructure )元组,像这样:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let tup = (500, 6.4, 1);
|
||
|
||
let (x, y, z) = tup;
|
||
|
||
println!("The value of y is: {}", y);
|
||
}
|
||
</code></pre>
|
||
<p>程序首先创建了一个元组并绑定到<code>tup</code>变量上。接着使用了<code>let</code>和一个模式将<code>tup</code>分成了三个不同的变量,<code>x</code>、<code>y</code>和<code>z</code>。这叫做<em>解构</em>(<em>destructuring</em>),因为它将一个元组拆成了三个部分。最后,程序打印出了<code>y</code>的值,也就是<code>6.4</code>。</p>
|
||
<p>除了使用模式匹配解构之外,也可以使用点号(<code>.</code>)后跟值的索引来直接访问他们。例如:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let x: (i32, f64, u8) = (500, 6.4, 1);
|
||
|
||
let five_hundred = x.0;
|
||
|
||
let six_point_four = x.1;
|
||
|
||
let one = x.2;
|
||
}
|
||
</code></pre>
|
||
<p>这个程序创建了一个元组,<code>x</code>,并接着使用索引为每个元素创建新变量。跟大多数编程语言一样,元组的第一个索引值是 0。</p>
|
||
<a class="header" href="#数组" name="数组"><h4>数组</h4></a>
|
||
<p>另一个获取一个多个值集合的方式是<strong>数组</strong>(<em>array</em>)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,他们的长度不能增长或缩小。</p>
|
||
<p>Rust 中数组的值位于中括号中的逗号分隔的列表中:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let a = [1, 2, 3, 4, 5];
|
||
}
|
||
</code></pre>
|
||
<p>数组在想要在栈(stack)而不是在堆(heap)上为数据分配空间时十分有用(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时。虽然它并不如 vector 类型那么灵活。vector 类型是标准库提供的一个<strong>允许</strong>增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector:第八章会详细讨论 vector。</p>
|
||
<p>一个你可能想要使用数组而不是 vector 的例子是当程序需要知道一年中月份的名字时。程序不大可能回去增加或减少月份,这时你可以使用数组因为我们知道它总是含有 12 个元素:</p>
|
||
<pre><code class="language-rust">let months = ["January", "February", "March", "April", "May", "June", "July",
|
||
"August", "September", "October", "November", "December"];
|
||
</code></pre>
|
||
<a class="header" href="#访问数组元素" name="访问数组元素"><h5>访问数组元素</h5></a>
|
||
<p>数组是一整块分配在栈上的内存。可以使用索引来访问数组的元素,像这样:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let a = [1, 2, 3, 4, 5];
|
||
|
||
let first = a[0];
|
||
let second = a[1];
|
||
}
|
||
</code></pre>
|
||
<p>在这个例子中,叫做<code>first</code>的变量的值是<code>1</code>,因为它是数组索引<code>[0]</code>的值。<code>second</code>将会是数组索引<code>[1]</code>的值<code>2</code>。</p>
|
||
<a class="header" href="#无效的数组元素访问" name="无效的数组元素访问"><h5>无效的数组元素访问</h5></a>
|
||
<p>如果我们访问数组结尾之后的元素会发生什么呢?比如我们将上面的例子改为如下:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let a = [1, 2, 3, 4, 5];
|
||
|
||
let element = a[10];
|
||
|
||
println!("The value of element is: {}", element);
|
||
}
|
||
</code></pre>
|
||
<p>使用<code>cargo run</code>运行代码后会产生如下结果:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling arrays v0.1.0 (file:///projects/arrays)
|
||
Running `target/debug/arrays`
|
||
thread '<main>' panicked at 'index out of bounds: the len is 5 but the index is
|
||
10', src/main.rs:4
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
error: Process didn't exit successfully: `target/debug/arrays` (exit code: 101)
|
||
</code></pre>
|
||
<p>编译并没有产生任何错误,不过程序会导致一个<strong>运行时</strong>(<em>runtime</em>)错误并且不会成功退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会<em>panic</em>,这是 Rust 中的术语,它用于程序因为错误而退出的情况。</p>
|
||
<p>这是第一个在实战中遇到的 Rust 安全原则的例子。在很多底层语言中,并没有进行这类检查,这样当提供了一个不正确的索引时,就会访问无效的内存。Rust 通过立即退出而不是允许内存访问并继续执行来使你免受这类错误困扰。第九章会讨论更多 Rust 的错误处理。</p>
|
||
<a class="header" href="#函数如何工作" name="函数如何工作"><h2>函数如何工作</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-03-how-functions-work.md">ch03-03-how-functions-work.md</a>
|
||
<br>
|
||
commit 04aa3a45eb72855b34213703718f50a12a3eeec8</p>
|
||
</blockquote>
|
||
<p>函数在 Rust 代码中应用广泛。你已经见过一个语言中最重要的函数:<code>main</code>函数,它时很多程序的入口点。你也见过了<code>fn</code>关键字,它用来声明新函数。</p>
|
||
<p>Rust 代码使用 <em>snake case</em> 作为函数和变量名称的规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。这里是一个函数定义程序的例子:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
println!("Hello, world!");
|
||
|
||
another_function();
|
||
}
|
||
|
||
fn another_function() {
|
||
println!("Another function.");
|
||
}
|
||
</code></pre>
|
||
<p>Rust 中的函数定义以<code>fn</code>开始并在函数名后跟一对括号。大括号告诉编译器哪里是函数体的开始和结尾。</p>
|
||
<p>可以使用定义过的函数名后跟括号来调用任意函数。因为<code>another_function</code>已经在程序中定义过了,它可以在<code>main</code>函数中被调用。注意,源码中<code>another_function</code>在<code>main</code>函数<em>之后</em>被定义;也可以在其之前定义。Rust 不关心函数定义于何处,只要他们被定义了。</p>
|
||
<p>让我们开始一个叫做 <em>functions</em> 的新二进制项目来进一步探索函数。将上面的<code>another_function</code>例子写入 <em>src/main.rs</em> 中并运行。你应该会看到如下输出:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling functions v0.1.0 (file:///projects/functions)
|
||
Running `target/debug/functions`
|
||
Hello, world!
|
||
Another function.
|
||
</code></pre>
|
||
<p>代码在<code>main</code>函数中按照他们出现的顺序被执行。首先,打印“Hello, world!”信息,接着<code>another_function</code>被调用并打印它的信息。</p>
|
||
<a class="header" href="#函数参数" name="函数参数"><h3>函数参数</h3></a>
|
||
<p>函数也可以被定义为拥有<strong>参数</strong>(<em>parameters</em>),他们是作为函数签名一部分的特殊变量。当函数拥有参数,可以为这些参数提供具体的值。技术上讲,这些具体值被称为参数( <em>arguments</em>),不过通常的习惯是倾向于在函数定义中的变量和调用函数时传递的具体值都可以用“parameter”和“argument”而不加区别。</p>
|
||
<p>如下被重写的<code>another_function</code>版本展示了 Rust 中参数是什么样的:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
another_function(5);
|
||
}
|
||
|
||
fn another_function(x: i32) {
|
||
println!("The value of x is: {}", x);
|
||
}
|
||
</code></pre>
|
||
<p>尝试运行程序,将会得到如下输出:</p>
|
||
<pre><code class="language-sh">$ cargo run
|
||
Compiling functions v0.1.0 (file:///projects/functions)
|
||
Running `target/debug/functions`
|
||
The value of x is: 5
|
||
</code></pre>
|
||
<p><code>another_function</code>的声明有一个叫做<code>x</code>的参数。<code>x</code>的类型被指定为<code>i32</code>。当<code>5</code>被传递给<code>another_function</code>时,<code>println!</code>宏将<code>5</code>放入格式化字符串中大括号的位置。</p>
|
||
<p>在函数签名中,<strong>必须</strong>声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解意味着编译器再也不需要在别的地方要求你注明类型就能知道你的意图。</p>
|
||
<p>当一个函数有多个参数时,使用逗号隔开他们,像这样:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
another_function(5, 6);
|
||
}
|
||
|
||
fn another_function(x: i32, y: i32) {
|
||
println!("The value of x is: {}", x);
|
||
println!("The value of y is: {}", y);
|
||
}
|
||
</code></pre>
|
||
<p>这个例子创建了一个有两个参数的函数,都是<code>i32</code>类型的。函数打印出了这两个参数的值。注意函数参数并不一定都是相同类型的,这个例子中他们只是碰巧相同罢了。</p>
|
||
<p>尝试运行代码。使用上面的例子替换当前 <em>function</em> 项目的 <em>src/main.rs</em> 文件,并<code>cargo run</code>运行它:</p>
|
||
<pre><code class="language-sh">$ cargo run
|
||
Compiling functions v0.1.0 (file:///projects/functions)
|
||
Running `target/debug/functions`
|
||
The value of x is: 5
|
||
The value of y is: 6
|
||
</code></pre>
|
||
<p>因为我们使用<code>5</code>作为<code>x</code>的值和<code>6</code>作为<code>y</code>的值来调用函数,这两个字符串和他们的值并被打印出来。</p>
|
||
<a class="header" href="#函数体" name="函数体"><h3>函数体</h3></a>
|
||
<p>函数体由一系列的语句和一个可选的表达式构成。目前为止,我们只涉及到了没有结尾表达式的函数,不过我们见过表达式作为了语句的一部分。因为 Rust 是一个基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及他们是如何影响函数体的。</p>
|
||
<a class="header" href="#语句与表达式" name="语句与表达式"><h3>语句与表达式</h3></a>
|
||
<p>我们已经用过语句与表达式了。<strong>语句</strong>(<em>Statements</em>)是执行一些操作但不返回值的指令。表达式(<em>Expressions</em>)计算并产生一个值。让我们看看一些例子:</p>
|
||
<p>使用<code>let</code>关键字创建变量并绑定一个值是一个语句。在列表 3-3 中,<code>let y = 6;</code>是一个语句:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let y = 6;
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 3-3: A <code>main</code> function declaration containing one statement.</span></p>
|
||
<p>函数定义也是语句;上面整个例子本身就是一个语句。</p>
|
||
<p>语句并不返回值。因此,不能把<code>let</code>语句赋值给另一个变量,比如下面的例子尝试做的:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let x = (let y = 6);
|
||
}
|
||
</code></pre>
|
||
<p>当运行这个程序,会得到如下错误:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling functions v0.1.0 (file:///projects/functions)
|
||
error: expected expression, found statement (`let`)
|
||
--> src/main.rs:2:14
|
||
|
|
||
2 | let x = (let y = 6);
|
||
| ^^^
|
||
|
|
||
= note: variable declaration using `let` is a statement
|
||
</code></pre>
|
||
<p><code>let y = 6</code>语句并不返回值,所以并没有<code>x</code>可以绑定的值。这与其他语言不同,例如 C 和 Ruby,他们的赋值语句返回所赋的值。在这些语言中,可以这么写<code>x = y = 6</code>这样<code>x</code>和<code>y</code>的值都是<code>6</code>;这在 Rust 中可不行。</p>
|
||
<p>表达式计算出一些值,而且他们组成了其余大部分你将会编写的 Rust 代码。考虑一个简单的数学运算,比如<code>5 + 6</code>,这是一个表达式并计算出值<code>11</code>。表达式可以是语句的一部分:在列表 3-3 中有这个语句<code>let y = 6;</code>,<code>6</code>是一个表达式它计算出的值是<code>6</code>。函数调用是一个表达式。宏调用是一个表达式。我们用来创新建作用域的大括号(代码块),<code>{}</code>,也是一个表达式,例如:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let x = 5;
|
||
|
||
let y = {
|
||
let x = 3;
|
||
x + 1
|
||
};
|
||
|
||
println!("The value of y is: {}", y);
|
||
}
|
||
</code></pre>
|
||
<p>这个表达式:</p>
|
||
<pre><code class="language-rust,ignore">{
|
||
let x = 3;
|
||
x + 1
|
||
}
|
||
</code></pre>
|
||
<p>这个代码块的值是<code>4</code>。这个值作为<code>let</code>语句的一部分被绑定到<code>y</code>上。注意结尾没有分号的那一行,与大部分我们见过的代码行不同。表达式并不包含结尾的分号。如果在表达式的结尾加上分号,他就变成了语句,这也就使其不返回一个值。在接下来的探索中记住函数和表达式都返回值就行了。</p>
|
||
<a class="header" href="#函数的返回值" name="函数的返回值"><h3>函数的返回值</h3></a>
|
||
<p>可以向调用它的代码返回值。并不对返回值命名,不过会在一个箭头(<code>-></code>)后声明它的类型。在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。这是一个有返回值的函数的例子:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn five() -> i32 {
|
||
5
|
||
}
|
||
|
||
fn main() {
|
||
let x = five();
|
||
|
||
println!("The value of x is: {}", x);
|
||
}
|
||
</code></pre>
|
||
<p>在函数<code>five</code>中并没有函数调用、宏、甚至也没有<code>let</code>语句————只有数字<code>5</code>它自己。这在 Rust 中是一个完全有效的函数。注意函数的返回值类型也被指定了,就是<code>-> i32</code>。尝试运行代码;输出应该看起来像这样:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling functions v0.1.0 (file:///projects/functions)
|
||
Running `target/debug/functions`
|
||
The value of x is: 5
|
||
</code></pre>
|
||
<p>函数<code>five</code>的返回值是<code>5</code>,也就是为什么返回值类型是<code>i32</code>。让我们仔细检查一下这段代码。这有两个重要的部分:首先,<code>let x = five();</code>这一行表明我们使用函数的返回值来初始化了一个变量。因为函数<code>five</code>返回<code>5</code>,这一行与如下这行相同:</p>
|
||
<pre><code class="language-rust">let x = 5;
|
||
</code></pre>
|
||
<p>其次,函数<code>five</code>没有参数并定义了返回值类型,不过函数体只有单单一个<code>5</code>也没有分号,因为这是我们想要返回值的表达式。让我们看看另一个例子:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let x = plus_one(5);
|
||
|
||
println!("The value of x is: {}", x);
|
||
}
|
||
|
||
fn plus_one(x: i32) -> i32 {
|
||
x + 1
|
||
}
|
||
</code></pre>
|
||
<p>运行代码会打印出<code>The value of x is: 6</code>。如果在包含<code>x + 1</code>的那一行的结尾加上一个分号,把它从表达式变成语句后会怎样呢?</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let x = plus_one(5);
|
||
|
||
println!("The value of x is: {}", x);
|
||
}
|
||
|
||
fn plus_one(x: i32) -> i32 {
|
||
x + 1;
|
||
}
|
||
</code></pre>
|
||
<p>运行代码会产生一个错误,如下:</p>
|
||
<pre><code>error[E0308]: mismatched types
|
||
--> src/main.rs:7:28
|
||
|
|
||
7 | fn plus_one(x: i32) -> i32 {
|
||
| ____________________________^ starting here...
|
||
8 | | x + 1;
|
||
9 | | }
|
||
| |_^ ...ending here: expected i32, found ()
|
||
|
|
||
= note: expected type `i32`
|
||
found type `()`
|
||
help: consider removing this semicolon:
|
||
--> src/main.rs:8:10
|
||
|
|
||
8 | x + 1;
|
||
| ^
|
||
</code></pre>
|
||
<p>主要的错误信息,“mismatched types,”(类型不匹配),揭示了代码的核心问题。函数<code>plus_one</code>的定义说明它要返回一个<code>i32</code>,不过语句并不返回一个值,这由那个空元组<code>()</code>表明。因此,这个函数返回了空元组<code>()</code>(译者注:原文说此函数没有返回任何值,可能有误),这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。</p>
|
||
<a class="header" href="#注释" name="注释"><h2>注释</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-04-comments.md">ch03-04-comments.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p>所有编程语言都力求使他们的代码易于理解,不过有时需要提供额外的解释。在这种情况下,程序员在源码中留下记录,或者<strong>注释</strong>(<em>comments</em>),编译器会忽略他们不过其他阅读代码的人可能会用得上。</p>
|
||
<p>这是一个注释的例子:</p>
|
||
<pre><code class="language-rust">// Hello, world.
|
||
</code></pre>
|
||
<p>在 Rust 中,注释必须以两道斜杠开始并持续到本行的结尾。对于超过一行的注释,需要在每一行都加上<code>//</code>,像这样:</p>
|
||
<pre><code class="language-rust">// So we’re doing something complicated here, long enough that we need
|
||
// multiple lines of comments to do it! Whew! Hopefully, this comment will
|
||
// explain what’s going on.
|
||
</code></pre>
|
||
<p>注释也可以在放在包含代码的行的末尾:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let lucky_number = 7; // I’m feeling lucky today.
|
||
}
|
||
</code></pre>
|
||
<p>不过你会经常看到他们被以这种格式使用,也就是位于它所解释的代码行的上面一行:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
// I’m feeling lucky today.
|
||
let lucky_number = 7;
|
||
}
|
||
</code></pre>
|
||
<p>这就是注释的全部。并没有什么特别复杂的。</p>
|
||
<a class="header" href="#控制流" name="控制流"><h2>控制流</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch03-05-control-flow.md">ch03-05-control-flow.md</a>
|
||
<br>
|
||
commit 04aa3a45eb72855b34213703718f50a12a3eeec8</p>
|
||
</blockquote>
|
||
<p>通过条件是不是真来决定是否某些代码,或者根据条件是否为真来重复运行一段代码是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是<code>if</code>表达式和循环。</p>
|
||
<a class="header" href="#if表达式" name="if表达式"><h3><code>if</code>表达式</h3></a>
|
||
<p><code>if</code>表达式允许根据条件执行不同的代码分支。我们提供一个条件并表示“如果符合这个条件,运行这段代码。如果条件不满足,不运行这段代码。”</p>
|
||
<p>在 <em>projects</em> 目录创建一个叫做 <em>branches</em> 的新项目来学习<code>if</code>表达式。在 <em>src/main.rs</em> 文件中,输入如下内容:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let number = 3;
|
||
|
||
if number < 5 {
|
||
println!("condition was true");
|
||
} else {
|
||
println!("condition was false");
|
||
}
|
||
}
|
||
</code></pre>
|
||
<!-- NEXT PARAGRAPH WRAPPED WEIRD INTENTIONALLY SEE #199 -->
|
||
<p>所有<code>if</code>表达式以<code>if</code>关键字开头,它后跟一个条件。在这个例子中,条件检查<code>number</code>是否有一个小于 5 的值。在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。<code>if</code>表达式中与条件关联的代码块有时被叫做 <em>arms</em>,就像第二章“比较猜测与秘密数字”部分中讨论到的<code>match</code>表达式中分支一样。也可以包含一个可选的<code>else</code>表达式,这里我们就这么做了,来提供一个在条件为假时应当执行的代码块。如果不提供<code>else</code>表达式并且条件为假时,程序会直接忽略<code>if</code>代码块并继续执行下面的代码。</p>
|
||
<p>尝试运行代码,应该能看到如下输出:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling branches v0.1.0 (file:///projects/branches)
|
||
Running `target/debug/branches`
|
||
condition was true
|
||
</code></pre>
|
||
<p>尝试改变<code>number</code>的值使条件为假时看看会发生什么:</p>
|
||
<pre><code class="language-rust,ignore">let number = 7;
|
||
</code></pre>
|
||
<p>再次运行程序并查看输出:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling branches v0.1.0 (file:///projects/branches)
|
||
Running `target/debug/branches`
|
||
condition was false
|
||
</code></pre>
|
||
<p>另外值得注意的是代码中的条件<strong>必须</strong>是<code>bool</code>。如果像看看条件不是<code>bool</code>值时会发生什么,尝试运行如下代码:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let number = 3;
|
||
|
||
if number {
|
||
println!("number was three");
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>这里<code>if</code>条件的值是<code>3</code>,Rust 抛出了一个错误:</p>
|
||
<pre><code>error[E0308]: mismatched types
|
||
--> src/main.rs:4:8
|
||
|
|
||
4 | if number {
|
||
| ^^^^^^ expected bool, found integral variable
|
||
|
|
||
= note: expected type `bool`
|
||
found type `{integer}`
|
||
</code></pre>
|
||
<p>这个错误表明 Rust 期望一个<code>bool</code>不过却得到了一个整型。Rust 并不会尝试自动地将非布尔值转换为布尔值,不像例如 Ruby 和 JavaScript 这样的语言。必须总是显式地使用<code>boolean</code>作为<code>if</code>的条件。例如如果想要<code>if</code>代码块只在一个数字不等于<code>0</code>时执行,可以把<code>if</code>表达式修改为如下:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let number = 3;
|
||
|
||
if number != 0 {
|
||
println!("number was something other than zero");
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>运行代码会打印出<code>number was something other than zero</code>。</p>
|
||
<a class="header" href="#使用else-if实现多重条件" name="使用else-if实现多重条件"><h4>使用<code>else if</code>实现多重条件</h4></a>
|
||
<p>可以将<code>else if</code>表达式与<code>if</code>和<code>else</code>组合来实现多重条件。例如:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let number = 6;
|
||
|
||
if number % 4 == 0 {
|
||
println!("number is divisible by 4");
|
||
} else if number % 3 == 0 {
|
||
println!("number is divisible by 3");
|
||
} else if number % 2 == 0 {
|
||
println!("number is divisible by 2");
|
||
} else {
|
||
println!("number is not divisible by 4, 3, or 2");
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>这个程序有四个可能的执行路径。运行后应该能看到如下输出:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling branches v0.1.0 (file:///projects/branches)
|
||
Running `target/debug/branches`
|
||
number is divisible by 3
|
||
</code></pre>
|
||
<p>当执行这个程序,它按顺序检查每个<code>if</code>表达式并执行第一个条件为真的代码块。注意即使 6 可以被 2 整除,也不会出现<code>number is divisible by 2</code>的输出,更不会出现<code>else</code>块中的<code>number is not divisible by 4, 3, or 2</code>。原因是 Rust 只会执行第一个条件为真的代码块,并且它一旦找到一个以后,就不会检查剩下的条件了。</p>
|
||
<p>使用过多的<code>else if</code>表达式会使代码显得杂乱无章,所以如果有多于一个<code>else if</code>,最好重构代码。为此第六章会介绍 Rust 中一个叫做<code>match</code>的强大的分支结构(branching construct)。</p>
|
||
<a class="header" href="#在let语句中使用if" name="在let语句中使用if"><h4>在<code>let</code>语句中使用<code>if</code></h4></a>
|
||
<p>因为<code>if</code>是一个表达式,我们可以在<code>let</code>语句的右侧使用它,例如在列表 3-4 中:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let condition = true;
|
||
let number = if condition {
|
||
5
|
||
} else {
|
||
6
|
||
};
|
||
|
||
println!("The value of number is: {}", number);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 3-4: Assigning the result of an <code>if</code> expression
|
||
to a variable</span></p>
|
||
<p><code>number</code>变量将会绑定到基于<code>if</code>表达式结果的值。运行这段代码看看会出现什么:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling branches v0.1.0 (file:///projects/branches)
|
||
Running `target/debug/branches`
|
||
The value of number is: 5
|
||
</code></pre>
|
||
<p>还记得代码块的值是其最后一个表达式的值,以及数字本身也是一个表达式吗。在这个例子中,整个<code>if</code>表达式的值依赖哪个代码块被执行。这意味着<code>if</code>的每个分支的可能的返回值都必须是相同类型;在列表 3-4 中,<code>if</code>分支和<code>else</code>分支的结果都是<code>i32</code>整型。不过如果像下面的例子那样这些类型并不匹配会怎么样呢?</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let condition = true;
|
||
|
||
let number = if condition {
|
||
5
|
||
} else {
|
||
"six"
|
||
};
|
||
|
||
println!("The value of number is: {}", number);
|
||
}
|
||
</code></pre>
|
||
<p>当运行这段代码,会得到一个错误。<code>if</code>和<code>else</code>分支的值类型是不相容的,同时 Rust 也准确地表明了在程序中的何处发现的这个问题:</p>
|
||
<pre><code>error[E0308]: if and else have incompatible types
|
||
--> src/main.rs:4:18
|
||
|
|
||
4 | let number = if condition {
|
||
| __________________^ starting here...
|
||
5 | | 5
|
||
6 | | } else {
|
||
7 | | "six"
|
||
8 | | };
|
||
| |_____^ ...ending here: expected integral variable, found reference
|
||
|
|
||
= note: expected type `{integer}`
|
||
found type `&'static str`
|
||
</code></pre>
|
||
<p><code>if</code>代码块的表达式返回一个整型,而<code>else</code>代码块返回一个字符串。这并不可行,因为变量必须只有一个类型。Rust 需要在编译时就确切的知道<code>number</code>变量的类型,这样它就可以在编译时证明其他使用<code>number</code>变量的地方它的类型是有效的。Rust 并不能够在<code>number</code>的类型只能在运行时确定的情况下工作;这样会使编译器变得更复杂而且只能为代码提供更少的保障,因为它不得不记录所有变量的多种可能的类型。</p>
|
||
<a class="header" href="#使用循环重复执行" name="使用循环重复执行"><h3>使用循环重复执行</h3></a>
|
||
<p>多次执行一段代码是很常用的。为了这个功能,Rust 提供了多种<strong>循环</strong>(<em>loops</em>)。一个循环执行循环体中的代码直到结尾并紧接着从回到开头继续执行。为了实验一下循环,让我们创建一个叫做 <em>loops</em> 的新项目。</p>
|
||
<p>Rust 有三种循环类型:<code>loop</code>、<code>while</code>和<code>for</code>。让我们每一个都试试。</p>
|
||
<a class="header" href="#使用loop重复执行代码" name="使用loop重复执行代码"><h4>使用<code>loop</code>重复执行代码</h4></a>
|
||
<p><code>loop</code>关键字告诉 Rust 一遍又一遍的执行一段代码直到你明确要求停止。</p>
|
||
<p>作为一个例子,将 <em>loops</em> 目录中的 <em>src/main.rs</em> 文件修改为如下:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
loop {
|
||
println!("again!");
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>当执行这个程序,我们会看到<code>again!</code>被连续的打印直到我们手动停止程序.大部分终端都支持一个键盘快捷键,ctrl-C,来终止一个陷入无限循环的程序。尝试一下:</p>
|
||
<pre><code class="language-sh">$ cargo run
|
||
Compiling loops v0.1.0 (file:///projects/loops)
|
||
Running `target/debug/loops`
|
||
again!
|
||
again!
|
||
again!
|
||
again!
|
||
^Cagain!
|
||
</code></pre>
|
||
<p>符号<code>^C</code>代表你在这按下了 ctrl-C。在<code>^C</code>之后你可能看到<code>again!</code>也可能看不到,这依赖于在接收到终止信号时代码执行到了循环的何处。</p>
|
||
<p>幸运的是,Rust 提供了另一个更可靠的方式来退出循环。可以使用<code>break</code>关键字来告诉程序何时停止执行循环。还记得我们在第二章猜猜看游戏的“猜测正确后退出”部分使用过它来在用户猜对数字赢得游戏后退出程序吗。</p>
|
||
<a class="header" href="#while条件循环" name="while条件循环"><h4><code>while</code>条件循环</h4></a>
|
||
<p>在程序中计算循环的条件也很常见。当条件为真,执行循环。当条件不再为真,调用<code>break</code>停止循环。这个循环类型可以通过组合<code>loop</code>、<code>if</code>、<code>else</code>和<code>break</code>来实现;如果你喜欢的话,现在就可以在程序中试试。</p>
|
||
<p>然而,这个模式太常见了以至于 Rust 为此提供了一个内建的语言结构,它被称为<code>while</code>循环。下面的例子使用了<code>while</code>:程序循环三次,每次数字都减一。接着,在循环之后,打印出另一个信息并退出:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let mut number = 3;
|
||
|
||
while number != 0 {
|
||
println!("{}!", number);
|
||
|
||
number = number - 1;
|
||
}
|
||
|
||
println!("LIFTOFF!!!");
|
||
}
|
||
</code></pre>
|
||
<p>这个结构消除了很多需要嵌套使用<code>loop</code>、<code>if</code>、<code>else</code>和<code>break</code>的代码,这样显得更加清楚。当条件为真就执行,否则退出循环。</p>
|
||
<a class="header" href="#使用for遍历集合" name="使用for遍历集合"><h4>使用<code>for</code>遍历集合</h4></a>
|
||
<p>可以使用<code>while</code>结构来遍历一个元素集合,比如数组。例如:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let a = [10, 20, 30, 40, 50];
|
||
let mut index = 0;
|
||
|
||
while index < 5 {
|
||
println!("the value is: {}", a[index]);
|
||
|
||
index = index + 1;
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 3-5: Looping through each element of a collection
|
||
using a <code>while</code> loop</span></p>
|
||
<p>这里代码对数组中的元素进行计数。它从索引<code>0</code>开始,并接着循环直到遇到数组的最后一个索引(这时,<code>index < 5</code>不再为真)。运行这段代码会打印出数组中的每一个元素:</p>
|
||
<pre><code class="language-sh">$ cargo run
|
||
Compiling loops v0.1.0 (file:///projects/loops)
|
||
Running `target/debug/loops`
|
||
the value is: 10
|
||
the value is: 20
|
||
the value is: 30
|
||
the value is: 40
|
||
the value is: 50
|
||
</code></pre>
|
||
<p>所有数组中的五个元素都如期被打印出来。尽管<code>index</code>在某一时刻会到达值<code>5</code>,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。</p>
|
||
<p>不过这个过程是容易出错的;如果索引长度不正确会导致程序 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。</p>
|
||
<p>可以使用<code>for</code>循环来对一个集合的每个元素执行一些代码,来作为一个更有效率替代。<code>for</code>循环看起来像这样:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let a = [10, 20, 30, 40, 50];
|
||
|
||
for element in a.iter() {
|
||
println!("the value is: {}", element);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 3-6: Looping through each element of a collection
|
||
using a <code>for</code> loop</span></p>
|
||
<p>当运行这段代码,将看到与列表 3-5 一样的输出。更为重要的是,我们增强了代码安全性并消除了出现可能会导致超出数组的结尾或遍历长度不够而缺少一些元素这类 bug 机会。</p>
|
||
<p>例如,在列表 3-5 的代码中,如果从数组<code>a</code>中移除一个元素但忘记更新条件为<code>while index < 4</code>,代码将会 panic。使用<code>for</code>循环的话,就不需要惦记着在更新数组元素数量时修改其他的代码了。</p>
|
||
<p><code>for</code>循环的安全性和简洁性使得它在成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如列表 3-5 中使用<code>while</code>循环的倒计时例子,大部分 Rustacean 也会使用<code>for</code>循环。这么做的方式是使用<code>Range</code>,它是标准库提供的用来生成从一个数字开始到另一个数字结束的所有数字序列的类型。</p>
|
||
<p>下面是一个使用<code>for</code>循环来倒计时的例子,它还使用了一个我们还未讲到的方法,<code>rev</code>,用来反转 range:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
for number in (1..4).rev() {
|
||
println!("{}!", number);
|
||
}
|
||
println!("LIFTOFF!!!");
|
||
}
|
||
</code></pre>
|
||
<p>这段代码看起来更帅气不是吗?</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>你做到了!这是一个相当可观的章节:你学习了变量,标量和<code>if</code>表达式,还有循环!如果你想要实践本章讨论的概念,尝试构建如下的程序:</p>
|
||
<ul>
|
||
<li>相互转换摄氏与华氏温度</li>
|
||
<li>生成 n 阶斐波那契数列</li>
|
||
<li>打印圣诞颂歌“The Twelve Days of Christmas”的歌词,并利用歌曲中的重复部分(编写循环)</li>
|
||
</ul>
|
||
<p>当你准备好继续的时候,让我们讨论一个其他语言中<em>并不</em>常见的概念:所有权(ownership)。</p>
|
||
<a class="header" href="#认识所有权" name="认识所有权"><h1>认识所有权</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-00-understanding-ownership.md">ch04-00-understanding-ownership.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p>所有权(系统)是 Rust 最独特的功能,它使得 Rust 可以无需垃圾回收(garbage collector)就能保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能:借用、slice 以及 Rust 如何在内存中摆放数据。</p>
|
||
<a class="header" href="#什么是所有权" name="什么是所有权"><h2>什么是所有权</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-01-what-is-ownership.md">ch04-01-what-is-ownership.md</a>
|
||
<br>
|
||
commit 6d4ef020095a375483b2121d4fa2b1661062cc92</p>
|
||
</blockquote>
|
||
<p>Rust 的核心功能(之一)是<strong>所有权</strong>(<em>ownership</em>)。虽然这个功能理解起来很直观,不过它对语言的其余部分有着更深层的含义。</p>
|
||
<p>所有程序都必须管理他们运行时使用计算机内存的方式。一些语言中使用垃圾回收在程序运行过程中来时刻寻找不再被使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:内存被一个所有权系统管理,它拥有一系列的规则使编译器在编译时进行检查。任何所有权系统的功能都不会导致运行时开销。</p>
|
||
<p>因为所有权对很多程序员都是一个新概念,需要一些时间来适应。好消息是随着你对 Rust 和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒!</p>
|
||
<p>当你理解了所有权系统,你就会对这个使 Rust 如此独特的功能有一个坚实的基础。在本章中,你将会通过一些例子来学习所有权,他们关注一个非常常见的数据结构:字符串。</p>
|
||
<!-- PROD: START BOX -->
|
||
<blockquote>
|
||
<a class="header" href="#栈stack与堆heap" name="栈stack与堆heap"><h3>栈(Stack)与堆(Heap)</h3></a>
|
||
<p>在很多语言中并不经常需要考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的选择。我们会在本章的稍后部分描述所有权与堆与栈相关的部分,所以这里只是一个用来预热的简要解释。</p>
|
||
<p>栈和堆都是代码在运行时可供使用的内存部分,不过他们以不同的结构组成。栈以放入值的顺序存储并以相反顺序取出值。这也被称作<strong>后进先出</strong>(<em>last in, first out</em>)。想象一下一叠盘子:当增加更多盘子时,把他们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做<strong>进栈</strong>(<em>pushing onto the stack</em>),而移出数据叫做<strong>出栈</strong>(<em>popping off the stack</em>)。</p>
|
||
<p>操作栈是非常快的,因为它访问数据的方式:永远也不需要寻找一个位置放入新数据或者取出数据因为这个位置总是在栈顶。另一个使得栈快速的性质是栈中的所有数据都必须是一个已知的固定的大小。</p>
|
||
<p>相反对于在编译时未知大小或大小可能变化的数据,可以把他们储存在堆上。堆是缺乏组织的:当向堆放入数据时,我们请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回给我们一个它位置的指针。这个过程称作<strong>在堆上分配内存</strong>(<em>allocating on the heap</em>),并且有时这个过程就简称为“分配”(allocating)。向栈中放入数据并不被认为是分配。因为指针是已知的固定大小的,我们可以将指针储存在栈上,不过当需要实际数据时,必须访问指针。</p>
|
||
<p>想象一下去餐馆就坐吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。</p>
|
||
<p>访问堆上的数据要比访问栈上的数据要慢因为必须通过指针来访问。现代的处理器在内存中跳转越少就越快。继续类比,假设有一台服务器来处理来自多个桌子的订单。它在处理完一个桌子的所有订单后再移动到下一个桌子是最有效率的。从桌子 A 获取一个订单,接着再从桌子 B 获取一个订单,然后再从桌子 A,然后再从桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据之间彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。</p>
|
||
<p>当调用一个函数,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。</p>
|
||
<p>记录何处的代码在使用堆上的什么数据,最小化堆上的冗余数据的数量以及清理堆上不再使用的数据以致不至于耗尽空间,这些所有的问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过理解如何管理堆内存可以帮助我们理解所有权为何存在以及为什么以这种方式工作。</p>
|
||
</blockquote>
|
||
<!-- PROD: END BOX -->
|
||
<a class="header" href="#所有权规则" name="所有权规则"><h3>所有权规则</h3></a>
|
||
<p>首先,让我们看一下所有权的规则。请记住它们,我们将讲解一些它们的例子:</p>
|
||
<blockquote>
|
||
<ol>
|
||
<li>每一个值都被它的<strong>所有者</strong>(<em>owner</em>)变量拥有。</li>
|
||
<li>值在任意时刻只能被一个所有者拥有。</li>
|
||
<li>当所有者离开作用域,这个值将被丢弃。</li>
|
||
</ol>
|
||
</blockquote>
|
||
<a class="header" href="#变量作用域" name="变量作用域"><h3>变量作用域</h3></a>
|
||
<p>我们已经在第二章完成过一个 Rust 程序的例子。现在我们已经掌握了基本语法,所以不会在所有的例子中包含<code>fn main() {</code>代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个<code>main</code>函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。</p>
|
||
<p>作为所有权的第一个例子,我们看看一些变量的<strong>作用域</strong>(<em>scope</em>)。作用域是一个项(原文:item)在程序中有效的范围。假如有一个这样的变量:</p>
|
||
<pre><code class="language-rust">let s = "hello";
|
||
</code></pre>
|
||
<p>变量<code>s</code>绑定到了一个字符串字面值,这个字符串值是硬编码进我们程序代码中的。这个变量从声明的点开始直到当前<em>作用域</em>结束时都是有效的。列表 4-1 的注释标明了变量<code>s</code>在哪里是有效的:</p>
|
||
<pre><code class="language-rust">{ // s is not valid here, it’s not yet declared
|
||
let s = "hello"; // s is valid from this point forward
|
||
|
||
// do stuff with s
|
||
} // this scope is now over, and s is no longer valid
|
||
</code></pre>
|
||
<p><span class="caption">Listing 4-1: A variable and the scope in which it is
|
||
valid</span></p>
|
||
<p>换句话说,这里有两个重要的点:</p>
|
||
<ol>
|
||
<li>当<code>s</code><strong>进入作用域</strong>,它就是有效的。</li>
|
||
<li>这一直持续到它<strong>离开作用域</strong>为止。</li>
|
||
</ol>
|
||
<p>目前为止,变量是否有效与作用域的关系跟其他编程语言是类似的。现在我们在此基础上介绍<code>String</code>类型。</p>
|
||
<a class="header" href="#string类型" name="string类型"><h3><code>String</code>类型</h3></a>
|
||
<p>为了演示所有权的规则,我们需要一个比第三章讲到的任何一个都要复杂的数据类型。之前出现的数据类型都是储存在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个储存在堆上的数据来探索 Rust 如何知道该在何时清理数据的。</p>
|
||
<p>这里使用<code>String</code>作为例子并专注于<code>String</code>与所有权相关的部分。这些方面也同样适用于其他标准库提供的或你自己创建的复杂数据类型。在第八章会更深入地讲解<code>String</code>。</p>
|
||
<p>我们已经见过字符串字面值了,它被硬编码进程序里。字符串字面值是很方便的,不过他们并不总是适合所有需要使用文本的场景。原因之一就是他们是不可变的。另一个原因是不是所有字符串的值都能在编写代码时就知道:例如,如果想要获取用户输入并储存该怎么办呢?为此,Rust 有第二个字符串类型,<code>String</code>。这个类型储存在堆上所以储存在编译时未知大小的文本。可以用<code>from</code>从字符串字面值来创建<code>String</code>,如下:</p>
|
||
<pre><code class="language-rust">let s = String::from("hello");
|
||
</code></pre>
|
||
<p>这两个冒号(<code>::</code>)运算符允许将特定的<code>from</code>函数置于<code>String</code>类型的命名空间(namespace)下而不需要使用类似<code>string_from</code>这样的名字。在第五章的“方法语法”(“Method Syntax”)部分会着重讲解这个语法而且在第七章会讲到模块的命名空间。</p>
|
||
<p>这类字符串<em>可以</em>被修改:</p>
|
||
<pre><code class="language-rust">let mut s = String::from("hello");
|
||
|
||
s.push_str(", world!"); // push_str() appends a literal to a String
|
||
|
||
println!("{}", s); // This will print `hello, world!`
|
||
</code></pre>
|
||
<p>那么这里有什么区别呢?为什么<code>String</code>可变而字面值却不行呢?区别在于两个类型对内存的处理上。</p>
|
||
<a class="header" href="#内存与分配" name="内存与分配"><h3>内存与分配</h3></a>
|
||
<p>对于字符串字面值的情况,我们在编译时就知道内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速和高效。不过这些属性都只来源于它的不可变形。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。</p>
|
||
<p>对于<code>String</code>类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:</p>
|
||
<ol>
|
||
<li>内存必须在运行时向操作系统请求</li>
|
||
<li>需要一个当我们处理完<code>String</code>时将内存返回给操作系统的方法</li>
|
||
</ol>
|
||
<p>第一部分由我们完成:当调用<code>String::from</code>时,它的实现请求它需要的内存。这在编程语言中是非常通用的。</p>
|
||
<p>然而,第二部分实现起来就各有区别了。在有<strong>垃圾回收</strong>(<em>GC</em>)的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要<code>allocate</code>和<code>free</code>一一对应。</p>
|
||
<p>Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是列表 4-1 中作用域例子的一个使用<code>String</code>而不是字符串字面值的版本:</p>
|
||
<pre><code class="language-rust">{
|
||
let s = String::from("hello"); // s is valid from this point forward
|
||
|
||
// do stuff with s
|
||
} // this scope is now over, and s is no
|
||
// longer valid
|
||
</code></pre>
|
||
<p>这里是一个将<code>String</code>需要的内存返回给操作系统的很自然的位置:当<code>s</code>离开作用域的时候。当变量离开作用域,Rust 为其调用一个特殊的函数。这个函数叫做 <code>drop</code>,在这里<code>String</code>的作者可以放置释放内存的代码。Rust 在结尾的<code>}</code>处自动调用<code>drop</code>。</p>
|
||
<blockquote>
|
||
<p>注意:在 C++ 中,这种 item 在生命周期结束时释放资源的方法有时被称作<strong>资源获取即初始化</strong>(<em>Resource Acquisition Is Initialization (RAII)</em>)。如果你使用过 RAII 模式的话应该对 Rust 的<code>drop</code>函数不陌生。</p>
|
||
</blockquote>
|
||
<p>这个模式对编写 Rust 代码的方式有着深远的影响。它现在看起来很简单,不过在更复杂的场景下代码的行为可能是不可预测的,比如当有多个变量使用在堆上分配的内存时。现在让我们探索一些这样的场景。</p>
|
||
<a class="header" href="#变量与数据交互移动" name="变量与数据交互移动"><h4>变量与数据交互:移动</h4></a>
|
||
<p>Rust 中的多个变量以一种独特的方式与同一数据交互。让我们看看列表 4-2 中一个使用整型的例子:</p>
|
||
<pre><code class="language-rust">let x = 5;
|
||
let y = x;
|
||
</code></pre>
|
||
<p><span class="caption">Listing 4-2: Assigning the integer value of variable <code>x</code>
|
||
to <code>y</code></span></p>
|
||
<p>根据其他语言的经验大致可以猜到这在干什么:“将<code>5</code>绑定到<code>x</code>;接着生成一个值<code>x</code>的拷贝并绑定到<code>y</code>”。现在有了两个变量,<code>x</code>和<code>y</code>,都等于<code>5</code>。这也正是事实上发生了的,因为正数是有已知固定大小的简单值,所以这两个<code>5</code>被放入了栈中。</p>
|
||
<p>现在看看这个<code>String</code>版本:</p>
|
||
<pre><code class="language-rust">let s1 = String::from("hello");
|
||
let s2 = s1;
|
||
</code></pre>
|
||
<p>这看起来与上面的代码非常类似,所以我们可能会假设他们的运行方式也是类似的:也就是说,第二行可能会生成一个<code>s1</code>的拷贝并绑定到<code>s2</code>上。不过,事实上并不完全是这样。</p>
|
||
<p>为了更全面的解释这个问题,让我们看看图 4-3 中<code>String</code>真正是什么样。<code>String</code>由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据储存在栈上。右侧则是堆上存放内容的内存部分。</p>
|
||
<p><img alt="String in memory" src="img/trpl04-01.svg" class="center" style="width: 50%;" /></p>
|
||
<p><span class="caption">Figure 4-3: Representation in memory of a <code>String</code>
|
||
holding the value <code>"hello"</code> bound to <code>s1</code></span></p>
|
||
<p>长度代表当前<code>String</code>的内容使用了多少字节的内存。容量是<code>String</code>从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过这在目前为止的场景中并不重要,所以可以暂时忽略容量。</p>
|
||
<p>当我们把<code>s1</code>赋值给<code>s2</code>,<code>String</code>的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制堆上指针所指向的数据。换句话说,内存中数据的表现如图 4-4 所示。</p>
|
||
<p><img alt="s1 and s2 pointing to the same value" src="img/trpl04-02.svg" class="center" style="width: 50%;" /></p>
|
||
<p><span class="caption">Figure 4-4: Representation in memory of the variable <code>s2</code>
|
||
that has a copy of the pointer, length, and capacity of <code>s1</code></span></p>
|
||
<p>这个表现形式看起来<strong>并不像</strong>图 4-5 中的那样,它是如果 Rust 也拷贝了堆上的数据后内存看起来是怎么样的。如果 Rust 这么做了,那么操作<code>s2 = s1</code>在堆上数据比较大的时候可能会对运行时性能造成非常大的影响。</p>
|
||
<p><img alt="s1 and s2 to two places" src="img/trpl04-03.svg" class="center" style="width: 50%;" /></p>
|
||
<p><span class="caption">Figure 4-5: Another possibility of what <code>s2 = s1</code> might
|
||
do if Rust copied the heap data as well</span></p>
|
||
<p>之前,我们提到过当变量离开作用域后 Rust 自动调用<code>drop</code>函数并清理变量的堆内存。不过图 4-4 展示了两个数据指针指向了同一位置。这就有了一个问题:当<code>s2</code>和<code>s1</code>离开作用域,他们都会尝试释放相同的内存。这是一个叫做 <em>double free</em> 的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。</p>
|
||
<p>为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为<code>s1</code>不再有效,因此 Rust 不需要在<code>s1</code>离开作用域后清理任何东西。看看在<code>s2</code>被创建之后尝试使用<code>s1</code>会发生生么:</p>
|
||
<pre><code class="language-rust,ignore">let s1 = String::from("hello");
|
||
let s2 = s1;
|
||
|
||
println!("{}", s1);
|
||
</code></pre>
|
||
<p>你会得到一个类似如下的错误,因为 Rust 禁止你使用无效的引用。</p>
|
||
<pre><code class="language-sh">error[E0382]: use of moved value: `s1`
|
||
--> src/main.rs:4:27
|
||
|
|
||
3 | let s2 = s1;
|
||
| -- value moved here
|
||
4 | println!("{}, world!", s1);
|
||
| ^^ value used here after move
|
||
|
|
||
= note: move occurs because `s1` has type `std::string::String`,
|
||
which does not implement the `Copy` trait
|
||
</code></pre>
|
||
<p>如果你在其他语言中听说过术语“浅拷贝”(“shallow copy”)和“深拷贝”(“deep copy”),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效化了,这个操作被称为<strong>移动</strong>(<em>move</em>),而不是浅拷贝。上面的例子可以解读为<code>s1</code>被<strong>移动</strong>到了<code>s2</code>中。那么具体发生了什么如图 4-6 所示。</p>
|
||
<p><img alt="s1 moved to s2" src="img/trpl04-04.svg" class="center" style="width: 50%;" /></p>
|
||
<p><span class="caption">Figure 4-6: Representation in memory after <code>s1</code> has been
|
||
invalidated</span></p>
|
||
<p>这样就解决了我们的麻烦!因为只有<code>s2</code>是有效的,当其离开作用域,它就释放自己的内存,完毕。</p>
|
||
<p>另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的“深拷贝”。因此,任何<strong>自动</strong>的复制可以被认为对运行时性能影响较小。</p>
|
||
<a class="header" href="#变量与数据交互克隆" name="变量与数据交互克隆"><h4>变量与数据交互:克隆</h4></a>
|
||
<p>如果我们<strong>确实</strong>需要深度复制<code>String</code>中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做<code>clone</code>的通用函数。第五章会讨论方法语法,不过因为方法在很多语言中是一个常见功能,所以之前你可能已经见过了。</p>
|
||
<p>这是一个实际使用<code>clone</code>方法的例子:</p>
|
||
<pre><code class="language-rust">let s1 = String::from("hello");
|
||
let s2 = s1.clone();
|
||
|
||
println!("s1 = {}, s2 = {}", s1, s2);
|
||
</code></pre>
|
||
<p>这段代码能正常运行,也是如何显式产生图 4-5 中行为的方式,这里堆上的数据<strong>被复制了</strong>。</p>
|
||
<p>当出现<code>clone</code>调用时,你知道一些特有的代码被执行而且这些代码可能相当消耗资源。所以它作为一个可视化的标识代表了不同的行为。</p>
|
||
<a class="header" href="#只在栈上的数据拷贝" name="只在栈上的数据拷贝"><h4>只在栈上的数据:拷贝</h4></a>
|
||
<p>这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的,他们是之前列表 4-2 中的一部分:</p>
|
||
<pre><code class="language-rust">let x = 5;
|
||
let y = x;
|
||
|
||
println!("x = {}, y = {}", x, y);
|
||
</code></pre>
|
||
<p>他们似乎与我们刚刚学到的内容相抵触:没有调用<code>clone</code>,不过<code>x</code>依然有效且没有被移动到<code>y</code>中。</p>
|
||
<p>原因是像整型这样的在编译时已知大小的类型被整个储存在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量<code>y</code>后使<code>x</code>无效。换句话说,这里没有深浅拷贝的区别,所以调用<code>clone</code>并不会与通常的浅拷贝有什么不同,我们可以不用管它。</p>
|
||
<p>Rust 有一个叫做<code>Copy</code> trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait)。如果一个类型拥有<code>Copy</code> trait,一个旧的变量在(重新)赋值后仍然可用。Rust 不允许自身或其任何部分实现了<code>Drop</code> trait 的类型使用<code>Copy</code> trait。如果我们对其值离开作用域时需要特殊处理的类型使用<code>Copy</code>注解,将会出现一个编译时错误。关于如何为你的类型增加<code>Copy</code>注解,请阅读附录 C 中的 Derivable Trait。</p>
|
||
<p>那么什么类型是<code>Copy</code>的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是<code>Copy</code>的,任何不需要分配内存或类似形式资源的类型是<code>Copy</code>的,如下是一些<code>Copy</code>的类型:</p>
|
||
<ul>
|
||
<li>所有整数类型,比如<code>u32</code>。</li>
|
||
<li>布尔类型,<code>bool</code>,它的值是<code>true</code>和<code>false</code>。</li>
|
||
<li>所有浮点数类型,比如<code>f64</code>。</li>
|
||
<li>元组,当且仅当其包含的类型也都是<code>Copy</code>的时候。<code>(i32, i32)</code>是<code>Copy</code>的,不过<code>(i32, String)</code>就不是。</li>
|
||
</ul>
|
||
<a class="header" href="#所有权与函数" name="所有权与函数"><h3>所有权与函数</h3></a>
|
||
<p>将值传递给函数在语言上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。列表 4-7 是一个带有变量何时进入和离开作用域标注的例子:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let s = String::from("hello"); // s comes into scope.
|
||
|
||
takes_ownership(s); // s's value moves into the function...
|
||
// ... and so is no longer valid here.
|
||
let x = 5; // x comes into scope.
|
||
|
||
makes_copy(x); // x would move into the function,
|
||
// but i32 is Copy, so it’s okay to still
|
||
// use x afterward.
|
||
|
||
} // Here, x goes out of scope, then s. But since s's value was moved, nothing
|
||
// special happens.
|
||
|
||
fn takes_ownership(some_string: String) { // some_string comes into scope.
|
||
println!("{}", some_string);
|
||
} // Here, some_string goes out of scope and `drop` is called. The backing
|
||
// memory is freed.
|
||
|
||
fn makes_copy(some_integer: i32) { // some_integer comes into scope.
|
||
println!("{}", some_integer);
|
||
} // Here, some_integer goes out of scope. Nothing special happens.
|
||
</code></pre>
|
||
<p><span class="caption">Listing 4-7: Functions with ownership and scope
|
||
annotated</span></p>
|
||
<p>当尝试在调用<code>takes_ownership</code>后使用<code>s</code>时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在<code>main</code>函数中添加使用<code>s</code>和<code>x</code>的代码来看看哪里能使用他们,以及哪里所有权规则会阻止我们这么做。</p>
|
||
<a class="header" href="#返回值与作用域" name="返回值与作用域"><h3>返回值与作用域</h3></a>
|
||
<p>返回值也可以转移作用域。这里是一个有与列表 4-7 中类似标注的例子:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let s1 = gives_ownership(); // gives_ownership moves its return
|
||
// value into s1.
|
||
|
||
let s2 = String::from("hello"); // s2 comes into scope.
|
||
|
||
let s3 = takes_and_gives_back(s2); // s2 is moved into
|
||
// takes_and_gives_back, which also
|
||
// moves its return value into s3.
|
||
} // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
|
||
// moved, so nothing happens. s1 goes out of scope and is dropped.
|
||
|
||
fn gives_ownership() -> String { // gives_ownership will move its
|
||
// return value into the function
|
||
// that calls it.
|
||
|
||
let some_string = String::from("hello"); // some_string comes into scope.
|
||
|
||
some_string // some_string is returned and
|
||
// moves out to the calling
|
||
// function.
|
||
}
|
||
|
||
// takes_and_gives_back will take a String and return one.
|
||
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
|
||
// scope.
|
||
|
||
a_string // a_string is returned and moves out to the calling function.
|
||
}
|
||
</code></pre>
|
||
<p>变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过<code>drop</code>被清理掉,除非数据被移动为另一个变量所有。</p>
|
||
<p>在每一个函数中都获取并接着返回所有权是冗余乏味的。如果我们想要函数使用一个值但不获取所有权改怎么办呢?如果我们还要接着使用它的话,每次都传递出去再传回来就有点烦人了,另外我们也可能想要返回函数体产生的任何(不止一个)数据。</p>
|
||
<p>使用元组来返回多个值是可能的,像这样:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let s1 = String::from("hello");
|
||
|
||
let (s2, len) = calculate_length(s1);
|
||
|
||
println!("The length of '{}' is {}.", s2, len);
|
||
}
|
||
|
||
fn calculate_length(s: String) -> (String, usize) {
|
||
let length = s.len(); // len() returns the length of a String.
|
||
|
||
(s, length)
|
||
}
|
||
</code></pre>
|
||
<p>但是这不免有些形式主义,同时这离一个通用的观点还有很长距离。幸运的是,Rust 对此提供了一个功能,叫做<strong>引用</strong>(<em>references</em>)。</p>
|
||
<a class="header" href="#引用与借用" name="引用与借用"><h2>引用与借用</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-02-references-and-borrowing.md">ch04-02-references-and-borrowing.md</a>
|
||
<br>
|
||
commit 5e0546f53cce14b126527d9ba6d1b8eb212b4f3d</p>
|
||
</blockquote>
|
||
<p>在上一部分的结尾处的使用元组的代码是有问题的,我们需要将<code>String</code>返回给调用者函数这样就可以在调用<code>calculate_length</code>后仍然可以使用<code>String</code>了,因为<code>String</code>先被移动到了<code>calculate_length</code>。</p>
|
||
<p>下面是如何定义并使用一个(新的)<code>calculate_length</code>函数,它以一个对象的<strong>引用</strong>作为参数而不是获取值的所有权:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let s1 = String::from("hello");
|
||
|
||
let len = calculate_length(&s1);
|
||
|
||
println!("The length of '{}' is {}.", s1, len);
|
||
}
|
||
|
||
fn calculate_length(s: &String) -> usize {
|
||
s.len()
|
||
}
|
||
</code></pre>
|
||
<p>首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递<code>&s1</code>给<code>calculate_length</code>,同时在函数定义中,我们获取<code>&String</code>而不是<code>String</code>。</p>
|
||
<p>这些 & 符号就是<strong>引用</strong>,他们允许你使用值但不获取它的所有权。图 4-8 展示了一个图解。</p>
|
||
<p><img alt="&String s pointing at String s1" src="img/trpl04-05.svg" class="center" /></p>
|
||
<p><span class="caption">Figure 4-8: <code>&String s</code> pointing at <code>String s1</code></span></p>
|
||
<p>仔细看看这个函数调用:</p>
|
||
<pre><code class="language-rust"># fn calculate_length(s: &String) -> usize {
|
||
# s.len()
|
||
# }
|
||
let s1 = String::from("hello");
|
||
|
||
let len = calculate_length(&s1);
|
||
</code></pre>
|
||
<p><code>&s1</code>语法允许我们创建一个<strong>参考</strong>值<code>s1</code>的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域它指向的值也不会被丢弃。</p>
|
||
<p>同理,函数签名使用了<code>&</code>来表明参数<code>s</code>的类型是一个引用。让我们增加一些解释性的注解:</p>
|
||
<pre><code class="language-rust">fn calculate_length(s: &String) -> usize { // s is a reference to a String
|
||
s.len()
|
||
} // Here, s goes out of scope. But because it does not have ownership of what
|
||
// it refers to, nothing happens.
|
||
</code></pre>
|
||
<p>变量<code>s</code>有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有它。</p>
|
||
<p>我们将获取引用作为函数参数称为<strong>借用</strong>(<em>borrowing</em>)。正如现实生活中,如果一个人拥有某样东西,你可以从它哪里借来。当你使用完毕,必须还回去。</p>
|
||
<p>如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通!</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let s = String::from("hello");
|
||
|
||
change(&s);
|
||
}
|
||
|
||
fn change(some_string: &String) {
|
||
some_string.push_str(", world");
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 4-9: Attempting to modify a borrowed value</span></p>
|
||
<p>这里是错误:</p>
|
||
<pre><code>error: cannot borrow immutable borrowed content `*some_string` as mutable
|
||
--> error.rs:8:5
|
||
|
|
||
8 | some_string.push_str(", world");
|
||
| ^^^^^^^^^^^
|
||
</code></pre>
|
||
<p>正如变量默认是不可变的,引用也一样。不允许修改引用的值。</p>
|
||
<a class="header" href="#可变引用" name="可变引用"><h3>可变引用</h3></a>
|
||
<p>可以通过一个小调整来修复在列表 4-9 代码中的错误,在列表 4-9 的代码中:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let mut s = String::from("hello");
|
||
|
||
change(&mut s);
|
||
}
|
||
|
||
fn change(some_string: &mut String) {
|
||
some_string.push_str(", world");
|
||
}
|
||
</code></pre>
|
||
<p>首先,必须将<code>s</code>改为<code>mut</code>。然后必须创建一个可变引用<code>&mut s</code>和接受一个可变引用<code>some_string: &mut String</code>。</p>
|
||
<p>不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。这些代码会失败:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">let mut s = String::from("hello");
|
||
|
||
let r1 = &mut s;
|
||
let r2 = &mut s;
|
||
</code></pre>
|
||
<p>具体错误如下:</p>
|
||
<pre><code>error[E0499]: cannot borrow `s` as mutable more than once at a time
|
||
--> borrow_twice.rs:5:19
|
||
|
|
||
4 | let r1 = &mut s;
|
||
| - first mutable borrow occurs here
|
||
5 | let r2 = &mut s;
|
||
| ^ second mutable borrow occurs here
|
||
6 | }
|
||
| - first borrow ends here
|
||
</code></pre>
|
||
<p>这个限制允许可变性,不过是以一种受限制的方式。新 Rustacean 们经常与此作斗争,因为大部分语言任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争(data races)。</p>
|
||
<p><strong>数据竞争</strong>是一种特定类型的竞争状态,它可由这三个行为造成:</p>
|
||
<ol>
|
||
<li>两个或更多指针同时访问同一数据。</li>
|
||
<li>至少有一个指针被写入。</li>
|
||
<li>没有同步数据访问的机制。</li>
|
||
</ol>
|
||
<p>数据竞争会导致未定义行为,在运行时难以追踪,并且难以诊断和修复;Rust 避免了这种情况,它拒绝编译存在数据竞争的代码!</p>
|
||
<p>一如既往,可以使用大括号来创建一个新的作用域来允许拥有多个可变引用,只是不能<strong>同时</strong>拥有:</p>
|
||
<pre><code class="language-rust">let mut s = String::from("hello");
|
||
|
||
{
|
||
let r1 = &mut s;
|
||
|
||
} // r1 goes out of scope here, so we can make a new reference with no problems.
|
||
|
||
let r2 = &mut s;
|
||
</code></pre>
|
||
<p>当结合可变和不可变引用时有一个类似的规则存在。这些代码会导致一个错误:</p>
|
||
<pre><code class="language-rust,ignore">let mut s = String::from("hello");
|
||
|
||
let r1 = &s; // no problem
|
||
let r2 = &s; // no problem
|
||
let r3 = &mut s; // BIG PROBLEM
|
||
</code></pre>
|
||
<p>错误如下:</p>
|
||
<pre><code>error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
|
||
immutable
|
||
--> borrow_thrice.rs:6:19
|
||
|
|
||
4 | let r1 = &s; // no problem
|
||
| - immutable borrow occurs here
|
||
5 | let r2 = &s; // no problem
|
||
6 | let r3 = &mut s; // BIG PROBLEM
|
||
| ^ mutable borrow occurs here
|
||
7 | }
|
||
| - immutable borrow ends here
|
||
</code></pre>
|
||
<p>哇哦!我们<strong>也</strong>不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个读取数据的人有能力影响其他人读取到的数据。</p>
|
||
<p>即使这些错误有时是使人沮丧的。记住这是 Rust 编译器在提早指出一个潜在的 bug(在编译时而不是运行时)并明确告诉你问题在哪而不是任由你去追踪为何有时数据并不是你想象中的那样。</p>
|
||
<a class="header" href="#悬垂引用" name="悬垂引用"><h3>悬垂引用</h3></a>
|
||
<p>在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个<strong>悬垂指针</strong>(<em>dangling pointer</em>),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。</p>
|
||
<p>让我们尝试创建一个悬垂引用:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let reference_to_nothing = dangle();
|
||
}
|
||
|
||
fn dangle() -> &String {
|
||
let s = String::from("hello");
|
||
|
||
&s
|
||
}
|
||
</code></pre>
|
||
<p>这里是错误:</p>
|
||
<pre><code>error[E0106]: missing lifetime specifier
|
||
--> dangle.rs:5:16
|
||
|
|
||
5 | fn dangle() -> &String {
|
||
| ^^^^^^^
|
||
|
|
||
= help: this function's return type contains a borrowed value, but there is no
|
||
value for it to be borrowed from
|
||
= help: consider giving it a 'static lifetime
|
||
|
||
error: aborting due to previous error
|
||
</code></pre>
|
||
<p>错误信息引用了一个我们还未涉及到的功能:<strong>生命周期</strong>(<em>lifetimes</em>)。第十章会详细介绍生命周期。不过,如果你不理会生命周期的部分,错误信息确实包含了为什么代码是有问题的关键:</p>
|
||
<pre><code>this function's return type contains a borrowed value, but there is no value
|
||
for it to be borrowed from.
|
||
</code></pre>
|
||
<p>让我们仔细看看我们的<code>dangle</code>代码的每一步到底放生了什么:</p>
|
||
<pre><code class="language-rust,ignore">fn dangle() -> &String { // dangle returns a reference to a String
|
||
|
||
let s = String::from("hello"); // s is a new String
|
||
|
||
&s // we return a reference to the String, s
|
||
} // Here, s goes out of scope, and is dropped. Its memory goes away.
|
||
// Danger!
|
||
</code></pre>
|
||
<p>因为<code>s</code>是在<code>dangle</code>创建的,当<code>dangle</code>的代码执行完毕后,<code>s</code>将被释放。不过我们尝试返回一个它的引用。这意味着这个引用会指向一个无效的<code>String</code>!这可不好。Rust 不会允许我们这么做的。</p>
|
||
<p>这里的解决方法是直接返回<code>String</code>:</p>
|
||
<pre><code class="language-rust">fn no_dangle() -> String {
|
||
let s = String::from("hello");
|
||
|
||
s
|
||
}
|
||
</code></pre>
|
||
<p>这样就可以没有任何错误的运行了。所有权被移动出去,所以没有值被释放掉。</p>
|
||
<a class="header" href="#引用的规则" name="引用的规则"><h3>引用的规则</h3></a>
|
||
<p>简要的概括一下对引用的讨论:</p>
|
||
<ol>
|
||
<li>在任意给定时间,<strong>只能</strong>拥有如下中的一个:</li>
|
||
</ol>
|
||
<ul>
|
||
<li>一个可变引用。</li>
|
||
<li>任意数量的不可变引用。</li>
|
||
</ul>
|
||
<ol start="2">
|
||
<li>引用必须总是有效的。</li>
|
||
</ol>
|
||
<p>接下来,我们来看看一种不同类型的引用:slice。</p>
|
||
<a class="header" href="#slices" name="slices"><h2>Slices</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-03-slices.md">ch04-03-slices.md</a>
|
||
<br>
|
||
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56</p>
|
||
</blockquote>
|
||
<p>另一个没有所有权的数据类型是 <em>slice</em>。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。</p>
|
||
<p>这里有一个小的编程问题:编写一个获取一个字符串并返回它在其中找到的第一个单词的函数。如果函数没有在字符串中找到一个空格,就意味着整个字符串是一个单词,所以整个字符串都应该返回。</p>
|
||
<p>让我们看看这个函数的签名:</p>
|
||
<pre><code class="language-rust,ignore">fn first_word(s: &String) -> ?
|
||
</code></pre>
|
||
<p><code>first_word</code>这个函数有一个参数<code>&String</code>。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取<strong>部分</strong>字符串的办法。不过,我们可以返回单词结尾的索引。让我们试试如列表 4-10 所示的代码:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn first_word(s: &String) -> usize {
|
||
let bytes = s.as_bytes();
|
||
|
||
for (i, &item) in bytes.iter().enumerate() {
|
||
if item == b' ' {
|
||
return i;
|
||
}
|
||
}
|
||
|
||
s.len()
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 4-10: The <code>first_word</code> function that returns a
|
||
byte index value into the <code>String</code> parameter</span></p>
|
||
<p>让我们将代码分解成小块。因为需要一个元素一个元素的检查<code>String</code>中的值是否是空格,需要用<code>as_bytes</code>方法将<code>String</code>转化为字节数组:</p>
|
||
<pre><code class="language-rust,ignore">let bytes = s.as_bytes();
|
||
</code></pre>
|
||
<p>接下来,使用<code>iter</code>方法在字节数据上创建一个迭代器:</p>
|
||
<pre><code class="language-rust,ignore">for (i, &item) in bytes.iter().enumerate() {
|
||
</code></pre>
|
||
<p>第十三章将讨论迭代器的更多细节。现在,只需知道<code>iter</code>方法返回集合中的每一个元素,而<code>enumerate</code>包装<code>iter</code>的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。</p>
|
||
<p>因为<code>enumerate</code>方法返回一个元组,我们可以使用模式来解构它,就像 Rust 中其他地方一样。所以在<code>for</code>循环中,我们指定了一个模式,其中<code>i</code>是元组中的索引而<code>&item</code>是单个字节。因为从<code>.iter().enumerate()</code>中获取了集合元素的引用,我们在模式中使用了<code>&</code>。</p>
|
||
<p>我们通过字节的字面值来寻找代表空格的字节。如果找到了,返回它的位置。否则,使用<code>s.len()</code>返回字符串的长度:</p>
|
||
<pre><code class="language-rust,ignore"> if item == b' ' {
|
||
return i;
|
||
}
|
||
}
|
||
s.len()
|
||
</code></pre>
|
||
<p>现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单单一个<code>usize</code>,不过它只在<code>&String</code>的上下文中才是一个有意义的数字。换句话说,因为它是一个与<code>String</code>相分离的值,无法保证将来它仍然有效。考虑一下列表 4-11 中使用了列表 4-10 <code>first_word</code>函数的程序:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust"># fn first_word(s: &String) -> usize {
|
||
# let bytes = s.as_bytes();
|
||
#
|
||
# for (i, &item) in bytes.iter().enumerate() {
|
||
# if item == b' ' {
|
||
# return i;
|
||
# }
|
||
# }
|
||
#
|
||
# s.len()
|
||
# }
|
||
#
|
||
fn main() {
|
||
let mut s = String::from("hello world");
|
||
|
||
let word = first_word(&s); // word will get the value 5.
|
||
|
||
s.clear(); // This empties the String, making it equal to "".
|
||
|
||
// word still has the value 5 here, but there's no more string that
|
||
// we could meaningfully use the value 5 with. word is now totally invalid!
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 4-11: Storing the result from calling the
|
||
<code>first_word</code> function then changing the <code>String</code> contents</span></p>
|
||
<p>这个程序编译时没有任何错误,而且在调用<code>s.clear()</code>之后使用<code>word</code>也不会出错。这时<code>word</code>与<code>s</code>状态就没有联系了,所以<code>word</code>仍然包含值<code>5</code>。可以尝试用值<code>5</code>来提取变量<code>s</code>的第一个单词,不过这是有 bug 的,因为在我们将<code>5</code>保存到<code>word</code>之后<code>s</code>的内容已经改变。</p>
|
||
<p>不得不担心<code>word</code>的索引与<code>s</code>中的数据不再同步是乏味且容易出错的!如果编写一个<code>second_word</code>函数的话管理索引将更加容易出问题。它的签名看起来像这样:</p>
|
||
<pre><code class="language-rust,ignore">fn second_word(s: &String) -> (usize, usize) {
|
||
</code></pre>
|
||
<p>现在我们跟踪了一个开始索引<strong>和</strong>一个结尾索引,同时有了更多从数据的某个特定状态计算而来的值,他们也完全没有与这个状态相关联。现在有了三个飘忽不定的不相关变量都需要被同步。</p>
|
||
<p>幸运的是,Rust 为这个问题提供了一个解决方案:字符串 slice。</p>
|
||
<a class="header" href="#字符串-slice" name="字符串-slice"><h3>字符串 slice</h3></a>
|
||
<p><strong>字符串 slice</strong>(<em>string slice</em>)是<code>String</code>中一部分值的引用,它看起来像这样:</p>
|
||
<pre><code class="language-rust">let s = String::from("hello world");
|
||
|
||
let hello = &s[0..5];
|
||
let world = &s[6..11];
|
||
</code></pre>
|
||
<p>这类似于获取整个<code>String</code>的引用不过带有额外的<code>[0..5]</code>部分。不同于整个<code>String</code>的引用,这是一个包含<code>String</code>内部的一个位置和所需元素数量的引用。</p>
|
||
<p>我们使用一个 range <code>[starting_index..ending_index]</code>来创建 slice,不过 slice 的数据结构实际上储存了开始位置和 slice 的长度。所以就<code>let world = &s[6..11];</code>来说,<code>world</code>将是一个包含指向<code>s</code>第 6 个字节的指针和长度值 5 的 slice。</p>
|
||
<p>图 4-12 展示了一个图例</p>
|
||
<p><img alt="world containing a pointer to the 6th byte of String s and a length 5" src="img/trpl04-06.svg" class="center" style="width: 50%;" /></p>
|
||
<p><span class="caption">Figure 4-12: String slice referring to part of a
|
||
<code>String</code></span></p>
|
||
<p>对于 Rust 的<code>..</code> range 语法,如果想要从第一个索引(0)开始,可以不写两个点号之前的值。换句话说,如下两个语句是相同的:</p>
|
||
<pre><code class="language-rust">let s = String::from("hello");
|
||
|
||
let slice = &s[0..2];
|
||
let slice = &s[..2];
|
||
</code></pre>
|
||
<p>由此类推,如果 slice 包含<code>String</code>的最后一个字节,也可以舍弃尾部的数字。这意味着如下也是相同的:</p>
|
||
<pre><code class="language-rust">let s = String::from("hello");
|
||
|
||
let len = s.len();
|
||
|
||
let slice = &s[0..len];
|
||
let slice = &s[..];
|
||
</code></pre>
|
||
<p>在记住所有这些知识后,让我们重写<code>first_word</code>来返回一个 slice。“字符串 slice”的签名写作<code>&str</code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn first_word(s: &String) -> &str {
|
||
let bytes = s.as_bytes();
|
||
|
||
for (i, &item) in bytes.iter().enumerate() {
|
||
if item == b' ' {
|
||
return &s[0..i];
|
||
}
|
||
}
|
||
|
||
&s[..]
|
||
}
|
||
</code></pre>
|
||
<p>我们使用跟列表 4-10 相同的方式获取单词结尾的索引,通过寻找第一个出现的空格。当我们找到一个空格,我们返回一个索引,它使用字符串的开始和空格的索引来作为开始和结束的索引。</p>
|
||
<p>现在当调用<code>first_word</code>时,会返回一个单独的与底层数据相联系的值。这个值由一个 slice 开始位置的引用和 slice 中元素的数量组成。</p>
|
||
<p><code>second_word</code>函数也可以改为返回一个 slice:</p>
|
||
<pre><code class="language-rust,ignore">fn second_word(s: &String) -> &str {
|
||
</code></pre>
|
||
<p>现在我们有了一个不易混杂的直观的 API 了,因为编译器会确保指向<code>String</code>的引用保持有效。还记得列表 4-11 程序中,那个当我们获取第一个单词结尾的索引不过接着就清除了字符串所以索引就无效了的 bug 吗?那些代码逻辑上时不正确的,不过却没有任何直观的错误。问题会在之后尝试对空字符串使用第一个单词的索引时出现。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的<code>first_word</code>会抛出一个编译时错误:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let mut s = String::from("hello world");
|
||
|
||
let word = first_word(&s);
|
||
|
||
s.clear(); // Error!
|
||
}
|
||
</code></pre>
|
||
<p>这里是编译错误:</p>
|
||
<pre><code>17:6 error: cannot borrow `s` as mutable because it is also borrowed as
|
||
immutable [E0502]
|
||
s.clear(); // Error!
|
||
^
|
||
15:29 note: previous borrow of `s` occurs here; the immutable borrow prevents
|
||
subsequent moves or mutable borrows of `s` until the borrow ends
|
||
let word = first_word(&s);
|
||
^
|
||
18:2 note: previous borrow ends here
|
||
fn main() {
|
||
|
||
}
|
||
^
|
||
</code></pre>
|
||
<p>回忆一下借用规则,当拥有某值的不可变引用时。不能再获取一个可变引用。因为<code>clear</code>需要清空<code>String</code>,它尝试获取一个可变引用,它失败了。Rust 不仅使得我们的 API 简单易用,也在编译时就消除了一整个错误类型!</p>
|
||
<a class="header" href="#字符串字面值就是-slice" name="字符串字面值就是-slice"><h4>字符串字面值就是 slice</h4></a>
|
||
<p>还记得我们讲到过字符串字面值被储存在二进制文件中吗。现在知道 slice 了,我们就可以正确的理解字符串字面值了:</p>
|
||
<pre><code class="language-rust">let s = "Hello, world!";
|
||
</code></pre>
|
||
<p>这里<code>s</code>的类型是<code>&str</code>:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;<code>&str</code>是一个不可变引用。</p>
|
||
<a class="header" href="#字符串-slice-作为参数" name="字符串-slice-作为参数"><h4>字符串 slice 作为参数</h4></a>
|
||
<p>在知道了能够获取字面值和<code>String</code>的 slice 后引起了另一个对<code>first_word</code>的改进,这是它的签名:</p>
|
||
<pre><code class="language-rust,ignore">fn first_word(s: &String) -> &str {
|
||
</code></pre>
|
||
<p>相反一个更有经验的 Rustacean 会写下如下这一行,因为它使得可以对<code>String</code>和<code>&str</code>使用相同的函数:</p>
|
||
<pre><code class="language-rust,ignore">fn first_word(s: &str) -> &str {
|
||
</code></pre>
|
||
<p>如果有一个字符串 slice,可以直接传递它。如果有一个<code>String</code>,则可以传递整个<code>String</code>的 slice。定义一个获取字符串 slice 而不是字符串引用的函数使得我们的 API 更加通用并且不会丢失任何功能:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust"># fn first_word(s: &str) -> &str {
|
||
# let bytes = s.as_bytes();
|
||
#
|
||
# for (i, &item) in bytes.iter().enumerate() {
|
||
# if item == b' ' {
|
||
# return &s[0..i];
|
||
# }
|
||
# }
|
||
#
|
||
# &s[..]
|
||
# }
|
||
fn main() {
|
||
let my_string = String::from("hello world");
|
||
|
||
// first_word works on slices of `String`s
|
||
let word = first_word(&my_string[..]);
|
||
|
||
let my_string_literal = "hello world";
|
||
|
||
// first_word works on slices of string literals
|
||
let word = first_word(&my_string_literal[..]);
|
||
|
||
// since string literals *are* string slices already,
|
||
// this works too, without the slice syntax!
|
||
let word = first_word(my_string_literal);
|
||
}
|
||
</code></pre>
|
||
<a class="header" href="#其他-slice" name="其他-slice"><h3>其他 slice</h3></a>
|
||
<p>字符串 slice,正如你想象的那样,是针对字符串的。不过也有更通用的 slice 类型。考虑一下这个数组:</p>
|
||
<pre><code class="language-rust">let a = [1, 2, 3, 4, 5];
|
||
</code></pre>
|
||
<p>就跟我们想要获取字符串的一部分那样,我们也会想要引用数组的一部分,而我们可以这样做:</p>
|
||
<pre><code class="language-rust">let a = [1, 2, 3, 4, 5];
|
||
|
||
let slice = &a[1..3];
|
||
</code></pre>
|
||
<p>这个 slice 的类型是<code>&[i32]</code>。它跟以跟字符串 slice 一样的方式工作,通过储存第一个元素的引用和一个长度。你可以对其他所有类型的集合使用这类 slice。第八章讲到 vector 时会详细讨论这些集合。</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>所有权、借用和 slice 这些概念是 Rust 何以在编译时保障内存安全的关键所在。Rust 像其他系统编程语言那样给予你对内存使用的控制,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。</p>
|
||
<p>所有权系统影响了 Rust 中其他很多部分如何工作,所以我们会继续讲到这些概念,贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个<code>struct</code>中。</p>
|
||
<a class="header" href="#结构体" name="结构体"><h1>结构体</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch05-00-structs.md">ch05-00-structs.md</a>
|
||
<br>
|
||
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56</p>
|
||
</blockquote>
|
||
<p><code>struct</code>,是 <em>structure</em> 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,<code>struct</code>就像对象中的数据属性(字段)。在这一章的下一部分会讲到如何在结构体上定义方法;方法是如何为结构体数据指定<strong>行为</strong>的函数。<code>struct</code>和<code>enum</code>(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查来在程序范围内创建新类型的基本组件。</p>
|
||
<p>对结构体的一种看法是他们与元组类似,这个我们在第三章讲过了。就像元组,结构体的每一部分可以是不同类型。可以命名各部分数据以便能更清楚的知道其值的意义。由于有了这些名字使得结构体更灵活:不需要依赖顺序来指定或访问实例中的值。</p>
|
||
<p>为了定义结构体,通过<code>struct</code>关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作<strong>字段</strong>(<em>field</em>),并定义字段类型。例如,列表 5-1 展示了一个储存用户账号信息的结构体:</p>
|
||
<pre><code class="language-rust">struct User {
|
||
username: String,
|
||
email: String,
|
||
sign_in_count: u64,
|
||
active: bool,
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 5-1: A <code>User</code> struct definition</span></p>
|
||
<p>一旦定义了结构体后为了使用它,通过为每个字段指定具体值来创建这个结构体的<strong>实例</strong>。创建一个实例需要以结构体的名字开头,接着在大括号中使用<code>key: value</code>对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,我们可以像这样来声明一个特定的用户:</p>
|
||
<pre><code class="language-rust"># struct User {
|
||
# username: String,
|
||
# email: String,
|
||
# sign_in_count: u64,
|
||
# active: bool,
|
||
# }
|
||
#
|
||
let user1 = User {
|
||
email: String::from("someone@example.com"),
|
||
username: String::from("someusername123"),
|
||
active: true,
|
||
sign_in_count: 1,
|
||
};
|
||
</code></pre>
|
||
<p>为了从结构体中获取某个值,可以使用点号。如果我们只想要用户的邮箱地址,可以用<code>user1.email</code>。</p>
|
||
<a class="header" href="#结构体数据的所有权" name="结构体数据的所有权"><h2>结构体数据的所有权</h2></a>
|
||
<p>在列表 5-1 中的<code>User</code>结构体的定义中,我们使用了自身拥有所有权的<code>String</code>类型而不是<code>&str</code>字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也应该是有效的。</p>
|
||
<p>可以使结构体储存被其他对象拥有的数据的引用,不过这么做的话需要用上<strong>生命周期</strong>(<em>lifetimes</em>),这是第十章会讨论的一个 Rust 的功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中储存一个引用而不指定生命周期,比如这样:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">struct User {
|
||
username: &str,
|
||
email: &str,
|
||
sign_in_count: u64,
|
||
active: bool,
|
||
}
|
||
|
||
fn main() {
|
||
let user1 = User {
|
||
email: "someone@example.com",
|
||
username: "someusername123",
|
||
active: true,
|
||
sign_in_count: 1,
|
||
};
|
||
}
|
||
</code></pre>
|
||
<p>编译器会抱怨它需要生命周期说明符:</p>
|
||
<pre><code>error[E0106]: missing lifetime specifier
|
||
-->
|
||
|
|
||
2 | username: &str,
|
||
| ^ expected lifetime parameter
|
||
|
||
error[E0106]: missing lifetime specifier
|
||
-->
|
||
|
|
||
3 | email: &str,
|
||
| ^ expected lifetime parameter
|
||
</code></pre>
|
||
<p>第十章会讲到如何修复这个问题以便在结构体中储存引用,不过现在,通过从像<code>&str</code>这样的引用切换到像<code>String</code>这类拥有所有权的类型来修改修改这个错误。</p>
|
||
<a class="header" href="#一个示例程序" name="一个示例程序"><h2>一个示例程序</h2></a>
|
||
<p>为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。</p>
|
||
<p>使用 Cargo 来创建一个叫做 <em>rectangles</em> 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。列表 5-2 中是项目的 <em>src/main.rs</em> 文件中为此实现的一个小程序:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let length1 = 50;
|
||
let width1 = 30;
|
||
|
||
println!(
|
||
"The area of the rectangle is {} square pixels.",
|
||
area(length1, width1)
|
||
);
|
||
}
|
||
|
||
fn area(length: u32, width: u32) -> u32 {
|
||
length * width
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 5-2: Calculating the area of a rectangle
|
||
specified by its length and width in separate variables</span></p>
|
||
<p>尝试使用<code>cargo run</code>运行程序:</p>
|
||
<pre><code>The area of the rectangle is 1500 square pixels.
|
||
</code></pre>
|
||
<a class="header" href="#使用元组重构" name="使用元组重构"><h3>使用元组重构</h3></a>
|
||
<p>我们的小程序能正常运行;它调用<code>area</code>函数用长方形的每个维度来计算出面积。不过我们可以做的更好。长度和宽度是相关联的,因为他们在一起才能定义一个长方形。</p>
|
||
<p>这个做法的问题突显在<code>area</code>的签名上:</p>
|
||
<pre><code class="language-rust,ignore">fn area(length: u32, width: u32) -> u32 {
|
||
</code></pre>
|
||
<p>函数<code>area</code>本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序自身却哪里也没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。</p>
|
||
<p>第三章已经讨论过了一种可行的方法:元组。列表 5-3 是一个使用元组的版本:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let rect1 = (50, 30);
|
||
|
||
println!(
|
||
"The area of the rectangle is {} square pixels.",
|
||
area(rect1)
|
||
);
|
||
}
|
||
|
||
fn area(dimensions: (u32, u32)) -> u32 {
|
||
dimensions.0 * dimensions.1
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 5-3: Specifying the length and width of the
|
||
rectangle with a tuple</span></p>
|
||
<!-- I will add ghosting & wingdings once we're in libreoffice /Carol -->
|
||
<p>在某种程度上说这样好一点了。元组帮助我们增加了一些结构性,现在在调用<code>area</code>的时候只用传递一个参数。不过另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:</p>
|
||
<!-- I will change this to use wingdings instead of repeating this code once
|
||
we're in libreoffice /Carol -->
|
||
<pre><code class="language-rust,ignore">dimensions.0 * dimensions.1
|
||
</code></pre>
|
||
<p>在面积计算时混淆长宽并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引<code>0</code>是<code>length</code>而<code>1</code>是<code>width</code>。如果其他人要使用这些代码,他们也不得不搞清楚后再记住他们。容易忘记或者混淆这些值而造成错误,因为我们没有表明代码中数据的意义。</p>
|
||
<a class="header" href="#使用结构体重构增加更多意义" name="使用结构体重构增加更多意义"><h3>使用结构体重构:增加更多意义</h3></a>
|
||
<p>现在引入结构体的时候了。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如列表 5-4 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">struct Rectangle {
|
||
length: u32,
|
||
width: u32,
|
||
}
|
||
|
||
fn main() {
|
||
let rect1 = Rectangle { length: 50, width: 30 };
|
||
|
||
println!(
|
||
"The area of the rectangle is {} square pixels.",
|
||
area(&rect1)
|
||
);
|
||
}
|
||
|
||
fn area(rectangle: &Rectangle) -> u32 {
|
||
rectangle.length * rectangle.width
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 5-4: Defining a <code>Rectangle</code> struct</span></p>
|
||
<!-- Will add ghosting & wingdings once we're in libreoffice /Carol -->
|
||
<p>这里我们定义了一个结构体并称其为<code>Rectangle</code>。在<code>{}</code>中定义了字段<code>length</code>和<code>width</code>,都是<code>u32</code>类型的。接着在<code>main</code>中,我们创建了一个长度为 50 和宽度为 30 的<code>Rectangle</code>的具体实例。</p>
|
||
<p>函数<code>area</code>现在被定义为接收一个名叫<code>rectangle</code>的参数,它的类型是一个结构体<code>Rectangle</code>实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权这样<code>main</code>函数就可以保持<code>rect1</code>的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有<code>&</code>。</p>
|
||
<p><code>area</code>函数访问<code>Rectangle</code>的<code>length</code>和<code>width</code>字段。<code>area</code>的签名现在明确的表明了我们的意图:通过其<code>length</code>和<code>width</code>字段,计算一个<code>Rectangle</code>的面积,。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值<code>0</code>和<code>1</code>。结构体胜在更清晰明了。</p>
|
||
<a class="header" href="#通过衍生-trait-增加实用功能" name="通过衍生-trait-增加实用功能"><h3>通过衍生 trait 增加实用功能</h3></a>
|
||
<p>如果能够在调试程序时打印出<code>Rectangle</code>实例来查看其所有字段的值就更好了。列表 5-5 尝试像往常一样使用<code>println!</code>宏:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">struct Rectangle {
|
||
length: u32,
|
||
width: u32,
|
||
}
|
||
|
||
fn main() {
|
||
let rect1 = Rectangle { length: 50, width: 30 };
|
||
|
||
println!("rect1 is {}", rect1);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 5-5: Attempting to print a <code>Rectangle</code>
|
||
instance</span></p>
|
||
<p>如果运行代码,会出现带有如下核心信息的错误:</p>
|
||
<pre><code>error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied
|
||
</code></pre>
|
||
<p><code>println!</code>宏能处理很多类型的格式,不过,<code>{}</code>,默认告诉<code>println!</code>使用称为<code>Display</code>的格式:直接提供给终端用户查看的输出。目前为止见过的基本类型都默认实现了<code>Display</code>,所以它就是向用户展示<code>1</code>或其他任何基本类型的唯一方式。不过对于结构体,<code>println!</code>应该用来输出的格式是不明确的,因为这有更多显示的可能性:是否需要逗号?需要打印出结构体的<code>{}</code>吗?所有字段都应该显示吗?因为这种不确定性,Rust 不尝试猜测我们的意图所以结构体并没有提供一个<code>Display</code>的实现。</p>
|
||
<p>但是如果我们继续阅读错误,将会发现这个有帮助的信息:</p>
|
||
<pre><code>note: `Rectangle` cannot be formatted with the default formatter; try using
|
||
`:?` instead if you are using a format string
|
||
</code></pre>
|
||
<p>让我们来试试!现在<code>println!</code>看起来像<code>println!("rect1 is {:?}", rect1);</code>这样。在<code>{}</code>中加入<code>:?</code>指示符告诉<code>println!</code>我们想要使用叫做<code>Debug</code>的输出格式。<code>Debug</code>是一个 trait,它允许我们在调试代码时以一种对开发者有帮助的方式打印出结构体。</p>
|
||
<p>让我们试试运行这个变化...见鬼了。仍然能看到一个错误:</p>
|
||
<pre><code>error: the trait bound `Rectangle: std::fmt::Debug` is not satisfied
|
||
</code></pre>
|
||
<p>虽然编译器又一次给出了一个有帮助的信息!</p>
|
||
<pre><code>note: `Rectangle` cannot be formatted using `:?`; if it is defined in your
|
||
crate, add `#[derive(Debug)]` or manually implement it
|
||
</code></pre>
|
||
<p>Rust <strong>确实</strong>包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上<code>#[derive(Debug)]</code>注解,如列表 5-6 所示:</p>
|
||
<pre><code class="language-rust">#[derive(Debug)]
|
||
struct Rectangle {
|
||
length: u32,
|
||
width: u32,
|
||
}
|
||
|
||
fn main() {
|
||
let rect1 = Rectangle { length: 50, width: 30 };
|
||
|
||
println!("rect1 is {:?}", rect1);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 5-6: Adding the annotation to derive the <code>Debug</code>
|
||
trait and printing the <code>Rectangle</code> instance using debug formatting</span></p>
|
||
<p>此时此刻运行程序,运行这个程序,不会有任何错误并会出现如下输出:</p>
|
||
<pre><code>rect1 is Rectangle { length: 50, width: 30 }
|
||
</code></pre>
|
||
<p>好极了!这不是最漂亮的输出,不过它显示这个实例的所有字段,毫无疑问这对调试有帮助。如果想要输出再好看和易读一点,这对更大的结构体会有帮助,可以将<code>println!</code>的字符串中的<code>{:?}</code>替换为<code>{:#?}</code>。如果在这个例子中使用了美化的调试风格的话,输出会看起来像这样:</p>
|
||
<pre><code>rect1 is Rectangle {
|
||
length: 50,
|
||
width: 30
|
||
}
|
||
</code></pre>
|
||
<p>Rust 为我们提供了很多可以通过<code>derive</code>注解来使用的 trait,他们可以为我们的自定义类型增加有益的行为。这些 trait 和行为在附录 C 中列出。第十章会涉及到如何通过自定义行为来实现这些 trait,同时还有如何创建你自己的 trait。</p>
|
||
<p>我们的<code>area</code>函数是非常明确的————它只是计算了长方形的面积。如果这个行为与<code>Rectangle</code>结构体再结合得更紧密一些就更好了,因为这明显就是<code>Rectangle</code>类型的行为。现在让我们看看如何继续重构这些代码,来将<code>area</code>函数协调进<code>Rectangle</code>类型定义的<code>area</code><strong>方法</strong>中。</p>
|
||
<a class="header" href="#方法语法" name="方法语法"><h2>方法语法</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch05-01-method-syntax.md">ch05-01-method-syntax.md</a>
|
||
<br>
|
||
commit 8c1c1a55d5c0f9bc3c866ee79b267df9dc5c04e2</p>
|
||
</blockquote>
|
||
<p><strong>方法</strong>与函数类似:他们使用<code>fn</code>关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与函数是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十七章讲解)的上下文中被定义,并且他们第一个参数总是<code>self</code>,它代表方法被调用的结构体的实例。</p>
|
||
<a class="header" href="#定义方法" name="定义方法"><h3>定义方法</h3></a>
|
||
<p>让我们将获取一个<code>Rectangle</code>实例作为参数的<code>area</code>函数改写成一个定义于<code>Rectangle</code>结构体上的<code>area</code>方法,如列表 5-7 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">#[derive(Debug)]
|
||
struct Rectangle {
|
||
length: u32,
|
||
width: u32,
|
||
}
|
||
|
||
impl Rectangle {
|
||
fn area(&self) -> u32 {
|
||
self.length * self.width
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
let rect1 = Rectangle { length: 50, width: 30 };
|
||
|
||
println!(
|
||
"The area of the rectangle is {} square pixels.",
|
||
rect1.area()
|
||
);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 5-7: Defining an <code>area</code> method on the <code>Rectangle</code>
|
||
struct</span></p>
|
||
<!-- Will add ghosting and wingdings here in libreoffice /Carol -->
|
||
<p>为了使函数定义于<code>Rectangle</code>的上下文中,我们开始了一个<code>impl</code>块(<code>impl</code>是 <em>implementation</em> 的缩写)。接着将函数移动到<code>impl</code>大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成<code>self</code>。然后在<code>main</code>中将我们调用<code>area</code>方法并传递<code>rect1</code>作为参数的地方,改成使用<strong>方法语法</strong>在<code>Rectangle</code>实例上调用<code>area</code>方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。</p>
|
||
<p>在<code>area</code>的签名中,开始使用<code>&self</code>来替代<code>rectangle: &Rectangle</code>,因为该方法位于<code>impl Rectangle</code> 上下文中所以 Rust 知道<code>self</code>的类型是<code>Rectangle</code>。注意仍然需要在<code>self</code>前面加上<code>&</code>,就像<code>&Rectangle</code>一样。方法可以选择获取<code>self</code>的所有权,像我们这里一样不可变的借用<code>self</code>,或者可变的借用<code>self</code>,就跟其他别的参数一样。</p>
|
||
<p>这里选择<code>&self</code>跟在函数版本中使用<code>&Rectangle</code>出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将第一个参数改为<code>&mut self</code>。通过仅仅使用<code>self</code>作为第一个参数来使方法获取实例的所有权,不过这是很少见的;这通常用在当方法将<code>self</code>转换成别的实例的时候,同时我们想要防止调用者在转换之后使用原始的实例。</p>
|
||
<p>使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复<code>self</code>类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入<code>impl</code>块中,而不是让将来的用户在我们的代码中到处寻找<code>Rectangle</code>的功能。</p>
|
||
<!-- PROD: START BOX -->
|
||
<blockquote>
|
||
<a class="header" href="#-运算符到哪去了" name="-运算符到哪去了"><h3><code>-></code>运算符到哪去了?</h3></a>
|
||
<p>像在 C++ 这样的语言中,有两个不同的运算符来调用方法:<code>.</code>直接在对象上调用方法,而<code>-></code>在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果<code>object</code>是一个指针,那么<code>object->something()</code>就像<code>(*object).something()</code>一样。</p>
|
||
<p>Rust 并没有一个与<code>-></code>等效的运算符;相反,Rust 有一个叫<strong>自动引用和解引用</strong>(<em>automatic referencing and dereferencing</em>)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。</p>
|
||
<p>这是它如何工作的:当使用<code>object.something()</code>调用方法时,Rust 会自动增加<code>&</code>、<code>&mut</code>或<code>*</code>以便使<code>object</code>符合方法的签名。也就是说,这些代码是等同的:</p>
|
||
<pre><code class="language-rust"># #[derive(Debug,Copy,Clone)]
|
||
# struct Point {
|
||
# x: f64,
|
||
# y: f64,
|
||
# }
|
||
#
|
||
# impl Point {
|
||
# fn distance(&self, other: &Point) -> f64 {
|
||
# let x_squared = f64::powi(other.x - self.x, 2);
|
||
# let y_squared = f64::powi(other.y - self.y, 2);
|
||
#
|
||
# f64::sqrt(x_squared + y_squared)
|
||
# }
|
||
# }
|
||
# let p1 = Point { x: 0.0, y: 0.0 };
|
||
# let p2 = Point { x: 5.0, y: 6.5 };
|
||
p1.distance(&p2);
|
||
(&p1).distance(&p2);
|
||
</code></pre>
|
||
<p>第一行看起来简洁的多。这种自动引用的行为之所以能行得通是因为方法有一个明确的接收者————<code>self</code>的类型。在给出接收者和方法名的前提下,Rust 可以明确的计算出方法是仅仅读取(所以需要<code>&self</code>),做出修改(所以是<code>&mut self</code>)或者是获取所有权(所以是<code>self</code>)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统程序员友好性实现的一大部分。</p>
|
||
</blockquote>
|
||
<!-- PROD: END BOX -->
|
||
<a class="header" href="#带有更多参数的方法" name="带有更多参数的方法"><h3>带有更多参数的方法</h3></a>
|
||
<p>让我们更多的实践一下方法,通过为<code>Rectangle</code>结构体实现第二个方法。这回,我们让一个<code>Rectangle</code>的实例获取另一个<code>Rectangle</code>实例并返回<code>self</code>能否完全包含第二个长方形,如果能返回<code>true</code>若不能则返回<code>false</code>。当我们定义了<code>can_hold</code>方法,就可以运行列表 5-8 中的代码了:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let rect1 = Rectangle { length: 50, width: 30 };
|
||
let rect2 = Rectangle { length: 40, width: 10 };
|
||
let rect3 = Rectangle { length: 45, width: 60 };
|
||
|
||
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
|
||
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 5-8: Demonstration of using the as-yet-unwritten
|
||
<code>can_hold</code> method</span></p>
|
||
<p>我们希望看到如下输出,因为<code>rect2</code>的长宽都小于<code>rect1</code>,而<code>rect3</code>比<code>rect1</code>要宽:</p>
|
||
<pre><code>Can rect1 hold rect2? true
|
||
Can rect1 hold rect3? false
|
||
</code></pre>
|
||
<p>因为我们想定义一个方法,所以它应该位于<code>impl Rectangle</code>块中。方法名是<code>can_hold</code>,并且它会获取另一个<code>Rectangle</code>的不可变借用作为参数。通过观察调用点可以看出参数是什么类型的:<code>rect1.can_hold(&rect2)</code>传入了<code>&rect2</code>,它是一个<code>Rectangle</code>的实例<code>rect2</code>的不可变借用。这是可以理解的,因为我们只需要读取<code>rect2</code>(而不是写入,这意味着我们需要一个可变借用)而且希望<code>main</code>保持<code>rect2</code>的所有权这样就可以在调用这个方法后继续使用它。<code>can_hold</code>的返回值是一个布尔值,其实现会分别检查<code>self</code>的长宽是够都大于另一个<code>Rectangle</code>。让我们在列表 5-7 的<code>impl</code>块中增加这个新方法,如列表 5-9 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust"># #[derive(Debug)]
|
||
# struct Rectangle {
|
||
# length: u32,
|
||
# width: u32,
|
||
# }
|
||
#
|
||
impl Rectangle {
|
||
fn area(&self) -> u32 {
|
||
self.length * self.width
|
||
}
|
||
|
||
fn can_hold(&self, other: &Rectangle) -> bool {
|
||
self.length > other.length && self.width > other.width
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 5-9: Implementing the <code>can_hold</code> method on
|
||
<code>Rectangle</code> that takes another <code>Rectangle</code> instance as an argument</span></p>
|
||
<!-- Will add ghosting here in libreoffice /Carol -->
|
||
<p>如果结合列表 5-8 的<code>main</code>函数来运行,就会看到想要得到的输出!方法可以在<code>self</code>后增加多个参数,而且这些参数就像函数中的参数一样工作。</p>
|
||
<a class="header" href="#关联函数" name="关联函数"><h3>关联函数</h3></a>
|
||
<p><code>impl</code>块的另一个好用的功能是:允许在<code>impl</code>块中定义<strong>不</strong>以<code>self</code>作为参数的函数。这被称为<strong>关联函数</strong>(<em>associated functions</em>),因为他们与结构体相关联。即便如此他们也是函数而不是方法,因为他们并不作用于一个结构体的实例。你已经使用过一个关联函数了:<code>String::from</code>。</p>
|
||
<p>关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且用来作为长和宽,这样可以更轻松的创建一个正方形<code>Rectangle</code>而不必指定两次同样的值:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust"># #[derive(Debug)]
|
||
# struct Rectangle {
|
||
# length: u32,
|
||
# width: u32,
|
||
# }
|
||
#
|
||
impl Rectangle {
|
||
fn square(size: u32) -> Rectangle {
|
||
Rectangle { length: size, width: size }
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>使用结构体名和<code>::</code>语法来调用这个关联函数:比如<code>let sq = Rectangle::square(3);</code>。这个方法位于结构体的命名空间中:<code>::</code>语法用于关联函数和模块创建的命名空间,第七章会讲到后者。</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>结构体让我们可以在自己的范围内创建有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名他们来使得代码更清晰。方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。</p>
|
||
<p>结构体并不是创建自定义类型的唯一方法;让我们转向 Rust 的<code>enum</code>功能并为自己的工具箱再填一个工具。</p>
|
||
<a class="header" href="#枚举和模式匹配" name="枚举和模式匹配"><h1>枚举和模式匹配</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-00-enums.md">ch06-00-enums.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p>本章介绍<strong>枚举</strong>,也被称作 <em>enums</em>。枚举允许你通过列举可能的值来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做<code>Option</code>,它代表一个值要么是一些值要么什么都不是。然后会讲到<code>match</code>表达式中的模式匹配如何使对枚举不同的值运行不同的代码变得容易。最后会涉及到<code>if let</code>,另一个简洁方便处理代码中枚举的结构。</p>
|
||
<p>枚举是一个很多语言都有的功能,不过不同语言中的功能各不相同。Rust 的枚举与像F#、OCaml 和 Haskell这样的函数式编程语言中的<strong>代数数据类型</strong>(<em>algebraic data types</em>)最为相似。</p>
|
||
<a class="header" href="#定义枚举" name="定义枚举"><h1>定义枚举</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-01-defining-an-enum.md">ch06-01-defining-an-enum.md</a>
|
||
<br>
|
||
commit e6d6caab41471f7115a621029bd428a812c5260e</p>
|
||
</blockquote>
|
||
<p>让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地址。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:所以可以<strong>枚举</strong>出所有可能的值,这也正是它名字的由来。</p>
|
||
<p>任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值只可能是其中一个成员。IPv4 和 IPv6 从根本上讲仍是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。</p>
|
||
<p>可以通过在代码中定义一个<code>IpAddrKind</code>枚举来表现这个概念并列出可能的 IP 地址类型,<code>V4</code>和<code>V6</code>。这被称为枚举的<strong>成员</strong>(<em>variants</em>):</p>
|
||
<pre><code class="language-rust">enum IpAddrKind {
|
||
V4,
|
||
V6,
|
||
}
|
||
</code></pre>
|
||
<p>现在<code>IpAddrKind</code>就是一个可以在代码中使用的自定义类型了。</p>
|
||
<a class="header" href="#枚举值" name="枚举值"><h3>枚举值</h3></a>
|
||
<p>可以像这样创建<code>IpAddrKind</code>两个不同成员的实例:</p>
|
||
<pre><code class="language-rust"># enum IpAddrKind {
|
||
# V4,
|
||
# V6,
|
||
# }
|
||
#
|
||
let four = IpAddrKind::V4;
|
||
let six = IpAddrKind::V6;
|
||
</code></pre>
|
||
<p>注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在<code>IpAddrKind::V4</code>和<code>IpAddrKind::V6</code>是相同类型的:<code>IpAddrKind</code>。例如,接着我们可以定义一个函数来获取<code>IpAddrKind</code>:</p>
|
||
<pre><code class="language-rust"># enum IpAddrKind {
|
||
# V4,
|
||
# V6,
|
||
# }
|
||
#
|
||
fn route(ip_type: IpAddrKind) { }
|
||
</code></pre>
|
||
<p>现在可以使用任意成员来调用这个函数:</p>
|
||
<pre><code class="language-rust"># enum IpAddrKind {
|
||
# V4,
|
||
# V6,
|
||
# }
|
||
#
|
||
# fn route(ip_type: IpAddrKind) { }
|
||
#
|
||
route(IpAddrKind::V4);
|
||
route(IpAddrKind::V6);
|
||
</code></pre>
|
||
<p>使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址<strong>数据</strong>的方法;只知道它是什么<strong>类型</strong>的。考虑到已经在第五章学习过结构体了,你可以像列表 6-1 那样修改这个问题:</p>
|
||
<pre><code class="language-rust">enum IpAddrKind {
|
||
V4,
|
||
V6,
|
||
}
|
||
|
||
struct IpAddr {
|
||
kind: IpAddrKind,
|
||
address: String,
|
||
}
|
||
|
||
let home = IpAddr {
|
||
kind: IpAddrKind::V4,
|
||
address: String::from("127.0.0.1"),
|
||
};
|
||
|
||
let loopback = IpAddr {
|
||
kind: IpAddrKind::V6,
|
||
address: String::from("::1"),
|
||
};
|
||
</code></pre>
|
||
<p><span class="caption">Listing 6-1: Storing the data and <code>IpAddrKind</code> variant of
|
||
an IP address using a <code>struct</code></span></p>
|
||
<p>这里我们定义了一个有两个字段的结构体<code>IpAddr</code>:<code>kind</code>字段是<code>IpAddrKind</code>(之前定义的枚举)类型的而<code>address</code>字段是<code>String</code>类型的。这里有两个结构体的实例。第一个,<code>home</code>,它的<code>kind</code>的值是<code>IpAddrKind::V4</code>与之相关联的地址数据是<code>127.0.0.1</code>。第二个实例,<code>loopback</code>,<code>kind</code>的值是<code>IpAddrKind</code>的另一个成员,<code>V6</code>,关联的地址是<code>::1</code>。我们使用了要给结构体来将<code>kind</code>和<code>address</code>打包在一起,现在枚举成员就与值相关联了。</p>
|
||
<p>我们可以使用一种更简洁的方式来表达相同的概念,仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。<code>IpAddr</code>枚举的新定义表明了<code>V4</code>和<code>V6</code>成员都关联了<code>String</code>值:</p>
|
||
<pre><code class="language-rust">enum IpAddr {
|
||
V4(String),
|
||
V6(String),
|
||
}
|
||
|
||
let home = IpAddr::V4(String::from("127.0.0.1"));
|
||
|
||
let loopback = IpAddr::V6(String::from("::1"));
|
||
</code></pre>
|
||
<p>我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。</p>
|
||
<p>使用枚举而不是结构体还有另外一个优势:每个成员可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将<code>V4</code>地址储存为四个<code>u8</code>值而<code>V6</code>地址仍然表现为一个<code>String</code>,这就不能使用结构体了。枚举可以轻易处理的这个情况:</p>
|
||
<pre><code class="language-rust">enum IpAddr {
|
||
V4(u8, u8, u8, u8),
|
||
V6(String),
|
||
}
|
||
|
||
let home = IpAddr::V4(127, 0, 0, 1);
|
||
|
||
let loopback = IpAddr::V6(String::from("::1"));
|
||
</code></pre>
|
||
<p>这些代码展示了使用枚举来储存两种不同 IP 地址的几种可能的选择。然而,事实证明储存和编码 IP 地址实在是太常见了<a href="https://doc.rust-lang.org/std/net/enum.IpAddr.html">以致标准库提供了一个可供使用的定义!</a><!-- ignore -->让我们看看标准库如何定义<code>IpAddr</code>的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员中的地址数据嵌入到了两个不同形式的结构体中,他们对不同的成员的定义是不同的:</p>
|
||
<pre><code class="language-rust">struct Ipv4Addr {
|
||
// details elided
|
||
}
|
||
|
||
struct Ipv6Addr {
|
||
// details elided
|
||
}
|
||
|
||
enum IpAddr {
|
||
V4(Ipv4Addr),
|
||
V6(Ipv6Addr),
|
||
}
|
||
</code></pre>
|
||
<p>这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你设想出来的要复杂多少。</p>
|
||
<p>注意虽然标准库中包含一个<code>IpAddr</code>的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。第七章会讲到如何导入类型。</p>
|
||
<p>来看看列表 6-2 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型:</p>
|
||
<pre><code class="language-rust">enum Message {
|
||
Quit,
|
||
Move { x: i32, y: i32 },
|
||
Write(String),
|
||
ChangeColor(i32, i32, i32),
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 6-2: A <code>Message</code> enum whose variants each store
|
||
different amounts and types of values</span></p>
|
||
<p>这个枚举有四个含有不同类型的成员:</p>
|
||
<ul>
|
||
<li><code>Quit</code>没有关联任何数据。</li>
|
||
<li><code>Move</code>包含一个匿名结构体</li>
|
||
<li><code>Write</code>包含单独一个<code>String</code>。</li>
|
||
<li><code>ChangeColor</code>包含三个<code>i32</code>。</li>
|
||
</ul>
|
||
<p>定义一个像列表 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用<code>struct</code>关键字而且所有成员都被组合在一起位于<code>Message</code>下之外。如下这些结构体可以包含与之前枚举成员中相同的数据:</p>
|
||
<pre><code class="language-rust">struct QuitMessage; // unit struct
|
||
struct MoveMessage {
|
||
x: i32,
|
||
y: i32,
|
||
}
|
||
struct WriteMessage(String); // tuple struct
|
||
struct ChangeColorMessage(i32, i32, i32); // tuple struct
|
||
</code></pre>
|
||
<p>不过如果我们使用不同的结构体,他们都有不同的类型,将不能轻易的定义一个获取任何这些信息类型的函数,正如可以使用列表 6-2 中定义的<code>Message</code>枚举那样因为他们是一个类型的。</p>
|
||
<p>结构体和枚举还有另一个相似点:就像可以使用<code>impl</code>来为结构体定义方法那样,也可以在枚举上定义方法。这是一个定义于我们<code>Message</code>枚举上的叫做<code>call</code>的方法:</p>
|
||
<pre><code class="language-rust"># enum Message {
|
||
# Quit,
|
||
# Move { x: i32, y: i32 },
|
||
# Write(String),
|
||
# ChangeColor(i32, i32, i32),
|
||
# }
|
||
#
|
||
impl Message {
|
||
fn call(&self) {
|
||
// method body would be defined here
|
||
}
|
||
}
|
||
|
||
let m = Message::Write(String::from("hello"));
|
||
m.call();
|
||
</code></pre>
|
||
<p>方法体使用了<code>self</code>来获取调用方法的值。这个例子中,创建了一个拥有类型<code>Message::Write("hello")</code>的变量<code>m</code>,而且这就是当<code>m.call()</code>运行时<code>call</code>方法中的<code>self</code>的值。</p>
|
||
<p>让我们看看标准库中的另一个非常常见和实用的枚举:<code>Option</code>。</p>
|
||
<a class="header" href="#option枚举和其相对空值的优势" name="option枚举和其相对空值的优势"><h3><code>Option</code>枚举和其相对空值的优势</h3></a>
|
||
<p>在之前的部分,我们看到了<code>IpAddr</code>枚举如何利用 Rust 的类型系统编码更多信息而不单单是程序中的数据。这一部分探索一个<code>Option</code>的案例分析,它是标准库定义的另一个枚举。<code>Option</code>类型应用广泛因为它编码了一个非常普遍的场景,就是一个值可能是某个值或者什么都不是。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。</p>
|
||
<p>编程语言的设计经常从其包含功能的角度考虑问题,但是从其所没有的功能的角度思考也很重要。Rust 并没有很多其他语言中有的空值功能。<strong>空值</strong>(<em>Null</em> )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。</p>
|
||
<p>在“Null References: The Billion Dollar Mistake”中,Tony Hoare,null 的发明者,曾经说到:</p>
|
||
<blockquote>
|
||
<p>I call it my billion-dollar mistake. At that time, I was designing the first
|
||
comprehensive type system for references in an object-oriented language. My
|
||
goal was to ensure that all use of references should be absolutely safe, with
|
||
checking performed automatically by the compiler. But I couldn't resist the
|
||
temptation to put in a null reference, simply because it was so easy to
|
||
implement. This has led to innumerable errors, vulnerabilities, and system
|
||
crashes, which have probably caused a billion dollars of pain and damage in
|
||
the last forty years.</p>
|
||
<p>我称之为我万亿美元的错误。当时,我在在一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的应有都应该是绝对安全的。不过我未能抗拒引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数以万计美元的苦痛和伤害。</p>
|
||
</blockquote>
|
||
<p>空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。</p>
|
||
<p>然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。</p>
|
||
<p>问题不在于具体的概念而在于特定的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是<code>Option<T></code>,而且它<a href="https://doc.rust-lang.org/std/option/enum.Option.html">定义于标准库中</a><!-- ignore -->,如下:</p>
|
||
<pre><code class="language-rust">enum Option<T> {
|
||
Some(T),
|
||
None,
|
||
}
|
||
</code></pre>
|
||
<p><code>Option<T></code>是如此有用以至于它甚至被包含在了 prelude 之中:不需要显式导入它。另外,它的成员也是如此:可以不需要<code>Option::</code>前缀来直接使用<code>Some</code>和<code>None</code>。即便如此<code>Option<T></code>也仍是常规的枚举,<code>Some(T)</code>和<code>None</code>仍是<code>Option<T></code>的成员。</p>
|
||
<p><code><T></code>语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,第十章会更详细的讲解泛型。目前,所有你需要知道的就是<code><T></code>意味着<code>Option</code>枚举的<code>Some</code>成员可以包含任意类型的数据。这里是一些包含数字类型和字符串类型<code>Option</code>值的例子:</p>
|
||
<pre><code class="language-rust">let some_number = Some(5);
|
||
let some_string = Some("a string");
|
||
|
||
let absent_number: Option<i32> = None;
|
||
</code></pre>
|
||
<p>如果使用<code>None</code>而不是<code>Some</code>,需要告诉 Rust <code>Option<T></code>是什么类型的,因为编译器只通过<code>None</code>值无法推断出<code>Some</code>成员的类型。</p>
|
||
<p>当有一个<code>Some</code>值时,我们就知道存在一个值,而这个值保存在<code>Some</code>中。当有个<code>None</code>值时,在某种意义上它跟空值是相同的意义:并没有一个有效的值。那么,<code>Option<T></code>为什么就比空值要好呢?</p>
|
||
<p>简而言之,因为<code>Option<T></code>和<code>T</code>(这里<code>T</code>可以是任何类型)是不同的类型,编译器不允许像一个被定义的有效的类型那样使用<code>Option<T></code>。例如,这些代码不能编译,因为它尝试将<code>Option<i8></code>与<code>i8</code>相比:</p>
|
||
<pre><code class="language-rust,ignore">let x: i8 = 5;
|
||
let y: Option<i8> = Some(5);
|
||
|
||
let sum = x + y;
|
||
</code></pre>
|
||
<p>如果运行这些代码,将得到类似这样的错误信息:</p>
|
||
<pre><code>error[E0277]: the trait bound `i8: std::ops::Add<std::option::Option<i8>>` is
|
||
not satisfied
|
||
-->
|
||
|
|
||
7 | let sum = x + y;
|
||
| ^^^^^
|
||
|
|
||
</code></pre>
|
||
<p>哇哦!事实上,错误信息意味着 Rust 不知道该如何将<code>Option<i8></code>与<code>i8</code>相加。当在 Rust 中拥有一个像<code>i8</code>这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需判空。只有当使用<code>Option<i8></code>(或者任何用到的类型)是需要担心可能没有一个值,而编译器会确保我们在使用值之前处理为空的情况。</p>
|
||
<p>换句话说,在对<code>Option<T></code>进行<code>T</code>的运算之前必须转为<code>T</code>。通常这能帮助我们捕获空值最常见的问题之一:假设某值不为空但实际上为空的情况。</p>
|
||
<p>无需担心错过存在非空值的假设让我们对代码更加有信心,为了拥有一个可能为空的值,必须显式的将其放入对应类型的<code>Option<T></code>中。接着,当使用这个值时,必须明确的处理值为空的情况。任何地方一个值不是<code>Option<T></code>类型的话,<strong>可以</strong>安全的假设它的值不为空。这是 Rust 的一个有意为之的设计选择,来限制空值的泛滥和增加 Rust 代码的安全性。</p>
|
||
<p>那么当有一个<code>Option<T></code>的值时,如何从<code>Some</code>成员中取出<code>T</code>的值来使用它呢?<code>Option<T></code>枚举拥有大量用于各种情况的方法:你可以查看<a href="https://doc.rust-lang.org/std/option/enum.Option.html">相关代码</a><!-- ignore -->。熟悉<code>Option<T></code>的方法将对你的 Rust 之旅提供巨大的帮助。</p>
|
||
<p>总的来说,为了使用<code>Option<T></code>值,需要编写处理每个成员的代码。我们想要一些代码只当拥有<code>Some(T)</code>值时运行,这些代码允许使用其中的<code>T</code>。也希望一些代码当在<code>None</code>值时运行,这些代码并没有一个可用的<code>T</code>值。<code>match</code>表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。</p>
|
||
<a class="header" href="#match控制流运算符" name="match控制流运算符"><h2><code>match</code>控制流运算符</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-02-match.md">ch06-02-match.md</a>
|
||
<br>
|
||
commit 64090418c23d615facfe49a8d548ad9baea6b097</p>
|
||
</blockquote>
|
||
<p>Rust 有一个叫做<code>match</code>的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据匹配的模式执行代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会涉及到所有不同种类的模式以及他们的作用。<code>match</code>的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。</p>
|
||
<p>把<code>match</code>表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查<code>match</code>的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。</p>
|
||
<p>因为刚刚提到了硬币,让我们用他们来作为一个使用<code>match</code>的例子!我们可以编写一个函数来获取一个未知的(美国)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值,如列表 6-3 中所示:</p>
|
||
<pre><code class="language-rust">enum Coin {
|
||
Penny,
|
||
Nickel,
|
||
Dime,
|
||
Quarter,
|
||
}
|
||
|
||
fn value_in_cents(coin: Coin) -> i32 {
|
||
match coin {
|
||
Coin::Penny => 1,
|
||
Coin::Nickel => 5,
|
||
Coin::Dime => 10,
|
||
Coin::Quarter => 25,
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 6-3: An enum and a <code>match</code> expression that has
|
||
the variants of the enum as its patterns.</span></p>
|
||
<p>拆开<code>value_in_cents</code>函数中的<code>match</code>来看。首先,我们列出<code>match</code>关键字后跟一个表达式,在这个例子中是<code>coin</code>的值。这看起来非常像<code>if</code>使用的表达式,不过这里有一个非常大的区别:对于<code>if</code>,表达式必须返回一个布尔值。而这里它可以是任何类型的。例子中的<code>coin</code>的类型是列表 6-3 中定义的<code>Coin</code>枚举。</p>
|
||
<p>接下来是<code>match</code>的分支。一个分支有两个部分:一个模式和一些代码。第一个分支的模式是值<code>Coin::Penny</code>而之后的<code>=></code>运算符将模式和将要运行的代码分开。这里的代码就仅仅是值<code>1</code>。每一个分支之间使用逗号分隔。</p>
|
||
<p>当<code>match</code>表达式执行时,它将结果值按顺序与每一个分支的模式相比较,如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支,非常像一个硬币分类器。可以拥有任意多的分支:列表 6-3 中的<code>match</code>有四个分支。</p>
|
||
<p>每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个<code>match</code>表达式的返回值。</p>
|
||
<p>如果分支代码较短的话通常不使用大括号,正如列表 6-3 中的每个分支都只是返回一个值。如果想要在分支中运行多行代码,可以使用大括号。例如,如下代码在每次使用<code>Coin::Penny</code>调用时都会打印出“Lucky penny!”,同时仍然返回代码块最后的值,<code>1</code>:</p>
|
||
<pre><code class="language-rust"># enum Coin {
|
||
# Penny,
|
||
# Nickel,
|
||
# Dime,
|
||
# Quarter,
|
||
# }
|
||
#
|
||
fn value_in_cents(coin: Coin) -> i32 {
|
||
match coin {
|
||
Coin::Penny => {
|
||
println!("Lucky penny!");
|
||
1
|
||
},
|
||
Coin::Nickel => 5,
|
||
Coin::Dime => 10,
|
||
Coin::Quarter => 25,
|
||
}
|
||
}
|
||
</code></pre>
|
||
<a class="header" href="#绑定值的模式" name="绑定值的模式"><h3>绑定值的模式</h3></a>
|
||
<p>匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值。</p>
|
||
<p>作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州的每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的<code>enum</code>,通过改变<code>Quarter</code>成员来包含一个<code>State</code>值,列表 6-4 中完成了这些修改:</p>
|
||
<pre><code class="language-rust">#[derive(Debug)] // So we can inspect the state in a minute
|
||
enum UsState {
|
||
Alabama,
|
||
Alaska,
|
||
// ... etc
|
||
}
|
||
|
||
enum Coin {
|
||
Penny,
|
||
Nickel,
|
||
Dime,
|
||
Quarter(UsState),
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 6-4: A <code>Coin</code> enum where the <code>Quarter</code> variant
|
||
also holds a <code>UsState</code> value</span></p>
|
||
<p>想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如果我们的朋友没有的话,他可以把它加入收藏。</p>
|
||
<p>在这些代码的匹配表达式中,我们在匹配<code>Coin::Quarter</code>成员的分支的模式中增加了一个叫做<code>state</code>的变量。当匹配到<code>Coin::Quarter</code>时,变量<code>state</code>将会绑定 25 美分硬币所对应州的值。接着在那个分支的代码中使用<code>state</code>,如下:</p>
|
||
<pre><code class="language-rust"># #[derive(Debug)]
|
||
# enum UsState {
|
||
# Alabama,
|
||
# Alaska,
|
||
# }
|
||
#
|
||
# enum Coin {
|
||
# Penny,
|
||
# Nickel,
|
||
# Dime,
|
||
# Quarter(UsState),
|
||
# }
|
||
#
|
||
fn value_in_cents(coin: Coin) -> i32 {
|
||
match coin {
|
||
Coin::Penny => 1,
|
||
Coin::Nickel => 5,
|
||
Coin::Dime => 10,
|
||
Coin::Quarter(state) => {
|
||
println!("State quarter from {:?}!", state);
|
||
25
|
||
},
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>如果调用<code>value_in_cents(Coin::Quarter(UsState::Alaska))</code>,<code>coin</code>将是<code>Coin::Quarter(UsState::Alaska)</code>。当将值与每个分支相比较时,没有分支会匹配直到遇到<code>Coin::Quarter(state)</code>。这时,<code>state</code>绑定的将会是值<code>UsState::Alaska</code>。接着就可以在<code>println!</code>表达式中使用这个绑定了,像这样就可以获取<code>Coin</code>枚举的<code>Quarter</code>成员中内部的州的值。</p>
|
||
<a class="header" href="#匹配optiont" name="匹配optiont"><h3>匹配<code>Option<T></code></h3></a>
|
||
<p>在之前的部分在使用<code>Option<T></code>时我们想要从<code>Some</code>中取出其内部的<code>T</code>值;也可以像处理<code>Coin</code>枚举那样使用<code>match</code>处理<code>Option<T></code>!与其直接比较硬币,我们将比较<code>Option<T></code>的成员,不过<code>match</code>表达式的工作方式保持不变。</p>
|
||
<p>比如我们想要编写一个函数,它获取一个<code>Option<i32></code>并且如果其中有一个值,将其加一。如果其中没有值,函数应该返回<code>None</code>值并不尝试执行任何操作。</p>
|
||
<p>编写这个函数非常简单,得益于<code>match</code>,它将看起来像列表 6-5 中这样:</p>
|
||
<pre><code class="language-rust">fn plus_one(x: Option<i32>) -> Option<i32> {
|
||
match x {
|
||
None => None,
|
||
Some(i) => Some(i + 1),
|
||
}
|
||
}
|
||
|
||
let five = Some(5);
|
||
let six = plus_one(five);
|
||
let none = plus_one(None);
|
||
</code></pre>
|
||
<p><span class="caption">Listing 6-5: A function that uses a <code>match</code> expression on
|
||
an <code>Option<i32></code></span></p>
|
||
<a class="header" href="#匹配somet" name="匹配somet"><h4>匹配<code>Some(T)</code></h4></a>
|
||
<p>更仔细的检查<code>plus_one</code>的第一行操作。当调用<code>plus_one(five)</code>时,<code>plus_one</code>函数体中的<code>x</code>将会是值<code>Some(5)</code>。接着将其与每个分支比较。</p>
|
||
<pre><code class="language-rust,ignore">None => None,
|
||
</code></pre>
|
||
<p>值<code>Some(5)</code>并不匹配模式<code>None</code>,所以继续进行下一个分支。</p>
|
||
<pre><code class="language-rust,ignore">Some(i) => Some(i + 1),
|
||
</code></pre>
|
||
<p><code>Some(5)</code>与<code>Some(i)</code>匹配吗?为什么不呢!他们是相同的成员。<code>i</code>绑定了<code>Some</code>中包含的值,所以<code>i</code>的值是<code>5</code>。接着匹配分支的代码被执行,所以我们将<code>i</code>的值加一并返回一个含有值<code>6</code>的新<code>Some</code>。</p>
|
||
<a class="header" href="#匹配none" name="匹配none"><h4>匹配<code>None</code></h4></a>
|
||
<p>接着考虑下列表 6-5 中<code>plus_one</code>的第二个调用,这里<code>x</code>是<code>None</code>。我们进入<code>match</code>并与第一个分支相比较。</p>
|
||
<pre><code class="language-rust,ignore">None => None,
|
||
</code></pre>
|
||
<p>匹配上了!这里没有值来加一,所以程序结束并返回<code>=></code>右侧的值<code>None</code>,因为第一个分支就匹配到了,其他的分支将不再比较。</p>
|
||
<p>将<code>match</code>与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:<code>match</code>一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开始有点复杂,不过一旦习惯了,你会希望所有语言都拥有它!这一直是用户的最爱。</p>
|
||
<a class="header" href="#匹配是穷尽的" name="匹配是穷尽的"><h3>匹配是穷尽的</h3></a>
|
||
<p><code>match</code>还有另一方面需要讨论。考虑一下<code>plus_one</code>函数的这个版本:</p>
|
||
<pre><code class="language-rust,ignore">fn plus_one(x: Option<i32>) -> Option<i32> {
|
||
match x {
|
||
Some(i) => Some(i + 1),
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>我们没有处理<code>None</code>的情况,所以这些代码会造成一个 bug。幸运的是,这是一个 Rust 知道如何处理的 bug。如果尝试编译这段代码,会得到这个错误:</p>
|
||
<pre><code>error[E0004]: non-exhaustive patterns: `None` not covered
|
||
-->
|
||
|
|
||
6 | match x {
|
||
| ^ pattern `None` not covered
|
||
</code></pre>
|
||
<p>Rust 知道我们没有覆盖所有可能的情况甚至知道那些模式被忘记了!Rust 中的匹配是<strong>穷尽的</strong>(*exhaustive):必须穷举到最后的可能性来使代码有效。特别的在这个<code>Option<T></code>的例子中,Rust 防止我们忘记明确的处理<code>None</code>的情况,这使我们免于假设拥有一个实际上为空的值,这造成了之前提到过的价值亿万的错误。</p>
|
||
<a class="header" href="#_通配符" name="_通配符"><h3><code>_</code>通配符</h3></a>
|
||
<p>Rust 也提供了一个模式用于不想列举出所有可能值的场景。例如,<code>u8</code>可以拥有 0 到 255 的有效的值,如果我们只关心 1、3、5 和 7 这几个值,就并不想必须列出 0、2、4、6、8、9 一直到 255 的值。所幸我们不必这么做:可以使用特殊的模式<code>_</code>替代:</p>
|
||
<pre><code class="language-rust">let some_u8_value = 0u8;
|
||
match some_u8_value {
|
||
1 => println!("one"),
|
||
3 => println!("three"),
|
||
5 => println!("five"),
|
||
7 => println!("seven"),
|
||
_ => (),
|
||
}
|
||
</code></pre>
|
||
<p><code>_</code>模式会匹配所有的值。通过将其放置于其他分支之后,<code>_</code>将会匹配所有之前没有指定的可能的值。<code>()</code>就是 unit 值,所以<code>_</code>的情况什么也不会发生。因此,可以说我们想要对<code>_</code>通配符之前没有列出的所有可能的值不做任何处理。</p>
|
||
<p>然而,<code>match</code>在只关心<strong>一个</strong>情况的场景中可能就有点啰嗦了。为此 Rust 提供了<code>if let</code>。</p>
|
||
<a class="header" href="#if-let简单控制流" name="if-let简单控制流"><h2><code>if let</code>简单控制流</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-03-if-let.md">ch06-03-if-let.md</a>
|
||
<br>
|
||
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56</p>
|
||
</blockquote>
|
||
<p><code>if let</code>语法让我们以一种不那么冗长的方式结合<code>if</code>和<code>let</code>,来处理匹配一个模式的值而忽略其他的值。考虑列表 6-6 中的程序,它匹配一个<code>Option<u8></code>值并只希望当值是三时执行代码:</p>
|
||
<pre><code class="language-rust">let some_u8_value = Some(0u8);
|
||
match some_u8_value {
|
||
Some(3) => println!("three"),
|
||
_ => (),
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 6-6: A <code>match</code> that only cares about executing
|
||
code when the value is <code>Some(3)</code></span></p>
|
||
<p>我们想要对<code>Some(3)</code>匹配进行操作不过不想处理任何其他<code>Some<u8></code>值或<code>None</code>值。为了满足<code>match</code>表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上<code>_ => ()</code>,这样也要增加很多样板代码。</p>
|
||
<p>不过我们可以使用<code>if let</code>这种更短的方式编写。如下代码与列表 6-6 中的<code>match</code>行为一致:</p>
|
||
<pre><code class="language-rust"># let some_u8_value = Some(0u8);
|
||
if let Some(3) = some_u8_value {
|
||
println!("three");
|
||
}
|
||
</code></pre>
|
||
<p><code>if let</code>获取通过<code>=</code>分隔的一个模式和一个表达式。它的工作方式与<code>match</code>相同,这里的表达式对应<code>match</code>而模式则对应第一个分支。</p>
|
||
<p>使用<code>if let</code>意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去<code>match</code>强制要求的穷尽性检查。<code>match</code>和<code>if let</code>之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。</p>
|
||
<p>换句话说,可以认为<code>if let</code>是<code>match</code>的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。</p>
|
||
<p>可以在<code>if let</code>中包含一个<code>else</code>。<code>else</code>块中的代码与<code>match</code>表达式中的<code>_</code>分支块中的代码相同,这样的<code>match</code>表达式就等同于<code>if let</code>和<code>else</code>。回忆一下列表 6-4 中<code>Coin</code>枚举的定义,它的<code>Quarter</code>成员包含一个<code>UsState</code>值。如果想要计数所有不是 25 美分的硬币的同时也报告 25 美分硬币所属的州,可以使用这样一个<code>match</code>表达式:</p>
|
||
<pre><code class="language-rust"># #[derive(Debug)]
|
||
# enum UsState {
|
||
# Alabama,
|
||
# Alaska,
|
||
# }
|
||
#
|
||
# enum Coin {
|
||
# Penny,
|
||
# Nickel,
|
||
# Dime,
|
||
# Quarter(UsState),
|
||
# }
|
||
# let coin = Coin::Penny;
|
||
let mut count = 0;
|
||
match coin {
|
||
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
|
||
_ => count += 1,
|
||
}
|
||
</code></pre>
|
||
<p>或者可以使用这样的<code>if let</code>和<code>else</code>表达式:</p>
|
||
<pre><code class="language-rust"># #[derive(Debug)]
|
||
# enum UsState {
|
||
# Alabama,
|
||
# Alaska,
|
||
# }
|
||
#
|
||
# enum Coin {
|
||
# Penny,
|
||
# Nickel,
|
||
# Dime,
|
||
# Quarter(UsState),
|
||
# }
|
||
# let coin = Coin::Penny;
|
||
let mut count = 0;
|
||
if let Coin::Quarter(state) = coin {
|
||
println!("State quarter from {:?}!", state);
|
||
} else {
|
||
count += 1;
|
||
}
|
||
</code></pre>
|
||
<p>如果你的程序遇到一个使用<code>match</code>表达起来过于啰嗦的逻辑,记住<code>if let</code>也在你的 Rust 工具箱中。</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>现在我们涉及到了如何使用枚举来创建有一系列可列举值的自定义类型。我们也展示了标准库的<code>Option<T></code>类型是如何帮助你利用类型系统来避免出错。当枚举值包含数据时,你可以根据需要处理多少情况来选择使用<code>match</code>或<code>if let</code>来获取并使用这些值。</p>
|
||
<p>你的 Rust 程序现在能够使用结构体和枚举在自己的作用域内表现其内容了。在你的 API 中使用自定义类型保证了类型安全:编译器会确保你的函数只会得到它期望的类型的值。</p>
|
||
<p>为了向你的用户提供一个组织良好的 API,它使用起来很直观并且只向用户暴露他们确实需要的部分,那么现在就让我们转向 Rust 的模块系统吧。</p>
|
||
<a class="header" href="#模块" name="模块"><h1>模块</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-00-modules.md">ch07-00-modules.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p>在你刚开始编写 Rust 程序时,代码可能仅仅位于<code>main</code>函数里。随着代码数量的增长,最终你会将功能移动到其他函数中,为了复用也为了更好的组织。通过将代码分隔成更小的块,每一个块代码自身就更易于理解。不过当你发现自己有太多的函数了该怎么办呢?Rust 有一个模块系统来处理编写可复用代码同时保持代码组织度的问题。</p>
|
||
<p>就跟你将代码行提取到一个函数中一样,也可以将函数(和其他类似结构体和枚举的代码)提取到不同模块中。<strong>模块</strong>(<em>module</em>)是一个包含函数或类型定义的命名空间,你可以选择这些定义是能(公有)还是不能(私有)在其模块外可见。这是一个模块如何工作的概括:</p>
|
||
<ul>
|
||
<li>使用<code>mod</code>关键字声明模块</li>
|
||
<li>默认所有内容都是私有的(包括模块自身)。可以使用<code>pub</code>关键字将其变成公有并在其命名空间外可见。</li>
|
||
<li><code>use</code>关键字允许引入模块、或模块中的定义到作用域中以便于引用他们。</li>
|
||
</ul>
|
||
<p>我们会逐一了解这每一部分并学习如何将他们结合在一起。</p>
|
||
<a class="header" href="#mod和文件系统" name="mod和文件系统"><h2><code>mod</code>和文件系统</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-01-mod-and-the-filesystem.md">ch07-01-mod-and-the-filesystem.md</a>
|
||
<br>
|
||
commit b0481ac44ff2594c6c240baa36357737739db445</p>
|
||
</blockquote>
|
||
<p>我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章我们见过的<code>rand</code>就是这样的 crate。</p>
|
||
<p>我们将创建一个提供一些通用网络功能的项目的骨架结构;我们将专注于模块和函数的组织,而不担心函数体中的具体代码。这个项目叫做<code>communicator</code>。Cargo 默认会创建一个库 crate 除非指定其他项目类型,所以如果不像一直以来那样加入<code>--bin</code>参数则项目将会是一个库:</p>
|
||
<pre><code>$ cargo new communicator
|
||
$ cd communicator
|
||
</code></pre>
|
||
<p>注意 Cargo 生成了 <em>src/lib.rs</em> 而不是 <em>src/main.rs</em>。在 <em>src/lib.rs</em> 中我们会找到这些:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">#[cfg(test)]
|
||
mod tests {
|
||
#[test]
|
||
fn it_works() {
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>Cargo 创建了一个空的测试来帮助我们开始库项目,不像使用<code>--bin</code>参数那样创建一个“Hello, world!”二进制项目。稍后一点会介绍<code>#[]</code>和<code>mod tests</code>语法,目前只需确保他们位于 <em>src/lib.rs</em> 中。</p>
|
||
<p>因为没有 <em>src/main.rs</em> 文件,所以没有可供 Cargo 的<code>cargo run</code>执行的东西。因此,我们将使用<code>cargo build</code>命令只是编译库 crate 的代码。</p>
|
||
<p>我们将学习根据编写代码的意图来选择不同的织库项目代码组织来适应多种场景。</p>
|
||
<a class="header" href="#模块定义" name="模块定义"><h3>模块定义</h3></a>
|
||
<p>对于<code>communicator</code>网络库,首先要定义一个叫做<code>network</code>的模块,它包含一个叫做<code>connect</code>的函数定义。Rust 中所有模块的定义以关键字<code>mod</code>开始。在 <em>src/lib.rs</em> 文件的开头在测试代码的上面增加这些代码:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">mod network {
|
||
fn connect() {
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><code>mod</code>关键字的后面是模块的名字,<code>network</code>,接着是位于大括号中的代码块。代码块中的一切都位于<code>network</code>命名空间中。在这个例子中,只有一个函数,<code>connect</code>。如果想要在<code>network</code>模块外面的代码中调用这个函数,需要指定模块名并使用命名空间语法<code>::</code>,像这样:<code>network::connect()</code>,而不是只是<code>connect()</code>。</p>
|
||
<p>也可以在 <em>src/lib.rs</em> 文件中同时存在多个模块。例如,再拥有一个<code>client</code>模块,它也有一个叫做<code>connect</code>的函数,如列表 7-1 中所示那样增加这个模块:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">mod network {
|
||
fn connect() {
|
||
}
|
||
}
|
||
|
||
mod client {
|
||
fn connect() {
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 7-1: The <code>network</code> module and the <code>client</code> module
|
||
defined side-by-side in <em>src/lib.rs</em></span></p>
|
||
<p>现在我们有了<code>network::connect</code>函数和<code>client::connect</code>函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。</p>
|
||
<p>虽然在这个例子中,我们构建了一个库,但是 <em>src/lib.rs</em> 并没有什么特殊意义。也可以在 <em>src/main.rs</em> 中使用子模块。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,<code>client</code>模块和它的函数<code>connect</code>可能放在<code>network</code>命名空间里显得更有道理,如列表 7-2 所示:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">mod network {
|
||
fn connect() {
|
||
}
|
||
|
||
mod client {
|
||
fn connect() {
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 7-2: Moving the <code>client</code> module inside of the
|
||
<code>network</code> module</span></p>
|
||
<p>在 <em>src/lib.rs</em> 文件中,将现有的<code>mod network</code>和<code>mod client</code>的定义替换为<code>client</code>模块作为<code>network</code>的一个内部模块。现在我们有了<code>network::connect</code>和<code>network::client::connect</code>函数:又一次,这两个<code>connect</code>函数也不相冲突,因为他们在不同的命名空间中。</p>
|
||
<p>这样,模块之间形成了一个层次结构。<em>src/lib.rs</em> 的内容位于最顶层,而其子模块位于较低的层次。这是列表 7-1 中的例子以这种方式考虑的组织结构:</p>
|
||
<pre><code>communicator
|
||
├── network
|
||
└── client
|
||
</code></pre>
|
||
<p>而这是列表 7-2 中例子的的结构:</p>
|
||
<pre><code>communicator
|
||
└── network
|
||
└── client
|
||
</code></pre>
|
||
<p>可以看到列表 7-2 中,<code>client</code>是<code>network</code>的子模块,而不是它的同级模块。更为负责的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中“符合逻辑”的意义全凭你得理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块将是你会喜欢的结构。</p>
|
||
<a class="header" href="#将模块移动到其他文件" name="将模块移动到其他文件"><h3>将模块移动到其他文件</h3></a>
|
||
<p>位于层级结构中的模块,非常类似计算机领域的另一个我们非常熟悉的结构:文件系统!我们可以利用 Rust 的模块系统连同多个文件一起分解 Rust 项目,这样就不是所有的内容都落到 <em>src/lib.rs</em> 中了。作为例子,我们将从列表 7-3 中的代码开始:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">mod client {
|
||
fn connect() {
|
||
}
|
||
}
|
||
|
||
mod network {
|
||
fn connect() {
|
||
}
|
||
|
||
mod server {
|
||
fn connect() {
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 7-3: Three modules, <code>client</code>, <code>network</code>, and
|
||
<code>network::server</code>, all defined in <em>src/lib.rs</em></span></p>
|
||
<p>这是模块层次结构:</p>
|
||
<pre><code>communicator
|
||
├── client
|
||
└── network
|
||
└── server
|
||
</code></pre>
|
||
<p>如果这些模块有很多函数,而这些函数又很长,将难以在文件中寻找我们需要的代码。因为这些函数被嵌套进一个或多个模块中,同时函数中的代码也会开始变长。这就有充分的理由将<code>client</code>、<code>network</code>和<code>server</code>每一个模块从 <em>src/lib.rs</em> 抽出并放入他们自己的文件中。</p>
|
||
<p>让我们开始把<code>client</code>模块提取到另一个文件中。首先,将 <em>src/lib.rs</em> 中的<code>client</code>模块代码替换为如下:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">mod client;
|
||
|
||
mod network {
|
||
fn connect() {
|
||
}
|
||
|
||
mod server {
|
||
fn connect() {
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>这里我们仍然<strong>定义</strong>了<code>client</code>模块,不过去掉了大括号和<code>client</code>模块中的定义并替换为一个分号,这使得 Rust 知道去其他地方寻找模块中定义的代码。</p>
|
||
<p>那么现在需要创建对应模块名的外部文件。在 <em>src/</em> 目录创建一个 <em>client.rs</em> 文件,接着打开它并输入如下内容,它是上一步<code>client</code>模块中被去掉的<code>connect</code>函数:</p>
|
||
<p><span class="filename">Filename: src/client.rs</span></p>
|
||
<pre><code class="language-rust">fn connect() {
|
||
}
|
||
</code></pre>
|
||
<p>注意这个文件中并不需要一个<code>mod</code>声明;因为已经在 <em>src/lib.rs</em> 中已经使用<code>mod</code>声明了<code>client</code>模块。这个文件仅仅提供<code>client</code>模块的<strong>内容</strong>。如果在这里加上一个<code>mod client</code>,那么就等于给<code>client</code>模块增加了一个叫做<code>client</code>的子模块了!</p>
|
||
<p>Rust 默认只知道 <em>src/lib.rs</em> 中的内容。如果想要对项目加入更多文件,我们需要在 <em>src/lib.rs</em> 中告诉 Rust 去寻找其他文件;这就是为什么<code>mod client</code>需要被定义在 <em>src/lib.rs</em> 而不是在 <em>src/client.rs</em>。</p>
|
||
<p>现在,一切应该能成功编译,虽然会有一些警告。记住使用<code>cargo build</code>而不是<code>cargo run</code>因为这是一个库 crate 而不是二进制 crate:</p>
|
||
<pre><code>$ cargo build
|
||
Compiling communicator v0.1.0 (file:///projects/communicator)
|
||
|
||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||
--> src/client.rs:1:1
|
||
|
|
||
1 | fn connect() {
|
||
| ^
|
||
|
||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||
--> src/lib.rs:4:5
|
||
|
|
||
4 | fn connect() {
|
||
| ^
|
||
|
||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||
--> src/lib.rs:8:9
|
||
|
|
||
8 | fn connect() {
|
||
| ^
|
||
</code></pre>
|
||
<p>这些警告提醒我们有从未被使用的函数。目前不用担心这些警告;在本章的后面会解决他们。好消息是,他们仅仅是警告;我们的项目能够被成功编译。</p>
|
||
<p>下面使用相同的模式将<code>network</code>模块提取到它自己的文件中。删除 <em>src/lib.rs</em> 中<code>network</code>模块的内容并在声明后加上一个分号,像这样:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">mod client;
|
||
|
||
mod network;
|
||
</code></pre>
|
||
<p>接着新建 <em>src/network.rs</em> 文件并输入如下内容:</p>
|
||
<p><span class="filename">Filename: src/network.rs</span></p>
|
||
<pre><code class="language-rust">fn connect() {
|
||
}
|
||
|
||
mod server {
|
||
fn connect() {
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>注意这个模块文件中我们也使用了一个<code>mod</code>声明;这是因为我们希望<code>server</code>成为<code>network</code>的一个子模块。</p>
|
||
<p>现在再次运行<code>cargo build</code>。成功!不过我们还需要再提取出另一个模块:<code>server</code>。因为这是一个子模块——也就是模块中的模块——目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 <em>src/network.rs</em> 的第一个修改是用<code>mod server;</code>替换<code>server</code>模块的内容:</p>
|
||
<p><span class="filename">Filename: src/network.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn connect() {
|
||
}
|
||
|
||
mod server;
|
||
</code></pre>
|
||
<p>接着创建 <em>src/server.rs</em> 文件并输入需要提取的<code>server</code>模块的内容:</p>
|
||
<p><span class="filename">Filename: src/server.rs</span></p>
|
||
<pre><code class="language-rust">fn connect() {
|
||
}
|
||
</code></pre>
|
||
<p>当尝试运行<code>cargo build</code>时,会出现如列表 7-4 中所示的错误:</p>
|
||
<pre><code class="language-text">$ cargo build
|
||
Compiling communicator v0.1.0 (file:///projects/communicator)
|
||
error: cannot declare a new module at this location
|
||
--> src/network.rs:4:5
|
||
|
|
||
4 | mod server;
|
||
| ^^^^^^
|
||
|
|
||
note: maybe move this module `network` to its own directory via `network/mod.rs`
|
||
--> src/network.rs:4:5
|
||
|
|
||
4 | mod server;
|
||
| ^^^^^^
|
||
note: ... or maybe `use` the module `server` instead of possibly redeclaring it
|
||
--> src/network.rs:4:5
|
||
|
|
||
4 | mod server;
|
||
| ^^^^^^
|
||
</code></pre>
|
||
<p><span class="caption">Listing 7-4: Error when trying to extract the <code>server</code>
|
||
submodule into <em>src/server.rs</em></span></p>
|
||
<p>这个错误说明“不能在这个位置新声明一个模块”并指出 <em>src/network.rs</em> 中的<code>mod server;</code>这一行。看来 <em>src/network.rs</em> 与 <em>src/lib.rs</em> 在某些方面是不同的;让我们继续阅读以理解这是为什么。</p>
|
||
<p>列表 7-4 中间的记录事实上是非常有帮助的,因为它指出了一些我们还未讲到的操作:</p>
|
||
<pre><code>note: maybe move this module `network` to its own directory via `network/mod.rs`
|
||
</code></pre>
|
||
<p>我们可以按照记录所建议的去操作,而不是继续使用之前的与模块同名的文件的模式:</p>
|
||
<ol>
|
||
<li>新建一个叫做 <em>network</em> 的<strong>目录</strong>,这是父模块的名字</li>
|
||
<li>将 <em>src/network.rs</em> 移动到新建的 <em>network</em> 目录中并重命名,现在它是 <em>src/network/mod.rs</em></li>
|
||
<li>将子模块文件 <em>src/server.rs</em> 移动到 <em>network</em> 目录中</li>
|
||
</ol>
|
||
<p>如下是执行这些步骤的命令:</p>
|
||
<pre><code class="language-sh">$ mkdir src/network
|
||
$ mv src/network.rs src/network/mod.rs
|
||
$ mv src/server.rs src/network
|
||
</code></pre>
|
||
<p>现在如果运行<code>cargo build</code>的话将顺利编译(虽然仍有警告)。现在模块的布局看起来仍然与列表 7-3 中所有代码都在 <em>src/lib.rs</em> 中时完全一样:</p>
|
||
<pre><code>communicator
|
||
├── client
|
||
└── network
|
||
└── server
|
||
</code></pre>
|
||
<p>对应的文件布局现在看起来像这样:</p>
|
||
<pre><code>├── src
|
||
│ ├── client.rs
|
||
│ ├── lib.rs
|
||
│ └── network
|
||
│ ├── mod.rs
|
||
│ └── server.rs
|
||
</code></pre>
|
||
<p>那么,当我们想要提取<code>network::server</code>模块时,为什么也必须将 <em>src/network.rs</em> 文件改名成 <em>src/network/mod.rs</em> 文件呢,还有为什么要将<code>network::server</code>的代码放入 <em>network</em> 目录的 <em>src/network/server.rs</em> 文件中,而不能将<code>network::server</code>模块提取到 <em>src/server.rs</em> 中呢?原因是如果 <em>server.rs</em> 文件在 <em>src</em> 目录中那么 Rust 就不能知道<code>server</code>应当是<code>network</code>的子模块。为了更清楚得说明为什么 Rust 不知道,让我们考虑一下有着如下层级的另一个例子,它的所有定义都位于 <em>src/lib.rs</em> 中:</p>
|
||
<pre><code>communicator
|
||
├── client
|
||
└── network
|
||
└── client
|
||
</code></pre>
|
||
<p>在这个例子中,仍然有这三个模块,<code>client</code>、<code>network</code>和<code>network::client</code>。如果按照与上面最开始将模块提取到文件中相同的步骤来操作,对于<code>client</code>模块会创建 <em>src/client.rs</em>。对于<code>network</code>模块,会创建 <em>src/network.rs</em>。但是接下来不能将<code>network::client</code>模块提取到 <em>src/client.rs</em> 文件中,因为它已经存在了,对应顶层的<code>client</code>模块!如果将<code>client</code>和<code>network::client</code>的代码都放入 <em>src/client.rs</em> 文件,Rust 将无从可知这些代码是属于<code>client</code>还是<code>network::client</code>的。</p>
|
||
<p>因此,一旦想要将<code>network</code>模块的子模块<code>network::client</code>提取到一个文件中,需要为<code>network</code>模块新建一个目录替代 <em>src/network.rs</em> 文件。接着<code>network</code>模块的代码将进入 <em>src/network/mod.rs</em> 文件,而子模块<code>network::client</code>将拥有其自己的文件 <em>src/network/client.rs</em>。现在顶层的 <em>src/client.rs</em> 中的代码毫无疑问的都属于<code>client</code>模块。</p>
|
||
<a class="header" href="#模块文件系统的规则" name="模块文件系统的规则"><h3>模块文件系统的规则</h3></a>
|
||
<p>与文件系统相关的模块规则总结如下:</p>
|
||
<ul>
|
||
<li>如果一个叫做<code>foo</code>的模块没有子模块,应该将<code>foo</code>的声明放入叫做 <em>foo.rs</em> 的文件中。</li>
|
||
<li>如果一个叫做<code>foo</code>的模块有子模块,应该将<code>foo</code>的声明放入叫做 <em>foo/mod.rs</em> 的文件中。</li>
|
||
</ul>
|
||
<p>这些规则适用于递归(嵌套),所以如果<code>foo</code>模块有一个子模块<code>bar</code>而<code>bar</code>没有子模块,则 <em>src</em> 目录中应该有如下文件:</p>
|
||
<pre><code>├── foo
|
||
│ ├── bar.rs (contains the declarations in `foo::bar`)
|
||
│ └── mod.rs (contains the declarations in `foo`, including `mod bar`)
|
||
</code></pre>
|
||
<p>模块自身则应该使用<code>mod</code>关键字定义于父模块的文件中。</p>
|
||
<p>接下来,我们讨论一下<code>pub</code>关键字,并除掉那些警告!</p>
|
||
<a class="header" href="#使用pub控制可见性" name="使用pub控制可见性"><h2>使用<code>pub</code>控制可见性</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-02-controlling-visibility-with-pub.md">ch07-02-controlling-visibility-with-pub.md</a>
|
||
<br>
|
||
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56</p>
|
||
</blockquote>
|
||
<p>我们通过将<code>network</code>和<code>network::server</code>的代码分别移动到 <em>src/network/mod.rs</em> 和 <em>src/network/server.rs</em> 文件中解决了列表 7-4 中出现的错误信息。现在,<code>cargo build</code>能够构建我们的项目,不过仍然有一些警告信息,表示<code>client::connect</code>、<code>network::connect</code>和<code>network::server::connect</code>函数没有被使用:</p>
|
||
<pre><code>warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||
src/client.rs:1:1
|
||
|
|
||
1 | fn connect() {
|
||
| ^
|
||
|
||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||
--> src/network/mod.rs:1:1
|
||
|
|
||
1 | fn connect() {
|
||
| ^
|
||
|
||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||
--> src/network/server.rs:1:1
|
||
|
|
||
1 | fn connect() {
|
||
| ^
|
||
</code></pre>
|
||
<p>那么为什么会出现这些错误信息呢?我们构建的是一个库,它的函数的目的是被<strong>用户</strong>使用,而不一定要被项目自身使用,所以不应该担心这些<code>connect</code>函数是未使用的。创建他们的意义就在于被另一个项目而不是被自己使用。</p>
|
||
<p>为了理解为什么这个程序出现了这些警告,尝试作为另一个项目来使用这个<code>connect</code>库,从外部调用他们。为此,通过创建一个包含这些代码的 <em>src/main.rs</em> 文件,在与库 crate 相同的目录创建一个二进制 crate:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">extern crate communicator;
|
||
|
||
fn main() {
|
||
communicator::client::connect();
|
||
}
|
||
</code></pre>
|
||
<p>使用<code>extern crate</code>指令将<code>communicator</code>库 crate 引入到作用域,因为事实上我们的包包含<strong>两个</strong> crate。Cargo 认为 <em>src/main.rs</em> 是一个二进制 crate 的根文件,与现存的以 <em>src/lib.rs</em> 为根文件的库 crate 相区分。这个模式在可执行项目中非常常见:大部分功能位于库 crate 中,而二进制 crate 使用这个库 crate。通过这种方式,其他程序也可以使用这个库 crate,这是一个很好的关注分离(separation of concerns)。</p>
|
||
<p>从一个外部 crate 的视角观察<code>communicator</code>库的内部,我们创建的所有模块都位于一个与 crate 同名的模块内部,<code>communicator</code>。这个顶层的模块被称为 crate 的<strong>根模块</strong>(<em>root module</em>)。</p>
|
||
<p>另外注意到即便在项目的子模块中使用外部 crate,<code>extern crate</code>也应该位于根模块(也就是 <em>src/main.rs</em> 或 <em>src/lib.rs</em>)。接着,在子模块中,我们就可以像顶层模块那样引用外部 crate 中的项了。</p>
|
||
<p>我们的二进制 crate 如今正好调用了库中<code>client</code>模块的<code>connect</code>函数。然而,执行<code>cargo build</code>会在之前的警告之后出现一个错误:</p>
|
||
<pre><code>error: module `client` is private
|
||
--> src/main.rs:4:5
|
||
|
|
||
4 | communicator::client::connect();
|
||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
</code></pre>
|
||
<p>啊哈!这告诉了我们<code>client</code>模块是私有的,这也正是那些警告的症结所在。这也是我们第一次在 Rust 上下文中涉及到<strong>公有</strong>和<strong>私有</strong>的概念。Rust 所有代码的默认状态是私有的:除了自己之外别人不允许使用这些代码。如果不在自己的项目中使用一个私有函数,因为程序自身是唯一允许使用这个函数的代码,Rust 会警告说函数未被使用。</p>
|
||
<p>一旦我们指定一个像<code>client::connect</code>的函数为公有,不光二进制 crate 中的函数调用是允许的,函数未被使用的警告也会消失。将其标记为公有让 Rust 知道了我们意在使函数在我们程序的外部被使用。现在这个可能的理论上的外部可用性使得 Rust 认为这个函数“已经被使用”。因此。当某项被标记为公有,Rust 不再要求它在程序自身被使用并停止警告某项未被使用。</p>
|
||
<a class="header" href="#标记函数为公有" name="标记函数为公有"><h3>标记函数为公有</h3></a>
|
||
<p>为了告诉 Rust 某项为公有,在想要标记为公有的项的声明开头加上<code>pub</code>关键字。现在我们将致力于修复<code>client::connect</code>未被使用的警告,以及二进制 crate 中“模块<code>client</code>是私有的”的错误。像这样修改 <em>src/lib.rs</em> 使<code>client</code>模块公有:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">pub mod client;
|
||
|
||
mod network;
|
||
</code></pre>
|
||
<p><code>pub</code>写在<code>mod</code>之前。再次尝试构建:</p>
|
||
<pre><code><warnings>
|
||
error: function `connect` is private
|
||
--> src/main.rs:4:5
|
||
|
|
||
4 | communicator::client::connect();
|
||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
</code></pre>
|
||
<p>非常好!另一个不同的错误!好的,不同的错误信息是值得庆祝的(可能是程序员被黑的最惨的一次)。新错误表明“函数<code>connect</code>是私有的”,那么让我们修改 <em>src/client.rs</em> 将<code>client::connect</code>也设为公有:</p>
|
||
<p><span class="filename">Filename: src/client.rs</span></p>
|
||
<pre><code class="language-rust">pub fn connect() {
|
||
}
|
||
</code></pre>
|
||
<p>再再一次运行<code>cargo build</code>:</p>
|
||
<pre><code>warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||
--> src/network/mod.rs:1:1
|
||
|
|
||
1 | fn connect() {
|
||
| ^
|
||
|
||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||
--> src/network/server.rs:1:1
|
||
|
|
||
1 | fn connect() {
|
||
| ^
|
||
</code></pre>
|
||
<p>编译通过了,关于<code>client::connect</code>未被使用的警告消失了!</p>
|
||
<p>未被使用的代码并不总是意味着他们需要被设为公有的:如果你<strong>不</strong>希望这些函数成为公有 API 的一部分,未被使用的代码警告可能是在警告你这些代码不再需要并可以安全的删除他们。这也可能是警告你出 bug,如果你刚刚不小心删除了库中所有这个函数的调用。</p>
|
||
<p>当然我们的情况是,<strong>确实</strong>希望另外两个函数也作为 crate 公有 API 的一部分,所以让我们也将其标记为<code>pub</code>并去掉剩余的警告。修改 <em>src/network/mod.rs</em> 为:</p>
|
||
<p><span class="filename">Filename: src/network/mod.rs</span></p>
|
||
<pre><code class="language-rust,ignore">pub fn connect() {
|
||
}
|
||
|
||
mod server;
|
||
</code></pre>
|
||
<p>并编译:</p>
|
||
<pre><code>warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||
--> src/network/mod.rs:1:1
|
||
|
|
||
1 | pub fn connect() {
|
||
| ^
|
||
|
||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||
--> src/network/server.rs:1:1
|
||
|
|
||
1 | fn connect() {
|
||
| ^
|
||
</code></pre>
|
||
<p>恩,虽然将<code>network::connect</code>设为<code>pub</code>了我们仍然得到了一个未被使用函数的警告。这是因为模块中的函数是公有的,不过函数所在的<code>network</code>模块却不是公有的。这回我们是自内向外修改库文件的,而<code>client::connect</code>的时候是自外向内修改的。我们需要修改 <em>src/lib.rs</em> 让 <code>network</code> 也是公有的:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">pub mod client;
|
||
|
||
pub mod network;
|
||
</code></pre>
|
||
<p>现在再编译的话,那个警告就消失了:</p>
|
||
<pre><code>warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||
--> src/network/server.rs:1:1
|
||
|
|
||
1 | fn connect() {
|
||
| ^
|
||
</code></pre>
|
||
<p>只剩一个警告了!尝试自食其力修改它吧!</p>
|
||
<a class="header" href="#私有性规则" name="私有性规则"><h3>私有性规则</h3></a>
|
||
<p>总的来说,有如下项的可见性规则:</p>
|
||
<ol>
|
||
<li>如果一个项是公有的,它能被任何父模块访问</li>
|
||
<li>如果一个项是私有的,它只能被当前模块或其子模块访问</li>
|
||
</ol>
|
||
<a class="header" href="#私有性示例" name="私有性示例"><h3>私有性示例</h3></a>
|
||
<p>让我们看看更多例子作为练习。创建一个新的库项目并在新项目的 <em>src/lib.rs</em> 输入列表 7-5 中的代码:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">mod outermost {
|
||
pub fn middle_function() {}
|
||
|
||
fn middle_secret_function() {}
|
||
|
||
mod inside {
|
||
pub fn inner_function() {}
|
||
|
||
fn secret_function() {}
|
||
}
|
||
}
|
||
|
||
fn try_me() {
|
||
outermost::middle_function();
|
||
outermost::middle_secret_function();
|
||
outermost::inside::inner_function();
|
||
outermost::inside::secret_function();
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 7-5: Examples of private and public functions,
|
||
some of which are incorrect</span></p>
|
||
<p>在尝试编译这些代码之前,猜测一下<code>try_me</code>函数的哪一行会出错。接着编译项目来看看是否猜对了,然后继续阅读后面关于错误的讨论!</p>
|
||
<a class="header" href="#检查错误" name="检查错误"><h4>检查错误</h4></a>
|
||
<p><code>try_me</code>函数位于项目的根模块。叫做<code>outermost</code>的模块是私有的,不过第二条私有性规则说明<code>try_me</code>函数允许访问<code>outermost</code>模块,因为<code>outermost</code>位于当前(根)模块,<code>try_me</code>也是。</p>
|
||
<p><code>outermost::middle_function</code>的调用是正确的。因为<code>middle_function</code>是公有的,而<code>try_me</code>通过其父模块访问<code>middle_function</code>,<code>outermost</code>。根据上一段的规则我们可以确定这个模块是可访问的。</p>
|
||
<p><code>outermost::middle_secret_function</code>的调用会造成一个编译错误。<code>middle_secret_function</code>是私有的,所以第二条(私有性)规则生效了。根模块既不是<code>middle_secret_function</code>的当前模块(<code>outermost</code>是),也不是<code>middle_secret_function</code>当前模块的子模块。</p>
|
||
<p>叫做<code>inside</code>的模块是私有的且没有子模块,所以它只能被当前模块<code>outermost</code>访问。这意味着<code>try_me</code>函数不允许调用<code>outermost::inside::inner_function</code>或<code>outermost::inside::secret_function</code>任何一个。</p>
|
||
<a class="header" href="#修改错误" name="修改错误"><h4>修改错误</h4></a>
|
||
<p>这里有一些尝试修复错误的代码修改意见。在你尝试他们之前,猜测一下他们哪个能修复错误,接着编译查看你是否猜对了,并结合私有性规则理解为什么。</p>
|
||
<ul>
|
||
<li>如果<code>inside</code>模块是公有的?</li>
|
||
<li>如果<code>outermost</code>是公有的而<code>inside</code>是私有的?</li>
|
||
<li>如果在<code>inner_function</code>函数体中调用<code>::outermost::middle_secret_function()</code>?(开头的两个冒号意味着从根模块开始引用模块。)</li>
|
||
</ul>
|
||
<p>请随意设计更多的实验并尝试理解他们!</p>
|
||
<p>接下来,让我们讨论一下使用<code>use</code>关键字将模块项目引入作用域。</p>
|
||
<a class="header" href="#导入命名" name="导入命名"><h2>导入命名</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-03-importing-names-with-use.md">ch07-03-importing-names-with-use.md</a>
|
||
<br>
|
||
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56</p>
|
||
</blockquote>
|
||
<p>我们已经讲到了如何使用模块名称作为调用的一部分,来调用模块中的函数,如列表 7-6 中所示的<code>nested_modules</code>函数调用。</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">pub mod a {
|
||
pub mod series {
|
||
pub mod of {
|
||
pub fn nested_modules() {}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
a::series::of::nested_modules();
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 7-6: Calling a function by fully specifying its
|
||
enclosing module’s namespaces</span></p>
|
||
<p>如你所见,指定函数的完全限定名称可能会非常冗长。所幸 Rust 有一个关键字使得这些调用显得更简洁。</p>
|
||
<a class="header" href="#使用use的简单导入" name="使用use的简单导入"><h3>使用<code>use</code>的简单导入</h3></a>
|
||
<p>Rust 的<code>use</code>关键字的工作是缩短冗长的函数调用,通过将想要调用的函数所在的模块引入到作用域中。这是一个将<code>a::series::of</code>模块导入一个二进制 crate 的根作用域的例子:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">pub mod a {
|
||
pub mod series {
|
||
pub mod of {
|
||
pub fn nested_modules() {}
|
||
}
|
||
}
|
||
}
|
||
|
||
use a::series::of;
|
||
|
||
fn main() {
|
||
of::nested_modules();
|
||
}
|
||
</code></pre>
|
||
<p><code>use a::series::of;</code>这一行的意思是每当想要引用<code>of</code>模块时,不用使用完整的<code>a::series::of</code>路径,可以直接使用<code>of</code>。</p>
|
||
<p><code>use</code>关键字只将指定的模块引入作用域;它并不会将其子模块也引入。这就是为什么想要调用<code>nested_modules</code>函数时仍然必须写成<code>of::nested_modules</code>。</p>
|
||
<p>也可以将函数本身引入到作用域中,通过如下在<code>use</code>中指定函数的方式:</p>
|
||
<pre><code class="language-rust">pub mod a {
|
||
pub mod series {
|
||
pub mod of {
|
||
pub fn nested_modules() {}
|
||
}
|
||
}
|
||
}
|
||
|
||
use a::series::of::nested_modules;
|
||
|
||
fn main() {
|
||
nested_modules();
|
||
}
|
||
</code></pre>
|
||
<p>这使得我们可以忽略所有的模块并直接引用函数。</p>
|
||
<p>因为枚举也像模块一样组成了某种命名空间,也可以使用<code>use</code>来导入枚举的成员。对于任何类型的<code>use</code>语句,如果从一个命名空间导入多个项,可以使用大括号和逗号来列举他们,像这样:</p>
|
||
<pre><code class="language-rust">enum TrafficLight {
|
||
Red,
|
||
Yellow,
|
||
Green,
|
||
}
|
||
|
||
use TrafficLight::{Red, Yellow};
|
||
|
||
fn main() {
|
||
let red = Red;
|
||
let yellow = Yellow;
|
||
let green = TrafficLight::Green; // because we didn’t `use` TrafficLight::Green
|
||
}
|
||
</code></pre>
|
||
<a class="header" href="#使用的全局引用导入" name="使用的全局引用导入"><h3>使用<code>*</code>的全局引用导入</h3></a>
|
||
<p>为了一次导入某个命名空间的所有项,可以使用<code>*</code>语法。例如:</p>
|
||
<pre><code class="language-rust">enum TrafficLight {
|
||
Red,
|
||
Yellow,
|
||
Green,
|
||
}
|
||
|
||
use TrafficLight::*;
|
||
|
||
fn main() {
|
||
let red = Red;
|
||
let yellow = Yellow;
|
||
let green = Green;
|
||
}
|
||
</code></pre>
|
||
<p><code>*</code>被称为<strong>全局导入</strong>(<em>glob</em>),它会导入命名空间中所有可见的项。全局导入应该保守的使用:他们是方便的,但是也可能会引入多于你预期的内容从而导致命名冲突。</p>
|
||
<a class="header" href="#使用super访问父模块" name="使用super访问父模块"><h3>使用<code>super</code>访问父模块</h3></a>
|
||
<p>正如我们已经知道的,当创建一个库 crate 时,Cargo 会生成一个<code>tests</code>模块。现在让我们来深入了解一下。在<code>communicator</code>项目中,打开 <em>src/lib.rs</em>。</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">pub mod client;
|
||
|
||
pub mod network;
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
#[test]
|
||
fn it_works() {
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>第十一章会更详细的解释测试,不过其部分内容现在应该可以理解了:有一个叫做<code>tests</code>的模块紧邻其他模块,同时包含一个叫做<code>it_works</code>的函数。即便存在一些特殊注解,<code>tests</code>也不过是另外一个模块!所以我们的模块层次结构看起来像这样:</p>
|
||
<pre><code>communicator
|
||
├── client
|
||
├── network
|
||
| └── client
|
||
└── tests
|
||
</code></pre>
|
||
<p>测试是为了检验库中的代码而存在的,所以让我们尝试在<code>it_works</code>函数中调用<code>client::connect</code>函数,即便现在不准备测试任何功能:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">#[cfg(test)]
|
||
mod tests {
|
||
#[test]
|
||
fn it_works() {
|
||
client::connect();
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>使用<code>cargo test</code>命令运行测试:</p>
|
||
<pre><code>$ cargo test
|
||
Compiling communicator v0.1.0 (file:///projects/communicator)
|
||
error[E0433]: failed to resolve. Use of undeclared type or module `client`
|
||
--> src/lib.rs:9:9
|
||
|
|
||
9 | client::connect();
|
||
| ^^^^^^^^^^^^^^^ Use of undeclared type or module `client`
|
||
|
||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||
--> src/network/server.rs:1:1
|
||
|
|
||
1 | fn connect() {
|
||
| ^
|
||
</code></pre>
|
||
<p>编译失败了,不过为什么呢?并不需要像 <em>src/main.rs</em> 那样将<code>communicator::</code>置于函数前,因为这里肯定是在<code>communicator</code>库 crate 之内的。之所以失败的原因是路径是相对于当前模块的,在这里就是<code>tests</code>。唯一的例外就是<code>use</code>语句,它默认是相对于 crate 根模块的。我们的<code>tests</code>模块需要<code>client</code>模块位于其作用域中!</p>
|
||
<p>那么如何在模块层次结构中回退一级模块,以便在<code>tests</code>模块中能够调用<code>client::connect</code>函数呢?在<code>tests</code>模块中,要么可以在开头使用双冒号来让 Rust 知道我们想要从根模块开始并列出整个路径:</p>
|
||
<pre><code class="language-rust,ignore">::client::connect();
|
||
</code></pre>
|
||
<p>要么可以使用<code>super</code>在层级中获取当前模块的上一级模块:</p>
|
||
<pre><code class="language-rust,ignore">super::client::connect();
|
||
</code></pre>
|
||
<p>在这个例子中这两个选择看不出有多么大的区别,不过随着模块层次的更加深入,每次都从根模块开始就会显得很长了。在这些情况下,使用<code>super</code>来获取当前模块的同级模块是一个好的捷径。再加上,如果在代码中的很多地方指定了从根开始的路径,那么当通过移动子树或到其他位置来重新排列模块时,最终就需要更新很多地方的路径,这就非常乏味无趣了。</p>
|
||
<p>在每一个测试中总是不得不编写<code>super::</code>也会显得很恼人,不过你已经见过解决这个问题的利器了:<code>use</code>!<code>super::</code>的功能改变了提供给<code>use</code>的路径,使其不再相对于根模块而是相对于父模块。</p>
|
||
<p>为此,特别是在<code>tests</code>模块,<code>use super::something</code>是常用的手段。所以现在的测试看起来像这样:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">#[cfg(test)]
|
||
mod tests {
|
||
use super::client;
|
||
|
||
#[test]
|
||
fn it_works() {
|
||
client::connect();
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>如果再次运行<code>cargo test</code>,测试将会通过而且测试结果输出的第一部分将会是:</p>
|
||
<pre><code>$ cargo test
|
||
Compiling communicator v0.1.0 (file:///projects/communicator)
|
||
Running target/debug/communicator-92007ddb5330fa5a
|
||
|
||
running 1 test
|
||
test tests::it_works ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>现在你掌握了组织代码的核心科技!利用他们将相关的代码组合在一起、防止代码文件过长并将一个整洁的公有 API 展现给库的用户。</p>
|
||
<p>接下来,让我们看看一些标准库提供的集合数据类型,你可以利用他们编写出漂亮整洁的代码。</p>
|
||
<a class="header" href="#通用集合类型" name="通用集合类型"><h1>通用集合类型</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-00-common-collections.md">ch08-00-common-collections.md</a>
|
||
<br>
|
||
commit e6d6caab41471f7115a621029bd428a812c5260e</p>
|
||
</blockquote>
|
||
<p>Rust 标准库中包含一系列被称为<strong>集合</strong>(<em>collections</em>)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就可知并且可以随着程序的运行增长或缩小。每种集合都有着不同能力和代价,而为所处的场景选择合适的集合则是你将要始终发展的技能。在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合:</p>
|
||
<ul>
|
||
<li><em>vector</em> 允许我们一个挨着一个地储存一系列数量可变的值</li>
|
||
<li><strong>字符串</strong>(<em>string</em>)是一个字符的集合。我们之前见过<code>String</code>类型,现在将详细介绍它。</li>
|
||
<li><strong>哈希 map</strong>(<em>hash map</em>)允许我们将值与一个特定的键(key)相关联。这是一个叫做 <em>map</em> 的更通用的数据结构的特定实现。</li>
|
||
</ul>
|
||
<p>对于标准库提供的其他类型的集合,请查看<a href="https://doc.rust-lang.org/std/collections">文档</a>。</p>
|
||
<p>我们将讨论如何创建和更新 vector、字符串和哈希 map,以及他们有什么不同。</p>
|
||
<a class="header" href="#vector" name="vector"><h2>vector</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-01-vectors.md">ch08-01-vectors.md</a>
|
||
<br>
|
||
commit 6c24544ba718bce0755bdaf03423af86280051d5</p>
|
||
</blockquote>
|
||
<p>我们要讲到的第一个类型是<code>Vec<T></code>,也被称为 <em>vector</em>。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。</p>
|
||
<a class="header" href="#新建-vector" name="新建-vector"><h3>新建 vector</h3></a>
|
||
<p>为了创建一个新的,空的 vector,可以调用<code>Vec::new</code>函数:</p>
|
||
<pre><code class="language-rust">let v: Vec<i32> = Vec::new();
|
||
</code></pre>
|
||
<p>注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是同质的(homogeneous):他们可以储存很多值,不过这些值必须都是相同类型的。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用他们。现在,所有你需要知道的就是<code>Vec</code>是一个由标准库提供的类型,它可以存放任何类型,而当<code>Vec</code>存放某个特定类型时,那个类型位于尖括号中。这里我们告诉 Rust <code>v</code>这个<code>Vec</code>将存放<code>i32</code>类型的元素。</p>
|
||
<p>在实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。更常见的做法是使用初始值来创建一个<code>Vec</code>,而且为了方便 Rust 提供了<code>vec!</code>宏。这个宏会根据我们提供的值来创建一个新的<code>Vec</code>。如下代码会新建一个拥有值<code>1</code>、<code>2</code>和<code>3</code>的<code>Vec<i32></code>:</p>
|
||
<pre><code class="language-rust">let v = vec![1, 2, 3];
|
||
</code></pre>
|
||
<p>因为我们提供了<code>i32</code>类型的初始值,Rust 可以推断出<code>v</code>的类型是<code>Vec<i32></code>,因此类型注解就不是必须的。接下来让我们看看如何修改一个 vector。</p>
|
||
<a class="header" href="#更新-vector" name="更新-vector"><h3>更新 vector</h3></a>
|
||
<p>对于新建一个 vector 并向其增加元素,可以使用<code>push</code>方法:</p>
|
||
<pre><code class="language-rust">let mut v = Vec::new();
|
||
|
||
v.push(5);
|
||
v.push(6);
|
||
v.push(7);
|
||
v.push(8);
|
||
</code></pre>
|
||
<p>如第三章中讨论的任何变量一样,如果想要能够改变它的值,必须使用<code>mut</code>关键字使其可变。放入其中的所有值都是<code>i32</code>类型的,而且 Rust 也根据数据如此判断,所以不需要<code>Vec<i32></code>注解。</p>
|
||
<a class="header" href="#丢弃-vector-时也会丢弃其所有元素" name="丢弃-vector-时也会丢弃其所有元素"><h3>丢弃 vector 时也会丢弃其所有元素</h3></a>
|
||
<p>类似于任何其他的<code>struct</code>,vector 在其离开作用域时会被释放:</p>
|
||
<pre><code class="language-rust">{
|
||
let v = vec![1, 2, 3, 4];
|
||
|
||
// do stuff with v
|
||
|
||
} // <- v goes out of scope and is freed here
|
||
</code></pre>
|
||
<p>当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。这可能看起来非常直观,不过一旦开始使用 vector 元素的引用情况就变得有些复杂了。下面让我们处理这种情况!</p>
|
||
<a class="header" href="#读取-vector-的元素" name="读取-vector-的元素"><h3>读取 vector 的元素</h3></a>
|
||
<p>现在你知道如何创建、更新和销毁 vector 了,接下来的一步最好了解一下如何读取他们的内容。有两种方法引用 vector 中储存的值。为了更加清楚的说明这个例子,我们标注这些函数返回的值的类型。</p>
|
||
<p>这个例子展示了访问 vector 中一个值的两种方式,索引语法或者<code>get</code>方法:</p>
|
||
<pre><code class="language-rust">let v = vec![1, 2, 3, 4, 5];
|
||
|
||
let third: &i32 = &v[2];
|
||
let third: Option<&i32> = v.get(2);
|
||
</code></pre>
|
||
<p>这里有一些需要注意的地方。首先,我们使用索引值<code>2</code>来获取第三个元素,索引是从 0 开始的。其次,这两个不同的获取第三个元素的方式分别为:使用<code>&</code>和<code>[]</code>返回一个引用;或者使用<code>get</code>方法以索引作为参数来返回一个<code>Option<&T></code>。</p>
|
||
<p>Rust 有两个引用元素的方法的原因是程序可以选择如何处理当索引值在 vector 中没有对应值的情况。例如如下情况,如果有一个有五个元素的 vector 接着尝试访问索引为 100 的元素,程序该如何处理:</p>
|
||
<pre><code class="language-rust,should_panic">let v = vec![1, 2, 3, 4, 5];
|
||
|
||
let does_not_exist = &v[100];
|
||
let does_not_exist = v.get(100);
|
||
</code></pre>
|
||
<p>当运行这段代码,你会发现对于第一个<code>[]</code>方法,当引用一个不存在的元素时 Rust 会造成<code>panic!</code>。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。</p>
|
||
<p>当<code>get</code>方法被传递了一个数组外的索引时,它不会 panic 而是返回<code>None</code>。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理<code>Some(&element)</code>或<code>None</code>的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果他们不慎输入了一个过大的数字那么程序就会得到<code>None</code>值,你可以告诉用户<code>Vec</code>当前元素的数量并再请求他们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多!</p>
|
||
<a class="header" href="#无效引用" name="无效引用"><h4>无效引用</h4></a>
|
||
<p>一旦程序获取了一个有效的引用,借用检查器将会执行第四章讲到的所有权和借用规则来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于这个例子,当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候:</p>
|
||
<pre><code class="language-rust,ignore">let mut v = vec![1, 2, 3, 4, 5];
|
||
|
||
let first = &v[0];
|
||
|
||
v.push(6);
|
||
</code></pre>
|
||
<p>编译会给出这个错误:</p>
|
||
<pre><code>error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
|
||
immutable
|
||
|
|
||
4 | let first = &v[0];
|
||
| - immutable borrow occurs here
|
||
5 |
|
||
6 | v.push(6);
|
||
| ^ mutable borrow occurs here
|
||
7 | }
|
||
| - immutable borrow ends here
|
||
</code></pre>
|
||
<p>这些代码看起来应该能够运行:为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式。在 vector 的结尾增加新元素是,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。</p>
|
||
<blockquote>
|
||
<p>注意:关于更多内容,查看 Nomicon <em>https://doc.rust-lang.org/stable/nomicon/vec.html</em></p>
|
||
</blockquote>
|
||
<a class="header" href="#使用枚举来储存多种类型" name="使用枚举来储存多种类型"><h3>使用枚举来储存多种类型</h3></a>
|
||
<p>在本章的开始,我们提到 vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举!</p>
|
||
<p>例如,假如我们想要从电子表格的一行中获取值,而这一行的有些列包含数字,有些包含浮点值,还有些是字符串。我们可以定义一个枚举,其成员会存放这些不同类型的值,同时所有这些枚举成员都会被当作相同类型,那个枚举的类型。接着可以创建一个储存枚举值的 vector,这样最终就能够储存不同类型的值了:</p>
|
||
<pre><code class="language-rust">enum SpreadsheetCell {
|
||
Int(i32),
|
||
Float(f64),
|
||
Text(String),
|
||
}
|
||
|
||
let row = vec![
|
||
SpreadsheetCell::Int(3),
|
||
SpreadsheetCell::Text(String::from("blue")),
|
||
SpreadsheetCell::Float(10.12),
|
||
];
|
||
</code></pre>
|
||
<p><span class="caption">Listing 8-1: Defining an enum to be able to hold
|
||
different types of data in a vector</span></p>
|
||
<p>Rust 在编译时就必须准确的知道 vector 中类型的原因是它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加<code>match</code>意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。</p>
|
||
<p>如果在编写程序时不能确切无遗的知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十七章会讲到它。</p>
|
||
<p>现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中<code>Vec</code>定义的很多其他实用方法的 API 文档。例如,除了<code>push</code>之外还有一个<code>pop</code>方法,它会移除并返回 vector 的最后一个元素。让我们继续下一个集合类型:<code>String</code>!</p>
|
||
<a class="header" href="#字符串" name="字符串"><h2>字符串</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-02-strings.md">ch08-02-strings.md</a>
|
||
<br>
|
||
commit d362dadae60a7cc3212b107b9e9562769b0f20e3</p>
|
||
</blockquote>
|
||
<p>第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解一下它。字符串是新晋 Rustacean 们通常会被困住的领域。这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。</p>
|
||
<p>字符串出现在集合章节的原因是,字符串是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到<code>String</code>那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论<code>String</code>于其他集合不一样的地方,例如索引<code>String</code>是很复杂的,由于人和计算机理解<code>String</code>数据的不同方式。</p>
|
||
<a class="header" href="#什么是字符串" name="什么是字符串"><h3>什么是字符串?</h3></a>
|
||
<p>在开始深入这些方面之前,我们需要讨论一下术语<strong>字符串</strong>的具体意义。Rust 的核心语言中事实上就只有一种字符串类型:<code>str</code>,字符串 slice,它通常以被借用的形式出现,<code>&str</code>。第四章讲到了<strong>字符串 slice</strong>:他们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。</p>
|
||
<p>称作<code>String</code>的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的“字符串”时,他们通常指的是<code>String</code>和字符串 slice <code>&str</code>类型,而不是其中一个。这一部分大部分是关于<code>String</code>的,不过这些类型在 Rust 标准库中都被广泛使用。<code>String</code>和字符串 slice 都是 UTF-8 编码的。</p>
|
||
<p>Rust 标准库中还包含一系列其他字符串类型,比如<code>OsString</code>、<code>OsStr</code>、<code>CString</code>和<code>CStr</code>。相关库 crate 甚至会提供更多储存字符串数据的选择。与<code>*String</code>/<code>*Str</code>的命名类似,他们通常也提供有所有权和可借用的变体,就比如说<code>String</code>/<code>&str</code>。这些字符串类型在储存的编码或内存表现形式上可能有所不同。本章将不会讨论其他这些字符串类型;查看 API 文档来更多的了解如何使用他们以及各自适合的场景。</p>
|
||
<a class="header" href="#新建字符串" name="新建字符串"><h3>新建字符串</h3></a>
|
||
<p>很多<code>Vec</code>可用的操作在<code>String</code>中同样可用,从以<code>new</code>函数创建字符串开始,像这样:</p>
|
||
<pre><code class="language-rust">let s = String::new();
|
||
</code></pre>
|
||
<p>这新建了一个叫做<code>s</code>的空的字符串,接着我们可以向其中装载数据。</p>
|
||
<p>通常字符串会有初始数据因为我们希望一开始就有这个字符串。为此,使用<code>to_string</code>方法,它能用于任何实现了<code>Display</code> trait 的类型,对于字符串字面值是这样:</p>
|
||
<pre><code class="language-rust">let data = "initial contents";
|
||
|
||
let s = data.to_string();
|
||
|
||
// the method also works on a literal directly:
|
||
let s = "initial contents".to_string();
|
||
</code></pre>
|
||
<p>这会创建一个包好<code>initial contents</code>的字符串。</p>
|
||
<p>也可以使用<code>String::from</code>函数来从字符串字面值创建<code>String</code>。如下等同于使用<code>to_string</code>:</p>
|
||
<pre><code class="language-rust">let s = String::from("initial contents");
|
||
</code></pre>
|
||
<p>因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。他们有些可能显得有些多余,不过都有其用武之地!在这个例子中,<code>String::from</code>和<code>.to_string</code>最终做了完全相同的工作,所以如何选择就是风格问题了。</p>
|
||
<p>记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据:</p>
|
||
<pre><code class="language-rust">let hello = "السلام عليكم";
|
||
let hello = "Dobrý den";
|
||
let hello = "Hello";
|
||
let hello = "שָׁלוֹם";
|
||
let hello = "नमस्ते";
|
||
let hello = "こんにちは";
|
||
let hello = "안녕하세요";
|
||
let hello = "你好";
|
||
let hello = "Olá";
|
||
let hello = "Здравствуйте";
|
||
let hello = "Hola";
|
||
</code></pre>
|
||
<a class="header" href="#更新字符串" name="更新字符串"><h3>更新字符串</h3></a>
|
||
<p><code>String</code>的大小可以增长其内容也可以改变,就像可以放入更多数据来改变<code>Vec</code>的内容一样。另外,<code>String</code>实现了<code>+</code>运算符作为级联运算符以便于使用。</p>
|
||
<a class="header" href="#使用-push-附加字符串" name="使用-push-附加字符串"><h4>使用 push 附加字符串</h4></a>
|
||
<p>可以通过<code>push_str</code>方法来附加字符串 slice,从而使<code>String</code>变长:</p>
|
||
<pre><code class="language-rust">let mut s = String::from("foo");
|
||
s.push_str("bar");
|
||
</code></pre>
|
||
<p>执行这两行代码之后<code>s</code>将会包含“foobar”。<code>push_str</code>方法获取字符串 slice,因为并不需要获取参数的所有权。例如,如果将<code>s2</code>的内容附加到<code>s1</code>中后自身不能被使用就糟糕了:</p>
|
||
<pre><code class="language-rust">let mut s1 = String::from("foo");
|
||
let s2 = String::from("bar");
|
||
s1.push_str(&s2);
|
||
</code></pre>
|
||
<p><code>push</code>方法被定义为获取一个单独的字符作为参数,并附加到<code>String</code>中:</p>
|
||
<pre><code class="language-rust">let mut s = String::from("lo");
|
||
s.push('l');
|
||
</code></pre>
|
||
<p>执行这些代码之后,<code>s</code>将会包含“lol”。</p>
|
||
<a class="header" href="#使用--运算符或format宏级联字符串" name="使用--运算符或format宏级联字符串"><h4>使用 + 运算符或<code>format!</code>宏级联字符串</h4></a>
|
||
<p>通常我们希望将两个已知的字符串合并在一起。一种办法是像这样使用<code>+</code>运算符:</p>
|
||
<pre><code class="language-rust">let s1 = String::from("Hello, ");
|
||
let s2 = String::from("world!");
|
||
let s3 = s1 + &s2; // Note that s1 has been moved here and can no longer be used
|
||
</code></pre>
|
||
<p>执行完这些代码之后字符串<code>s3</code>将会包含<code>Hello, world!</code>。<code>s1</code>在相加后不再有效的原因,和使用<code>s2</code>的引用的原因与使用<code>+</code>运算符时调用的方法签名有关,这个函数签名看起来像这样:</p>
|
||
<pre><code class="language-rust,ignore">fn add(self, s: &str) -> String {
|
||
</code></pre>
|
||
<p>这并不是标准库中实际的签名;那个<code>add</code>使用泛型定义。这里的签名使用具体类型代替了泛型,这也正是当使用<code>String</code>值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解<code>+</code>运算那奇怪的部分的线索。</p>
|
||
<p>首先,<code>s2</code>使用了<code>&</code>,意味着我们使用第二个字符串的<strong>引用</strong>与第一个字符串相加。这是因为<code>add</code>函数的<code>s</code>参数:只能将<code>&str</code>和<code>String</code>相加,不能将两个<code>String</code>值相加。不过等一下——正如<code>add</code>的第二个参数所指定的,<code>&s2</code>的类型是<code>&String</code>而不是<code>&str</code>。那么为什么代码还能编译呢?之所以能够在<code>add</code>调用中使用<code>&s2</code>是因为<code>&String</code>可以被<strong>强转</strong>(<em>coerced</em>)成 <code>&str</code>——当<code>add</code>函数被调用时,Rust 使用了一个被称为<strong>解引用强制多态</strong>(<em>deref coercion</em>)的技术,你可以将其理解为它把<code>&s2</code>变成了<code>&s2[..]</code>以供<code>add</code>函数使用。第十五章会更深入的讨论解引用强制多态。因为<code>add</code>没有获取参数的所有权,所以<code>s2</code>在这个操作后仍然是有效的<code>String</code>。</p>
|
||
<p>其次,可以发现签名中<code>add</code>获取了<code>self</code>的所有权,因为<code>self</code><strong>没有</strong>使用<code>&</code>。这意味着上面例子中的<code>s1</code>的所有权将被移动到<code>add</code>调用中,之后就不再有效。所以虽然<code>let s3 = s1 + &s2;</code>看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取<code>s1</code>的所有权,附加上从<code>s2</code>中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。</p>
|
||
<p>如果想要级联多个字符串,<code>+</code>的行为就显得笨重了:</p>
|
||
<pre><code class="language-rust">let s1 = String::from("tic");
|
||
let s2 = String::from("tac");
|
||
let s3 = String::from("toe");
|
||
|
||
let s = s1 + "-" + &s2 + "-" + &s3;
|
||
</code></pre>
|
||
<p>这时<code>s</code>的内容会是“tic-tac-toe”。在有这么多<code>+</code>和<code>"</code>字符的情况下,很难理解具体发生了什么。对于更为复杂的字符串链接,可以使用<code>format!</code>宏:</p>
|
||
<pre><code class="language-rust">let s1 = String::from("tic");
|
||
let s2 = String::from("tac");
|
||
let s3 = String::from("toe");
|
||
|
||
let s = format!("{}-{}-{}", s1, s2, s3);
|
||
</code></pre>
|
||
<p>这些代码也会将<code>s</code>设置为“tic-tac-toe”。<code>format!</code>与<code>println!</code>的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果的<code>String</code>。这个版本就好理解的多,并且不会获取任何参数的所有权。</p>
|
||
<a class="header" href="#索引字符串" name="索引字符串"><h3>索引字符串</h3></a>
|
||
<p>在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果我们尝试使用索引语法访问<code>String</code>的一部分,会出现一个错误。比如如下代码:</p>
|
||
<pre><code class="language-rust,ignore">let s1 = String::from("hello");
|
||
let h = s1[0];
|
||
</code></pre>
|
||
<p>会导致如下错误:</p>
|
||
<pre><code>error: the trait bound `std::string::String: std::ops::Index<_>` is not
|
||
satisfied [--explain E0277]
|
||
|>
|
||
|> let h = s1[0];
|
||
|> ^^^^^
|
||
note: the type `std::string::String` cannot be indexed by `_`
|
||
</code></pre>
|
||
<p>错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 是如何在内存中储存字符串的。</p>
|
||
<a class="header" href="#内部表示" name="内部表示"><h4>内部表示</h4></a>
|
||
<p><code>String</code>是一个<code>Vec<u8></code>的封装。让我们看看之前一些正确编码的字符串的例子。首先是这一个:</p>
|
||
<pre><code class="language-rust">let len = String::from("Hola").len();
|
||
</code></pre>
|
||
<p>在这里,<code>len</code>的值是四,这意味着储存字符串“Hola”的<code>Vec</code>的长度是四个字节:每一个字符的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?</p>
|
||
<pre><code class="language-rust">let len = String::from("Здравствуйте").len();
|
||
</code></pre>
|
||
<p>当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。</p>
|
||
<p>作为演示,考虑如下无效的 Rust 代码:</p>
|
||
<pre><code class="language-rust,ignore">let hello = "Здравствуйте";
|
||
let answer = &hello[0];
|
||
</code></pre>
|
||
<p><code>answer</code>的值应该是什么呢?它应该是第一个字符<code>З</code>吗?当使用 UTF-8 编码时,<code>З</code>的第一个字节是<code>208</code>,第二个是<code>151</code>,所以<code>answer</code>实际上应该是<code>208</code>,不过<code>208</code>自身并不是一个有效的字母。返回<code>208</code>可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引零位置所能提供的唯一数据。返回字节值可能不是人们希望看到的,即便是只有拉丁字母时:<code>&"hello"[0]</code>会返回<code>104</code>而不是<code>h</code>。为了避免返回意想不到值并造成不能立刻发现的 bug。Rust 选择不编译这些代码并及早杜绝了误会的放生。</p>
|
||
<a class="header" href="#字节标量值和字形簇天呐" name="字节标量值和字形簇天呐"><h4>字节、标量值和字形簇!天呐!</h4></a>
|
||
<p>这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中<strong>字母</strong>的概念)。</p>
|
||
<p>比如这个用梵文书写的印度语单词“नमस्ते”,最终它储存在<code>Vec</code>中的<code>u8</code>值看起来像这样:</p>
|
||
<pre><code>[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
|
||
224, 165, 135]
|
||
</code></pre>
|
||
<p>这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解他们,也就像 Rust 的<code>char</code>类型那样,这些字节看起来像这样:</p>
|
||
<pre><code>['न', 'म', 'स', '्', 'त', 'े']
|
||
</code></pre>
|
||
<p>这里有六个<code>char</code>,不过第四个和第六个都不是字母,他们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:</p>
|
||
<pre><code>["न", "म", "स्", "ते"]
|
||
</code></pre>
|
||
<p>Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言。</p>
|
||
<p>最后一个 Rust 不允许使用索引获取<code>String</code>字符的原因是索引操作预期总是需要常数时间 (O(1))。但是对于<code>String</code>不可能保证这样的性能,因为 Rust 不得不检查从字符串的开头到索引位置的内容来确定这里有多少有效的字符。</p>
|
||
<a class="header" href="#字符串-slice" name="字符串-slice"><h3>字符串 slice</h3></a>
|
||
<p>因为字符串索引应该返回的类型是不明确的,而且索引字符串通常也是一个坏点子,所以 Rust 不建议这么做,而如果你确实需要它的话则需要更加明确一些。比使用<code>[]</code>和单个值的索引更加明确的方式是使用<code>[]</code>和一个 range 来创建包含特定字节的字符串 slice:</p>
|
||
<pre><code class="language-rust">let hello = "Здравствуйте";
|
||
|
||
let s = &hello[0..4];
|
||
</code></pre>
|
||
<p>这里,<code>s</code>是一个<code>&str</code>,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着<code>s</code>将会是“Зд”。</p>
|
||
<p>如果获取<code>&hello[0..1]</code>会发生什么呢?答案是:在运行时会 panic,就跟访问 vector 中的无效索引时一样:</p>
|
||
<pre><code class="language-text">thread 'main' panicked at 'index 0 and/or 1 in `Здравствуйте` do not lie on
|
||
character boundary', ../src/libcore/str/mod.rs:1694
|
||
</code></pre>
|
||
<p>你应该小心谨慎的使用这个操作,因为它可能会使你的程序崩溃。</p>
|
||
<a class="header" href="#遍历字符串的方法" name="遍历字符串的方法"><h3>遍历字符串的方法</h3></a>
|
||
<p>幸运的是,这里还有其他获取字符串元素的方式。</p>
|
||
<p>如果你需要操作单独的 Unicode 标量值,最好的选择是使用<code>chars</code>方法。对“नमस्ते”调用<code>chars</code>方法会将其分开并返回六个<code>char</code>类型的值,接着就可以遍历结果来访问每一个元素了:</p>
|
||
<pre><code class="language-rust">for c in "नमस्ते".chars() {
|
||
println!("{}", c);
|
||
}
|
||
</code></pre>
|
||
<p>这些代码会打印出如下内容:</p>
|
||
<pre><code>न
|
||
म
|
||
स
|
||
्
|
||
त
|
||
े
|
||
</code></pre>
|
||
<p><code>bytes</code>方法返回每一个原始字节,这可能会适合你的使用场景:</p>
|
||
<pre><code class="language-rust">for b in "नमस्ते".bytes() {
|
||
println!("{}", b);
|
||
}
|
||
</code></pre>
|
||
<p>这些代码会打印出组成<code>String</code>的 18 个字节,开头是这样的:</p>
|
||
<pre><code>224
|
||
164
|
||
168
|
||
224
|
||
// ... etc
|
||
</code></pre>
|
||
<p>不过请记住有效的 Unicode 标量值可能会由不止一个字节组成。</p>
|
||
<p>从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。</p>
|
||
<a class="header" href="#字符串并不简单" name="字符串并不简单"><h3>字符串并不简单</h3></a>
|
||
<p>总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理<code>String</code>数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何在前台处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期中免于处理涉及非 ASCII 字符的错误。</p>
|
||
<p>现在让我们转向一些不太复杂的集合:哈希 map!</p>
|
||
<a class="header" href="#哈希-map" name="哈希-map"><h2>哈希 map</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-03-hash-maps.md">ch08-03-hash-maps.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p>最后介绍的常用集合类型是 <strong>哈希 map</strong>(<em>hash map</em>)。<code>HashMap<K, V></code> 类型储存了一个键类型 <code>K</code> 对应一个值类型 <code>V</code> 的映射。它通过一个<strong>哈希函数</strong>(<em>hashing function</em>)来实现映射,决定如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。</p>
|
||
<p>哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。</p>
|
||
<p>本章我们会介绍哈希 map 的基本 API,不过还有更多吸引人的功能隐藏于标准库中的<code>HashMap</code>定义的函数中。请一如既往地查看标准库文档来了解更多信息。</p>
|
||
<a class="header" href="#新建一个哈希-map" name="新建一个哈希-map"><h3>新建一个哈希 map</h3></a>
|
||
<p>可以使用<code>new</code>创建一个空的<code>HashMap</code>,并使用<code>insert</code>来增加元素。这里我们记录两支队伍的分数,分别是蓝队和黄队。蓝队开始有 10 分而黄队开始有 50 分:</p>
|
||
<pre><code class="language-rust">use std::collections::HashMap;
|
||
|
||
let mut scores = HashMap::new();
|
||
|
||
scores.insert(String::from("Blue"), 10);
|
||
scores.insert(String::from("Yellow"), 50);
|
||
</code></pre>
|
||
<p>注意必须首先 <code>use</code> 标准库中集合部分的 <code>HashMap</code>。在这三个常用集合中,<code>HashMap</code> 是最不常用的,所以并没有被 prelude 自动引用。标准库中对 <code>HashMap</code> 的支持也相对较少,例如,并没有内建的构建宏。
|
||
像 vector 一样,哈希 map 将他们的数据储存在堆上,这个 <code>HashMap</code> 的键类型是 <code>String</code> 而值类型是 <code>i32</code>。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。</p>
|
||
<p>另一个构建哈希 map 的方法是使用一个元组的 vector 的 <code>collect</code> 方法,其中每个元组包含一个键值对。<code>collect</code> 方法可以将数据收集进一系列的集合类型,包括 <code>HashMap</code>。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用 <code>zip</code> 方法来创建一个元组的 vector,其中“Blue”与 10 是一对,依此类推。接着就可以使用 <code>collect</code> 方法将这个元组 vector 转换成一个 <code>HashMap</code>:</p>
|
||
<pre><code class="language-rust">use std::collections::HashMap;
|
||
|
||
let teams = vec![String::from("Blue"), String::from("Yellow")];
|
||
let initial_scores = vec![10, 50];
|
||
|
||
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
|
||
</code></pre>
|
||
<p>这里<code>HashMap<_, _></code>类型注解是必要的,因为可能<code>collect</code>进很多不同的数据结构,而除非显式指定 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 <code>HashMap</code> 所包含的类型。</p>
|
||
<a class="header" href="#哈希-map-和所有权" name="哈希-map-和所有权"><h3>哈希 map 和所有权</h3></a>
|
||
<p>对于像<code>i32</code>这样的实现了<code>Copy</code> trait 的类型,其值可以拷贝进哈希 map。对于像<code>String</code>这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者:</p>
|
||
<pre><code class="language-rust">use std::collections::HashMap;
|
||
|
||
let field_name = String::from("Favorite color");
|
||
let field_value = String::from("Blue");
|
||
|
||
let mut map = HashMap::new();
|
||
map.insert(field_name, field_value);
|
||
// field_name and field_value are invalid at this point
|
||
</code></pre>
|
||
<p>当<code>insert</code>调用将<code>field_name</code>和<code>field_value</code>移动到哈希 map 中后,将不能使用这两个绑定。</p>
|
||
<p>如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。第十章生命周期部分将会更多的讨论这个问题。</p>
|
||
<a class="header" href="#访问哈希-map-中的值" name="访问哈希-map-中的值"><h3>访问哈希 map 中的值</h3></a>
|
||
<p>可以通过<code>get</code>方法并提供对应的键来从哈希 map 中获取值:</p>
|
||
<pre><code class="language-rust">use std::collections::HashMap;
|
||
|
||
let mut scores = HashMap::new();
|
||
|
||
scores.insert(String::from("Blue"), 10);
|
||
scores.insert(String::from("Yellow"), 50);
|
||
|
||
let team_name = String::from("Blue");
|
||
let score = scores.get(&team_name);
|
||
</code></pre>
|
||
<p>这里,<code>score</code> 是与蓝队分数相关的值,应为 <code>Some(10)</code>。因为 <code>get</code> 返回 <code>Option<V></code>,所以结果被装进 <code>Some</code>;如果某个键在哈希 map 中没有对应的值,<code>get</code> 会返回 <code>None</code>。这时就要用某种第六章提到的方法来处理 <code>Option</code>。</p>
|
||
<p>可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是<code>for</code>循环:</p>
|
||
<pre><code class="language-rust">use std::collections::HashMap;
|
||
|
||
let mut scores = HashMap::new();
|
||
|
||
scores.insert(String::from("Blue"), 10);
|
||
scores.insert(String::from("Yellow"), 50);
|
||
|
||
for (key, value) in &scores {
|
||
println!("{}: {}", key, value);
|
||
}
|
||
</code></pre>
|
||
<p>这会以任意顺序打印出每一个键值对:</p>
|
||
<pre><code>Yellow: 50
|
||
Blue: 10
|
||
</code></pre>
|
||
<a class="header" href="#更新哈希-map" name="更新哈希-map"><h3>更新哈希 map</h3></a>
|
||
<p>尽管键值对的数量是可以增长的,不过任何时候,每个键只能关联一个值。当你想要改变哈希 map 中的数据时,根据目标键是否有值以及值的更新策略分成多种情况,下面我们了解一下:</p>
|
||
<a class="header" href="#覆盖一个值" name="覆盖一个值"><h4>覆盖一个值</h4></a>
|
||
<p>如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便下面的代码调用了两次<code>insert</code>,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值:</p>
|
||
<pre><code class="language-rust">use std::collections::HashMap;
|
||
|
||
let mut scores = HashMap::new();
|
||
|
||
scores.insert(String::from("Blue"), 10);
|
||
scores.insert(String::from("Blue"), 25);
|
||
|
||
println!("{:?}", scores);
|
||
</code></pre>
|
||
<p>这会打印出<code>{"Blue": 25}</code>。原始的值 10 将被覆盖。</p>
|
||
<a class="header" href="#只在键没有对应值时插入" name="只在键没有对应值时插入"><h4>只在键没有对应值时插入</h4></a>
|
||
<p>我们经常会检查某个特定的键是否有值,如果没有就插入一个值。为此哈希 map 有一个特有的 API,叫做<code>entry</code>,它获取我们想要检查的键作为参数。<code>entry</code>函数的返回值是一个枚举,<code>Entry</code>,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 50,对于蓝队也是如此。使用 entry API 的代码看起来像这样:</p>
|
||
<pre><code class="language-rust">use std::collections::HashMap;
|
||
|
||
let mut scores = HashMap::new();
|
||
scores.insert(String::from("Blue"), 10);
|
||
|
||
scores.entry(String::from("Yellow")).or_insert(50);
|
||
scores.entry(String::from("Blue")).or_insert(50);
|
||
|
||
println!("{:?}", scores);
|
||
</code></pre>
|
||
<p><code>Entry</code>的<code>or_insert</code>方法在键对应的值存在时就返回这个值的<code>Entry</code>,如果不存在则将参数作为新值插入并返回修改过的<code>Entry</code>。这比编写自己的逻辑要简明的多,另外也与借用检查器结合得更好。</p>
|
||
<p>这段代码会打印出<code>{"Yellow": 50, "Blue": 10}</code>。第一个<code>entry</code>调用会插入黄队的键和值 50,因为黄队并没有一个值。第二个<code>entry</code>调用不会改变哈希 map 因为蓝队已经有了值 10。</p>
|
||
<a class="header" href="#根据旧值更新一个值" name="根据旧值更新一个值"><h4>根据旧值更新一个值</h4></a>
|
||
<p>另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,如果我们想要计数一些文本中每一个单词分别出现了多少次,就可以使用哈希 map,以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就插入值<code>0</code>。</p>
|
||
<pre><code class="language-rust">use std::collections::HashMap;
|
||
|
||
let text = "hello world wonderful world";
|
||
|
||
let mut map = HashMap::new();
|
||
|
||
for word in text.split_whitespace() {
|
||
let count = map.entry(word).or_insert(0);
|
||
*count += 1;
|
||
}
|
||
|
||
println!("{:?}", map);
|
||
</code></pre>
|
||
<p>这会打印出<code>{"world": 2, "hello": 1, "wonderful": 1}</code>,<code>or_insert</code>方法事实上会返回这个键的值的一个可变引用(<code>&mut V</code>)。这里我们将这个可变引用储存在<code>count</code>变量中,所以为了赋值必须首先使用星号(<code>*</code>)解引用<code>count</code>。这个可变引用在<code>for</code>循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。</p>
|
||
<a class="header" href="#哈希函数" name="哈希函数"><h3>哈希函数</h3></a>
|
||
<p><code>HashMap</code>默认使用一种密码学安全的哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而并不是最快的,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 <em>hasher</em> 来切换为其它函数。hasher 是一个实现了<code>BuildHasher</code> trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>vector、字符串和哈希 map 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题:</p>
|
||
<ul>
|
||
<li>给定一系列数字,使用 vector 并返回这个列表的平均数(mean, average)、中位数(排列数组后位于中间的值)和众数(mode,出现次数最多的值;这里哈希函数会很有帮助)。</li>
|
||
<li>将字符串转换为 Pig Latin,也就是每一个单词的第一个辅音字母被移动到单词的结尾并增加“ay”,所以“first”会变成“irst-fay”。元音字母开头的单词则在结尾增加 “hay”(“apple”会变成“apple-hay”)。牢记 UTF-8 编码!</li>
|
||
<li>使用哈希 map 和 vector,创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如,“Add Sally to Engineering”或“Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表,或者公司每个部门的所有员工按照字母顺排序的列表。</li>
|
||
</ul>
|
||
<p>标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习!</p>
|
||
<p>我们已经开始接触可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!</p>
|
||
<a class="header" href="#错误处理" name="错误处理"><h1>错误处理</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-00-error-handling.md">ch09-00-error-handling.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p>Rust 对可靠性的执着也扩展到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多功能来处理当现错误的情况。在很多情况下,Rust 要求你承认出错的可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。</p>
|
||
<p>Rust 将错误组合成两个主要类别:<strong>可恢复错误</strong>(<em>recoverable</em>)和<strong>不可恢复错误</strong>(<em>unrecoverable</em>)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。</p>
|
||
<p>大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常。相反,对于可恢复错误有<code>Result<T, E></code>值和<code>panic!</code>,它在遇到不可恢复错误时停止程序执行。这一章会首先介绍<code>panic!</code>调用,接着会讲到如何返回<code>Result<T, E></code>。最后,我们会讨论当决定是尝试从错误中恢复还是停止执行时需要顾及的权衡考虑。</p>
|
||
<a class="header" href="#panic与不可恢复的错误" name="panic与不可恢复的错误"><h2><code>panic!</code>与不可恢复的错误</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-01-unrecoverable-errors-with-panic.md">ch09-01-unrecoverable-errors-with-panic.md</a>
|
||
<br>
|
||
commit e26bb338ab14b98a850c3464e821d54940a45672</p>
|
||
</blockquote>
|
||
<p>突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有<code>panic!</code>宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。</p>
|
||
<blockquote>
|
||
<a class="header" href="#panic-中的栈展开与终止" name="panic-中的栈展开与终止"><h3>Panic 中的栈展开与终止</h3></a>
|
||
<p>当出现<code>panic!</code>时,程序默认会开始<strong>展开</strong>(<em>unwinding</em>),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接<strong>终止</strong>(<em>abort</em>),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,可以由 panic 时展开切换为终止,通过在 <em>Cargo.toml</em> 的<code>[profile]</code>部分增加<code>panic = 'abort'</code>。例如,如果你想要在发布模式中 panic 时直接终止:</p>
|
||
<pre><code class="language-toml">[profile.release]
|
||
panic = 'abort'
|
||
</code></pre>
|
||
</blockquote>
|
||
<p>让我们在一个简单的程序中调用<code>panic!</code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,should_panic">fn main() {
|
||
panic!("crash and burn");
|
||
}
|
||
</code></pre>
|
||
<p>运行程序将会出现类似这样的输出:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling panic v0.1.0 (file:///projects/panic)
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.25 secs
|
||
Running `target/debug/panic`
|
||
thread 'main' panicked at 'crash and burn', src/main.rs:2
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
|
||
</code></pre>
|
||
<p>最后三行包含<code>panic!</code>造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:<em>src/main.rs:2</em> 表明这是 <em>src/main.rs</em> 文件的第二行。</p>
|
||
<p>在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现<code>panic!</code>宏的调用。换句话说,<code>panic!</code>可能会出现在我们的代码调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的<code>panic!</code>宏调用,而不是我们代码中最终导致<code>panic!</code>的那一行。可以使用<code>panic!</code>被调用的函数的 backtrace 来寻找(我们代码中出问题的地方)。</p>
|
||
<a class="header" href="#使用panic的-backtrace" name="使用panic的-backtrace"><h3>使用<code>panic!</code>的 backtrace</h3></a>
|
||
<p>让我们来看看另一个因为我们代码中的 bug 引起的别的库中<code>panic!</code>的例子,而不是直接的宏调用:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,should_panic">fn main() {
|
||
let v = vec![1, 2, 3];
|
||
|
||
v[100];
|
||
}
|
||
</code></pre>
|
||
<p>这里尝试访问 vector 的第一百个元素,不过它只有三个元素。这种情况下 Rust 会 panic。<code>[]</code>应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确的元素。</p>
|
||
<p>这种情况下其他像 C 这样语言会尝直接试提供所要求的值,即便这可能不是你期望的:你会得到对任何应 vector 中这个元素的内存位置的值,甚至是这些内存并不属于 vector 的情况。这被称为<strong>缓冲区溢出</strong>(<em>buffer overread</em>),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数组后面不被允许的数据。</p>
|
||
<p>为了使程序远离这类漏洞,如果尝试读取一个索引不存在的元素,Rust 会停止执行并拒绝继续。尝试运行上面的程序会出现如下:</p>
|
||
<pre><code>$ cargo run
|
||
Compiling panic v0.1.0 (file:///projects/panic)
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.27 secs
|
||
Running `target/debug/panic`
|
||
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
|
||
100', /stable-dist-rustc/build/src/libcollections/vec.rs:1362
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
|
||
</code></pre>
|
||
<p>这指向了一个不是我们编写的文件,<em>libcollections/vec.rs</em>。这是标准库中<code>Vec<T></code>的实现。这是当对 vector <code>v</code>使用<code>[]</code>时 <em>libcollections/vec.rs</em> 中会执行的代码,也是真正出现<code>panic!</code>的地方。</p>
|
||
<p>接下来的几行提醒我们可以设置<code>RUST_BACKTRACE</code>环境变量来得到一个 backtrace 来调查究竟是什么导致了错误。让我们来试试看。列表 9-1 显示了其输出:</p>
|
||
<pre><code>$ RUST_BACKTRACE=1 cargo run
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running `target/debug/panic`
|
||
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 100', /stable-dist-rustc/build/src/libcollections/vec.rs:1392
|
||
stack backtrace:
|
||
1: 0x560ed90ec04c - std::sys::imp::backtrace::tracing::imp::write::hf33ae72d0baa11ed
|
||
at /stable-dist-rustc/build/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:42
|
||
2: 0x560ed90ee03e - std::panicking::default_hook::{{closure}}::h59672b733cc6a455
|
||
at /stable-dist-rustc/build/src/libstd/panicking.rs:351
|
||
3: 0x560ed90edc44 - std::panicking::default_hook::h1670459d2f3f8843
|
||
at /stable-dist-rustc/build/src/libstd/panicking.rs:367
|
||
4: 0x560ed90ee41b - std::panicking::rust_panic_with_hook::hcf0ddb069e7abcd7
|
||
at /stable-dist-rustc/build/src/libstd/panicking.rs:555
|
||
5: 0x560ed90ee2b4 - std::panicking::begin_panic::hd6eb68e27bdf6140
|
||
at /stable-dist-rustc/build/src/libstd/panicking.rs:517
|
||
6: 0x560ed90ee1d9 - std::panicking::begin_panic_fmt::abcd5965948b877f8
|
||
at /stable-dist-rustc/build/src/libstd/panicking.rs:501
|
||
7: 0x560ed90ee167 - rust_begin_unwind
|
||
at /stable-dist-rustc/build/src/libstd/panicking.rs:477
|
||
8: 0x560ed911401d - core::panicking::panic_fmt::hc0f6d7b2c300cdd9
|
||
at /stable-dist-rustc/build/src/libcore/panicking.rs:69
|
||
9: 0x560ed9113fc8 - core::panicking::panic_bounds_check::h02a4af86d01b3e96
|
||
at /stable-dist-rustc/build/src/libcore/panicking.rs:56
|
||
10: 0x560ed90e71c5 - <collections::vec::Vec<T> as core::ops::Index<usize>>::index::h98abcd4e2a74c41
|
||
at /stable-dist-rustc/build/src/libcollections/vec.rs:1392
|
||
11: 0x560ed90e727a - panic::main::h5d6b77c20526bc35
|
||
at /home/you/projects/panic/src/main.rs:4
|
||
12: 0x560ed90f5d6a - __rust_maybe_catch_panic
|
||
at /stable-dist-rustc/build/src/libpanic_unwind/lib.rs:98
|
||
13: 0x560ed90ee926 - std::rt::lang_start::hd7c880a37a646e81
|
||
at /stable-dist-rustc/build/src/libstd/panicking.rs:436
|
||
at /stable-dist-rustc/build/src/libstd/panic.rs:361
|
||
at /stable-dist-rustc/build/src/libstd/rt.rs:57
|
||
14: 0x560ed90e7302 - main
|
||
15: 0x7f0d53f16400 - __libc_start_main
|
||
16: 0x560ed90e6659 - _start
|
||
17: 0x0 - <unknown>
|
||
</code></pre>
|
||
<p><span class="caption">Listing 9-1: The backtrace generated by a call to
|
||
<code>panic!</code> displayed when the environment variable <code>RUST_BACKTRACE</code> is set</span></p>
|
||
<p>这里有大量的输出!backtrace 第 11 行指向了我们程序中引起错误的行:<em>src/main.rs</em> 的第四行。backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。</p>
|
||
<p>如果你不希望我们的程序 panic,第一个提到我们编写的代码行的位置是你应该开始调查的,以便查明是什么值如何在这个地方引起了 panic。在上面的例子中,我们故意编写会 panic 的代码来演示如何使用 backtrace,修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你得代码出现了 panic,你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic,以及应当如何处理才能避免这个问题。</p>
|
||
<p>本章的后面会再次回到<code>panic!</code>并讲到何时应该何时不应该使用这个方式。接下来,我们来看看如何使用<code>Result</code>来从错误中恢复。</p>
|
||
<a class="header" href="#result与可恢复的错误" name="result与可恢复的错误"><h2><code>Result</code>与可恢复的错误</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-02-recoverable-errors-with-result.md">ch09-01-unrecoverable-errors-with-panic.md</a>
|
||
<br>
|
||
commit e6d6caab41471f7115a621029bd428a812c5260e</p>
|
||
</blockquote>
|
||
<p>大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反映的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这是我们可能想要创建这个文件而不是终止进程。</p>
|
||
<p>回忆一下第二章“使用<code>Result</code>类型来处理潜在的错误”部分中的那个<code>Result</code>枚举,它定义有如下两个成员,<code>Ok</code>和<code>Err</code>:</p>
|
||
<pre><code class="language-rust">enum Result<T, E> {
|
||
Ok(T),
|
||
Err(E),
|
||
}
|
||
</code></pre>
|
||
<p><code>T</code>和<code>E</code>是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是<code>T</code>代表成功时返回的<code>Ok</code>成员中的数据的类型,而<code>E</code>代表失败时返回的<code>Err</code>成员中的错误的类型。因为<code>Result</code>有这些泛型类型参数,我们可以将<code>Result</code>类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。</p>
|
||
<p>让我们调用一个返回<code>Result</code>的函数,因为它可能会失败:如列表 9-2 所示打开一个文件:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::fs::File;
|
||
|
||
fn main() {
|
||
let f = File::open("hello.txt");
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 9-2: Opening a file</span></p>
|
||
<p>如何知道<code>File::open</code>返回一个<code>Result</code>呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给<code>f</code>某个我们知道<strong>不是</strong>函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们<code>f</code>的类型<strong>应该</strong>是什么,为此我们将<code>let f</code>语句改为:</p>
|
||
<pre><code class="language-rust,ignore">let f: u32 = File::open("hello.txt");
|
||
</code></pre>
|
||
<p>现在尝试编译会给出如下错误:</p>
|
||
<pre><code>error[E0308]: mismatched types
|
||
--> src/main.rs:4:18
|
||
|
|
||
4 | let f: u32 = File::open("hello.txt");
|
||
| ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
|
||
`std::result::Result`
|
||
|
|
||
= note: expected type `u32`
|
||
= note: found type `std::result::Result<std::fs::File, std::io::Error>`
|
||
</code></pre>
|
||
<p>这就告诉我们了<code>File::open</code>函数的返回值类型是<code>Result<T, E></code>。这里泛型参数<code>T</code>放入了成功值的类型<code>std::fs::File</code>,它是一个文件句柄。<code>E</code>被用在失败值上其类型是<code>std::io::Error</code>。</p>
|
||
<p>这个返回值类型说明<code>File::open</code>调用可能会成功并返回一个可以进行读写的文件句柄。这个函数也可能会失败:例如,文件可能并不存在,或者可能没有访问文件的权限。<code>File::open</code>需要一个方式告诉我们是成功还是失败,并同时提供给我们文件句柄或错误信息。而这些信息正是<code>Result</code>枚举可以提供的。</p>
|
||
<p>当<code>File::open</code>成功的情况下,变量<code>f</code>的值将会是一个包含文件句柄的<code>Ok</code>实例。在失败的情况下,<code>f</code>会是一个包含更多关于出现了何种错误信息的<code>Err</code>实例。</p>
|
||
<p>我们需要在列表 9-2 的代码中增加根据<code>File::open</code>返回值进行不同处理的逻辑。列表 9-3 展示了一个处理<code>Result</code>的基本工具:第六章学习过的<code>match</code>表达式。</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,should_panic">use std::fs::File;
|
||
|
||
fn main() {
|
||
let f = File::open("hello.txt");
|
||
|
||
let f = match f {
|
||
Ok(file) => file,
|
||
Err(error) => {
|
||
panic!("There was a problem opening the file: {:?}", error)
|
||
},
|
||
};
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 9-3: Using a <code>match</code> expression to handle the
|
||
<code>Result</code> variants we might have</span></p>
|
||
<p>注意与<code>Option</code>枚举一样,<code>Result</code>枚举和其成员也被导入到了 prelude 中,所以就不需要在<code>match</code>分支中的<code>Ok</code>和<code>Err</code>之前指定<code>Result::</code>。</p>
|
||
<p>这里我们告诉 Rust 当结果是<code>Ok</code>,返回<code>Ok</code>成员中的<code>file</code>值,然后将这个文件句柄赋值给变量<code>f</code>。<code>match</code>之后,我们可以利用这个文件句柄来进行读写。</p>
|
||
<p><code>match</code>的另一个分支处理从<code>File::open</code>得到<code>Err</code>值的情况。在这种情况下,我们选择调用<code>panic!</code>宏。如果当前目录没有一个叫做 <em>hello.txt</em> 的文件,当运行这段代码时会看到如下来自<code>panic!</code>宏的输出:</p>
|
||
<pre><code>thread 'main' panicked at 'There was a problem opening the file: Error { repr:
|
||
Os { code: 2, message: "No such file or directory" } }', src/main.rs:8
|
||
</code></pre>
|
||
<a class="header" href="#匹配不同的错误" name="匹配不同的错误"><h3>匹配不同的错误</h3></a>
|
||
<p>列表 9-3 中的代码不管<code>File::open</code>是因为什么原因失败都会<code>panic!</code>。我们真正希望的是对不同的错误原因采取不同的行为:如果<code>File::open</code>因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果<code>File::open</code>因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像列表 9-3 那样<code>panic!</code>。让我们看看列表 9-4,其中<code>match</code>增加了另一个分支:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">use std::fs::File;
|
||
use std::io::ErrorKind;
|
||
|
||
fn main() {
|
||
let f = File::open("hello.txt");
|
||
|
||
let f = match f {
|
||
Ok(file) => file,
|
||
Err(ref error) if error.kind() == ErrorKind::NotFound => {
|
||
match File::create("hello.txt") {
|
||
Ok(fc) => fc,
|
||
Err(e) => {
|
||
panic!(
|
||
"Tried to create file but there was a problem: {:?}",
|
||
e
|
||
)
|
||
},
|
||
}
|
||
},
|
||
Err(error) => {
|
||
panic!(
|
||
"There was a problem opening the file: {:?}",
|
||
error
|
||
)
|
||
},
|
||
};
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 9-4: Handling different kinds of errors in
|
||
different ways</span></p>
|
||
<p><code>File::open</code>返回的<code>Err</code>成员中的值类型<code>io::Error</code>,它是一个标准库中提供的结构体。这个结构体有一个返回<code>io::ErrorKind</code>值的<code>kind</code>方法可供调用。<code>io::ErrorKind</code>是一个标准库提供的枚举,它的成员对应<code>io</code>操作可能导致的不同错误类型。我们感兴趣的成员是<code>ErrorKind::NotFound</code>,它代表尝试打开的文件并不存在。</p>
|
||
<p>条件<code>if error.kind() == ErrorKind::NotFound</code>被称作 <em>match guard</em>:它是一个进一步完善<code>match</code>分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑<code>match</code>中的下一个分支。模式中的<code>ref</code>是必须的,这样<code>error</code>就不会被移动到 guard 条件中而只是仅仅引用它。第十八章会详细解释为什么在模式中使用<code>ref</code>而不是<code>&</code>来获取一个引用。简而言之,在模式的上下文中,<code>&</code>匹配一个引用并返回它的值,而<code>ref</code>匹配一个值并返回一个引用。</p>
|
||
<p>在 match guard 中我们想要检查的条件是<code>error.kind()</code>是否是<code>ErrorKind</code>枚举的<code>NotFound</code>成员。如果是,尝试用<code>File::create</code>创建文件。然而<code>File::create</code>也可能会失败,我们还需要增加一个内部<code>match</code>语句。当文件不能被打开,会打印出一个不同的错误信息。外部<code>match</code>的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。</p>
|
||
<a class="header" href="#失败时-panic-的捷径unwrap和expect" name="失败时-panic-的捷径unwrap和expect"><h3>失败时 panic 的捷径:<code>unwrap</code>和<code>expect</code></h3></a>
|
||
<p><code>match</code>能够胜任它的工作,不过它可能有点冗长并且并不总是能很好的表明意图。<code>Result<T, E></code>类型定义了很多辅助方法来处理各种情况。其中之一叫做<code>unwrap</code>,它的实现就类似于列表 9-3 中的<code>match</code>语句。如果<code>Result</code>值是成员<code>Ok</code>,<code>unwrap</code>会返回<code>Ok</code>中的值。如果<code>Result</code>是成员<code>Err</code>,<code>unwrap</code>会为我们调用<code>panic!</code>。</p>
|
||
<pre><code class="language-rust,should_panic">use std::fs::File;
|
||
|
||
fn main() {
|
||
let f = File::open("hello.txt").unwrap();
|
||
}
|
||
</code></pre>
|
||
<p>如果调用这段代码时不存在 <em>hello.txt</em> 文件,我们将会看到一个<code>unwrap</code>调用<code>panic!</code>时提供的错误信息:</p>
|
||
<pre><code>thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
|
||
repr: Os { code: 2, message: "No such file or directory" } }',
|
||
/stable-dist-rustc/build/src/libcore/result.rs:868
|
||
</code></pre>
|
||
<p>还有另一个类似于<code>unwrap</code>的方法它还允许我们选择<code>panic!</code>的错误信息:<code>expect</code>。使用<code>expect</code>而不是<code>unwrap</code>并提供一个好的错误信息可以表明你的意图并有助于追踪 panic 的根源。<code>expect</code>的语法看起来像这样:</p>
|
||
<pre><code class="language-rust,should_panic">use std::fs::File;
|
||
|
||
fn main() {
|
||
let f = File::open("hello.txt").expect("Failed to open hello.txt");
|
||
}
|
||
</code></pre>
|
||
<p><code>expect</code>与<code>unwrap</code>的使用方式一样:返回文件句柄或调用<code>panic!</code>宏。<code>expect</code>用来调用<code>panic!</code>的错误信息将会作为传递给<code>expect</code>的参数,而不像<code>unwrap</code>那样使用默认的<code>panic!</code>信息。它看起来像这样:</p>
|
||
<pre><code>thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
|
||
2, message: "No such file or directory" } }',
|
||
/stable-dist-rustc/build/src/libcore/result.rs:868
|
||
</code></pre>
|
||
<a class="header" href="#传播错误" name="传播错误"><h3>传播错误</h3></a>
|
||
<p>当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为<strong>传播</strong>(<em>propagating</em>)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。</p>
|
||
<p>例如,列表 9-5 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:</p>
|
||
<pre><code class="language-rust">use std::io;
|
||
use std::io::Read;
|
||
use std::fs::File;
|
||
|
||
fn read_username_from_file() -> Result<String, io::Error> {
|
||
let f = File::open("hello.txt");
|
||
|
||
let mut f = match f {
|
||
Ok(file) => file,
|
||
Err(e) => return Err(e),
|
||
};
|
||
|
||
let mut s = String::new();
|
||
|
||
match f.read_to_string(&mut s) {
|
||
Ok(_) => Ok(s),
|
||
Err(e) => Err(e),
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 9-5: A function that returns errors to the
|
||
calling code using <code>match</code></span></p>
|
||
<p>首先让我们看看函数的返回值:<code>Result<String, io::Error></code>。这意味着函数返回一个<code>Result<T, E></code>类型的值,其中泛型参数<code>T</code>的具体类型是<code>String</code>,而<code>E</code>的具体类型是<code>io::Error</code>。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含<code>String</code>的<code>Ok</code>值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个<code>Err</code>值,它储存了一个包含更多这个问题相关信息的<code>io::Error</code>实例。我们选择<code>io::Error</code>作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:<code>File::open</code>函数和<code>read_to_string</code>方法。</p>
|
||
<p>函数体以<code>File::open</code>函数开头。接着使用<code>match</code>处理返回值<code>Result</code>,类似于列表 9-3 中的<code>match</code>,唯一的区别是不再当<code>Err</code>时调用<code>panic!</code>,而是提早返回并将<code>File::open</code>返回的错误值作为函数的错误返回值传递给调用者。如果<code>File::open</code>成功了,我们将文件句柄储存在变量<code>f</code>中并继续。</p>
|
||
<p>接着我们在变量<code>s</code>中创建了一个新<code>String</code>并调用文件句柄<code>f</code>的<code>read_to_string</code>方法来将文件的内容读取到<code>s</code>中。<code>read_to_string</code>方法也返回一个<code>Result</code>因为它也可能会失败:哪怕是<code>File::open</code>已经成功了。所以我们需要另一个<code>match</code>来处理这个<code>Result</code>:如果<code>read_to_string</code>成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进<code>Ok</code>的<code>s</code>中。如果<code>read_to_string</code>失败了,则像之前处理<code>File::open</code>的返回值的<code>match</code>那样返回错误值。并不需要显式的调用<code>return</code>,因为这是函数的最后一个表达式。</p>
|
||
<p>调用这个函数的代码最终会得到一个包含用户名的<code>Ok</code>值,亦或一个包含<code>io::Error</code>的<code>Err</code>值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个<code>Err</code>值,他们可能会选择<code>panic!</code>并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适处理方法。</p>
|
||
<p>这种传播错误的模式在 Rust 是如此的常见,以至于有一个更简便的专用语法:<code>?</code>。</p>
|
||
<a class="header" href="#传播错误的捷径" name="传播错误的捷径"><h3>传播错误的捷径:<code>?</code></h3></a>
|
||
<p>列表 9-6 展示了一个<code>read_username_from_file</code>的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符的:</p>
|
||
<pre><code class="language-rust">use std::io;
|
||
use std::io::Read;
|
||
use std::fs::File;
|
||
|
||
fn read_username_from_file() -> Result<String, io::Error> {
|
||
let mut f = File::open("hello.txt")?;
|
||
let mut s = String::new();
|
||
f.read_to_string(&mut s)?;
|
||
Ok(s)
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 9-6: A function that returns errors to the
|
||
calling code using <code>?</code></span></p>
|
||
<p><code>Result</code>值之后的<code>?</code>被定义为与列表 9-5 中定义的处理<code>Result</code>值的<code>match</code>表达式有着完全相同的工作方式。如果<code>Result</code>的值是<code>Ok</code>,这个表达式将会返回<code>Ok</code>中的值而程序将继续执行。如果值是<code>Err</code>,<code>Err</code>中的值将作为整个函数的返回值,就好像使用了<code>return</code>关键字一样,这样错误值就被传播给了调用者。</p>
|
||
<p>在列表 9-6 的上下文中,<code>File::open</code>调用结尾的<code>?</code>将会把<code>Ok</code>中的值返回给变量<code>f</code>。如果出现了错误,<code>?</code>会提早返回整个函数并将任何<code>Err</code>值传播给调用者。同理也适用于<code>read_to_string</code>调用结尾的<code>?</code>。</p>
|
||
<p><code>?</code>消除了大量样板代码并使得函数的实现更简单。我们甚至可以在<code>?</code>之后直接使用链式方法调用来进一步缩短代码:</p>
|
||
<pre><code class="language-rust">use std::io;
|
||
use std::io::Read;
|
||
use std::fs::File;
|
||
|
||
fn read_username_from_file() -> Result<String, io::Error> {
|
||
let mut s = String::new();
|
||
|
||
File::open("hello.txt")?.read_to_string(&mut s)?;
|
||
|
||
Ok(s)
|
||
}
|
||
</code></pre>
|
||
<p>在<code>s</code>中创建新的<code>String</code>被放到了函数开头;这没有什么变化。我们对<code>File::open("hello.txt")?</code>的结果直接链式调用了<code>read_to_string</code>,而不再创建变量<code>f</code>。仍然需要<code>read_to_string</code>调用结尾的<code>?</code>,而且当<code>File::open</code>和<code>read_to_string</code>都成功没有失败时返回包含用户名<code>s</code>的<code>Ok</code>值。其功能再一次与列表 9-5 和列表 9-5 保持一致,不过这是一个与众不同且更符合工程学的写法。</p>
|
||
<a class="header" href="#只能被用于返回result的函数" name="只能被用于返回result的函数"><h3><code>?</code>只能被用于返回<code>Result</code>的函数</h3></a>
|
||
<p><code>?</code>只能被用于返回值类型为<code>Result</code>的函数,因为他被定义为与列表 9-5 中的<code>match</code>表达式有着完全相同的工作方式。<code>match</code>的<code>return Err(e)</code>部分要求返回值类型是<code>Result</code>,所以函数的返回值必须是<code>Result</code>才能与这个<code>return</code>相兼容。</p>
|
||
<p>让我们看看在<code>main</code>函数中使用<code>?</code>会发生什么,如果你还记得的话它的返回值类型是<code>()</code>:</p>
|
||
<pre><code class="language-rust,ignore">use std::fs::File;
|
||
|
||
fn main() {
|
||
let f = File::open("hello.txt")?;
|
||
}
|
||
</code></pre>
|
||
<!-- NOTE: as of 2016-12-21, the error message when calling `?` in a function
|
||
that doesn't return a result is STILL confusing. Since we want to only explain
|
||
`?` now, I've changed the example, but if you try running this code you WON'T
|
||
get the error message below.
|
||
<p>I'm bugging people to try and get
|
||
https://github.com/rust-lang/rust/issues/35946 fixed soon, hopefully before this
|
||
chapter gets through copy editing-- at that point I'll make sure to update this
|
||
error message. /Carol --></p>
|
||
<p>当编译这些代码,会得到如下错误信息:</p>
|
||
<pre><code>error[E0308]: mismatched types
|
||
-->
|
||
|
|
||
3 | let f = File::open("hello.txt")?;
|
||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ expected (), found enum
|
||
`std::result::Result`
|
||
|
|
||
= note: expected type `()`
|
||
= note: found type `std::result::Result<_, _>`
|
||
</code></pre>
|
||
<p>错误指出存在不匹配的类型:<code>main</code>函数返回一个<code>()</code>类型,而<code>?</code>返回一个<code>Result</code>。编写不返回<code>Result</code>的函数时,如果调用其他返回<code>Result</code>的函数,需要使用<code>match</code>或者<code>Result</code>的方法之一来处理它,而不能用<code>?</code>将潜在的错误传播给调用者。</p>
|
||
<p>现在我们讨论过了调用<code>panic!</code>或返回<code>Result</code>的细节,是时候返回他们各自适合哪些场景的话题了。</p>
|
||
<a class="header" href="#panic还是不panic" name="panic还是不panic"><h2><code>panic!</code>还是不<code>panic!</code></h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-03-to-panic-or-not-to-panic.md">ch09-03-to-panic-or-not-to-panic.md</a>
|
||
<br>
|
||
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56</p>
|
||
</blockquote>
|
||
<p>那么,该如何决定何时应该<code>panic!</code>以及何时应该返回<code>Result</code>呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用<code>panic!</code>,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回<code>Result</code>值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为<code>Err</code>是不可恢复的,所以他们也可能会调用<code>panic!</code>并将可恢复的错误变成了不可恢复的错误。因此返回<code>Result</code>是定义可能会失败的函数的一个好的默认选择。</p>
|
||
<p>有一些情况 panic 比返回<code>Result</code>更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的,最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。</p>
|
||
<a class="header" href="#示例代码原型和测试非常适合-panic" name="示例代码原型和测试非常适合-panic"><h3>示例、代码原型和测试:非常适合 panic</h3></a>
|
||
<p>当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。例如,调用一个类似<code>unwrap</code>这样可能<code>panic!</code>的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。</p>
|
||
<p>类似的,<code>unwrap</code>和<code>expect</code>方法在原型设计时非常方便,在你决定该如何处理错误之前。他们在代码中留下了明显的记号,以便你准备使程序变得更健壮时作为参考。</p>
|
||
<p>如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为<code>panic!</code>是测试如何被标记为失败的,调用<code>unwrap</code>或<code>expect</code>都是非常有道理的。</p>
|
||
<a class="header" href="#当你比编译器知道更多的情况" name="当你比编译器知道更多的情况"><h3>当你比编译器知道更多的情况</h3></a>
|
||
<p>当你有一些其他的逻辑来确保<code>Result</code>会是<code>Ok</code>值的时候调用<code>unwrap</code>也是合适的,虽然编译器无法理解这种逻辑。仍然会有一个<code>Result</code>值等着你处理:总的来说你调用的任何操作都有失败的可能性,即便在特定情况下逻辑上是不可能的。如果通过人工检查代码来确保永远也不会出现<code>Err</code>值,那么调用<code>unwrap</code>也是完全可以接受的,这里是一个例子:</p>
|
||
<pre><code class="language-rust">use std::net::IpAddr;
|
||
|
||
let home = "127.0.0.1".parse::<IpAddr>().unwrap();
|
||
</code></pre>
|
||
<p>我们通过解析一个硬编码的字符来创建一个<code>IpAddr</code>实例。可以看出<code>127.0.0.1</code>是一个有效的 IP 地址,所以这里使用<code>unwrap</code>是没有问题的。然而,拥有一个硬编码的有效的字符串也不能改变<code>parse</code>方法的返回值类型:它仍然是一个<code>Result</code>值,而编译器仍然就好像还是有可能出现<code>Err</code>成员那样要求我们处理<code>Result</code>,因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就<strong>确实</strong>有失败的可能性,这时就绝对需要我们以一种更健壮的方式处理<code>Result</code>了。</p>
|
||
<a class="header" href="#错误处理指导原则" name="错误处理指导原则"><h3>错误处理指导原则</h3></a>
|
||
<p>在当有可能会导致有害状态的情况下建议使用<code>panic!</code>——在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值——外加如下几种情况:</p>
|
||
<ul>
|
||
<li>有害状态并不包含<strong>预期</strong>会偶尔发生的错误</li>
|
||
<li>之后的代码的运行依赖于不再处于这种有害状态</li>
|
||
<li>当没有可行的手段来将有害状态信息编码进可用的类型中(并返回)的情况</li>
|
||
</ul>
|
||
<p>如果别人调用你的代码并传递了一个没有意义的值,最好的情况也许就是<code>panic!</code>并警告使用你的库的人他的代码中有 bug 以便他能在开发时就修复它。类似的,<code>panic!</code>通常适合调用不能够控制的外部代码时,这时无法修复其返回的无效状态。</p>
|
||
<p>无论代码编写的多么好,当有害状态是预期会出现时,返回<code>Result</code>仍要比调用<code>panic!</code>更为合适。这样的例子包括解析器接收到错误数据,或者 HTTP 请求返回一个表明触发了限流的状态。在这些例子中,应该通过返回<code>Result</code>来表明失败预期是可能的,这样将有害状态向上传播,这样调用者就可以决定该如何处理这个问题。使用<code>panic!</code>来处理这些情况就不是最好的选择。</p>
|
||
<p>当代码对值进行操作时,应该首先验证值是有效的,并在其无效时<code>panic!</code>。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会<code>panic!</code>的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循<strong>契约</strong>(<em>contracts</em>):他们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这这通常代表调用方的 bug,而且这也不是那种你希望必须去处理的错误。事实上也没有合理的方式来恢复调用方的代码:调用方的<strong>程序员</strong>需要修复他的代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。</p>
|
||
<p>虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个不同于<code>Option</code>的类型,而且程序期望它是<strong>有值</strong>的而不是<strong>空值</strong>。你的代码无需处理<code>Some</code>和<code>None</code>这两种情况,它只会有一种情况且绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像<code>u32</code>这样的无符号整型,也会确保它永远不为负。</p>
|
||
<a class="header" href="#创建自定义类型作为验证" name="创建自定义类型作为验证"><h3>创建自定义类型作为验证</h3></a>
|
||
<p>让我们借用 Rust 类型系统的思想来进一步确保值的有效性,并尝试创建一个自定义类型作为验证。回忆一下第二章的猜猜看游戏,它的代码请求用户猜测一个 1 到 100 之间的数字,在将其与秘密数字做比较之前我们事实上从未验证用户的猜测是位于这两个数字之间的,只保证它为正。在当前情况下,其影响并不是很严重:“Too high”或“Too low”的输出仍然是正确的。但是这是一个很好的引导用户得出有效猜测的辅助,例如当用户猜测一个超出范围的数字和输入字母时采取不同的行为。</p>
|
||
<p>一种实现方式是将猜测解析成<code>i32</code>而不仅仅是<code>u32</code>,来默许输入负数,接着检查数字是否在范围内:</p>
|
||
<pre><code class="language-rust,ignore">loop {
|
||
// snip
|
||
|
||
let guess: i32 = match guess.trim().parse() {
|
||
Ok(num) => num,
|
||
Err(_) => continue,
|
||
};
|
||
|
||
if guess < 1 || guess > 100 {
|
||
println!("The secret number will be between 1 and 100.");
|
||
continue;
|
||
}
|
||
|
||
match guess.cmp(&secret_number) {
|
||
// snip
|
||
}
|
||
</code></pre>
|
||
<p><code>if</code>表达式检查了值是否超出范围,告诉用户出了什么问题,并调用<code>continue</code>开始下一次循环,请求另一个猜测。<code>if</code>表达式之后,就可以在知道<code>guess</code>在 1 到 100 之间的情况下与秘密数字作比较了。</p>
|
||
<p>然而,这并不是一个理想的解决方案:程序只处理 1 到 100 之间的值是绝对不可取的,而且如果有很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。</p>
|
||
<p>相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。列表 9-8 中展示了一个定义<code>Guess</code>类型的方法,只有在<code>new</code>函数接收到 1 到 100 之间的值时才会创建<code>Guess</code>的实例:</p>
|
||
<pre><code class="language-rust">struct Guess {
|
||
value: u32,
|
||
}
|
||
|
||
impl Guess {
|
||
pub fn new(value: u32) -> Guess {
|
||
if value < 1 || value > 100 {
|
||
panic!("Guess value must be between 1 and 100, got {}.", value);
|
||
}
|
||
|
||
Guess {
|
||
value: value,
|
||
}
|
||
}
|
||
|
||
pub fn value(&self) -> u32 {
|
||
self.value
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 9-8: A <code>Guess</code> type that will only continue with
|
||
values between 1 and 100</span></p>
|
||
<p>首先,我们定义了一个包含<code>u32</code>类型字段<code>value</code>的结构体<code>Guess</code>。这里是储存猜测值的地方。</p>
|
||
<p>接着在<code>Guess</code>上实现了一个叫做<code>new</code>的关联函数来创建<code>Guess</code>的实例。<code>new</code>定义为接收一个<code>u32</code>类型的参数<code>value</code>并返回一个<code>Guess</code>。<code>new</code>函数中代码的测试确保了其值是在 1 到 100 之间的。如果<code>value</code>没有通过测试则调用<code>panic!</code>,这会警告调用这个函数的程序员有一个需要修改的 bug,因为创建一个<code>value</code>超出范围的<code>Guess</code>将会违反<code>Guess::new</code>所遵循的契约。<code>Guess::new</code>会出现 panic 的条件应该在其公有 API 文档中被提及;第十四章会涉及到在 API 文档中表明<code>panic!</code>可能性的相关规则。如果<code>value</code>通过了测试,我们新建一个<code>Guess</code>,其字段<code>value</code>将被设置为参数<code>value</code>的值,接着返回这个<code>Guess</code>。</p>
|
||
<p>接着,我们实现了一个借用了<code>self</code>的方法<code>value</code>,它没有任何其他参数并返回一个<code>u32</code>。这类方法有时被称为 <em>getter</em>,因为它的目的就是返回对应字段的数据。这样的公有方法是必要的,因为<code>Guess</code>结构体的<code>value</code>字段是私有的。私有的字段<code>value</code>是很重要的,这样使用<code>Guess</code>结构体的代码将不允许直接设置<code>value</code>的值:调用者<strong>必须</strong>使用<code>Guess::new</code>方法来创建一个<code>Guess</code>的实例,这就确保了不会存在一个<code>value</code>没有通过<code>Guess::new</code>函数的条件检查的<code>Guess</code>。</p>
|
||
<p>如此获取一个参数并只返回 1 到 100 之间数字的函数就可以声明为获取或返回一个<code>Guess</code>,而不是<code>u32</code>,同时其函数体中也无需进行任何额外的检查。</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。<code>panic!</code>宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的<code>Result</code>枚举代表操作可能会在一种可以恢复的情况下失败。可以使用<code>Result</code>来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用<code>panic!</code>和<code>Result</code>将会使你的代码在面对无处不在的错误时显得更加可靠。</p>
|
||
<p>现在我们已经见识过了标准库中<code>Option</code>和<code>Result</code>泛型枚举的能力了,让我们聊聊泛型是如何工作的,以及如何在你的代码中利用他们。</p>
|
||
<a class="header" href="#泛型trait-和生命周期" name="泛型trait-和生命周期"><h1>泛型、trait 和生命周期</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-00-generics.md">ch10-00-generics.md</a>
|
||
<br>
|
||
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56</p>
|
||
</blockquote>
|
||
<p>每一个编程语言都有高效的处理重复概念的工具;在 Rust 中工具之一就是<strong>泛型</strong>(<em>generics</em>)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。</p>
|
||
<p>同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像<code>i32</code>或<code>String</code>这样的具体值。我们已经使用过第六章的<code>Option<T></code>,第八章的<code>Vec<T></code>和<code>HashMap<K, V></code>,以及第九章的<code>Result<T, E></code>这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法!</p>
|
||
<p>首先,我们将回顾一下提取函数以减少代码重复的机制。接着使用一个只在参数类型上不同的泛型函数来实现相同的功能。我们也会讲到结构体和枚举定义中的泛型。</p>
|
||
<p>之后,我们讨论 <em>traits</em>,这是一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型。</p>
|
||
<p>最后介绍<strong>生命周期</strong>(<em>lifetimes</em>),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值同时仍然使编译器能够检查这些引用的有效性。</p>
|
||
<a class="header" href="#提取函数来减少重复" name="提取函数来减少重复"><h2>提取函数来减少重复</h2></a>
|
||
<p>在介绍泛型语法之前,首先来回顾一个不使用泛型的处理重复的技术:提取一个函数。当熟悉了这个技术以后,我们将使用相同的机制来提取一个泛型函数!如同你识别出可以提取到函数中重复代码那样,你也会开始识别出能够使用泛型的重复代码。</p>
|
||
<p>考虑一下这个寻找列表中最大值的小程序,如列表 10-1 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let numbers = vec![34, 50, 25, 100, 65];
|
||
|
||
let mut largest = numbers[0];
|
||
|
||
for number in numbers {
|
||
if number > largest {
|
||
largest = number;
|
||
}
|
||
}
|
||
|
||
println!("The largest number is {}", largest);
|
||
# assert_eq!(largest, 100);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-1: Code to find the largest number in a list
|
||
of numbers</span></p>
|
||
<p>这段代码获取一个整型列表,存放在变量<code>numbers</code>中。它将列表的第一项放入了变量<code>largest</code>中。接着遍历了列表中的所有数字,如果当前值大于<code>largest</code>中储存的值,将<code>largest</code>替换为这个值。如果当前值小于目前为止的最大值,<code>largest</code>保持不变。当列表中所有值都被考虑到之后,<code>largest</code>将会是最大值,在这里也就是 100。</p>
|
||
<p>如果需要在两个不同的列表中寻找最大值,我们可以重复列表 10-1 中的代码这样程序中就会存在两段相同逻辑的代码,如列表 10-2 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let numbers = vec![34, 50, 25, 100, 65];
|
||
|
||
let mut largest = numbers[0];
|
||
|
||
for number in numbers {
|
||
if number > largest {
|
||
largest = number;
|
||
}
|
||
}
|
||
|
||
println!("The largest number is {}", largest);
|
||
|
||
let numbers = vec![102, 34, 6000, 89, 54, 2, 43, 8];
|
||
|
||
let mut largest = numbers[0];
|
||
|
||
for number in numbers {
|
||
if number > largest {
|
||
largest = number;
|
||
}
|
||
}
|
||
|
||
println!("The largest number is {}", largest);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-2: Code to find the largest number in <em>two</em>
|
||
lists of numbers</span></p>
|
||
<p>虽然代码能够执行,但是重复的代码是冗余且容易出错的,并且意味着当更新逻辑时需要修改多处地方的代码。</p>
|
||
<!-- Are we safe assuming the reader will be familiar with the term
|
||
"abstraction" in this context, or do we want to give a brief definition? -->
|
||
<!-- Yes, our audience will be familiar with this term. /Carol -->
|
||
<p>为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独立。</p>
|
||
<p>在列表 10-3 的程序中将寻找最大值的代码提取到了一个叫做<code>largest</code>的函数中。这个程序可以找出两个不同数字列表的最大值,不过列表 10-1 中的代码只存在于一个位置:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn largest(list: &[i32]) -> i32 {
|
||
let mut largest = list[0];
|
||
|
||
for &item in list.iter() {
|
||
if item > largest {
|
||
largest = item;
|
||
}
|
||
}
|
||
|
||
largest
|
||
}
|
||
|
||
fn main() {
|
||
let numbers = vec![34, 50, 25, 100, 65];
|
||
|
||
let result = largest(&numbers);
|
||
println!("The largest number is {}", result);
|
||
# assert_eq!(result, 100);
|
||
|
||
let numbers = vec![102, 34, 6000, 89, 54, 2, 43, 8];
|
||
|
||
let result = largest(&numbers);
|
||
println!("The largest number is {}", result);
|
||
# assert_eq!(result, 6000);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-3: Abstracted code to find the largest number
|
||
in two lists</span></p>
|
||
<p>这个函数有一个参数<code>list</code>,它代表会传递给函数的任何具体<code>i32</code>值的 slice。函数定义中的<code>list</code>代表任何<code>&[i32]</code>。当调用<code>largest</code>函数时,其代码实际上运行于我们传递的特定值上。</p>
|
||
<p>从列表 10-2 到列表 10-3 中涉及的机制经历了如下几步:</p>
|
||
<ol>
|
||
<li>我们注意到了重复代码。</li>
|
||
<li>我们将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。</li>
|
||
<li>我们将两个具体的存在重复代码的位置替换为了函数调用。</li>
|
||
</ol>
|
||
<p>在不同的场景使用不同的方式泛型也可以利用相同的步骤来减少重复代码。与函数体中现在作用于一个抽象的<code>list</code>而不是具体值一样,使用泛型的代码也作用于抽象类型。支持泛型背后的概念与你已经了解的支持函数的概念是一样的,不过是实现方式不同。</p>
|
||
<p>如果我们有两个函数,一个寻找一个<code>i32</code>值的 slice 中的最大项而另一个寻找<code>char</code>值的 slice 中的最大项该怎么办?该如何消除重复呢?让我们拭目以待!</p>
|
||
<a class="header" href="#泛型数据类型" name="泛型数据类型"><h2>泛型数据类型</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-01-syntax.md">ch10-01-syntax.md</a>
|
||
<br>
|
||
commit 55d9e75ffec92e922273c997026bb10613a76578</p>
|
||
</blockquote>
|
||
<p>泛型用于通常我们放置类型的位置,比如函数签名或结构体,允许我们创建可以代替许多具体数据类型的结构体定义。让我们看看如何使用泛型定义函数、结构体、枚举和方法,并且在本部分的结尾我们会讨论泛型代码的性能。</p>
|
||
<a class="header" href="#在函数定义中使用泛型" name="在函数定义中使用泛型"><h3>在函数定义中使用泛型</h3></a>
|
||
<p>定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。</p>
|
||
<p>回到<code>largest</code>函数上,列表 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从列表 10-3 中提取的寻找 slice 中<code>i32</code>最大值的函数。第二个函数寻找 slice 中<code>char</code>的最大值:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn largest_i32(list: &[i32]) -> i32 {
|
||
let mut largest = list[0];
|
||
|
||
for &item in list.iter() {
|
||
if item > largest {
|
||
largest = item;
|
||
}
|
||
}
|
||
|
||
largest
|
||
}
|
||
|
||
fn largest_char(list: &[char]) -> char {
|
||
let mut largest = list[0];
|
||
|
||
for &item in list.iter() {
|
||
if item > largest {
|
||
largest = item;
|
||
}
|
||
}
|
||
|
||
largest
|
||
}
|
||
|
||
fn main() {
|
||
let numbers = vec![34, 50, 25, 100, 65];
|
||
|
||
let result = largest_i32(&numbers);
|
||
println!("The largest number is {}", result);
|
||
# assert_eq!(result, 100);
|
||
|
||
let chars = vec!['y', 'm', 'a', 'q'];
|
||
|
||
let result = largest_char(&chars);
|
||
println!("The largest char is {}", result);
|
||
# assert_eq!(result, 'y');
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-4: Two functions that differ only in their
|
||
names and the types in their signatures</span></p>
|
||
<p>这里<code>largest_i32</code>和<code>largest_char</code>有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现!</p>
|
||
<p>为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称<code>T</code>。任何标识符都可以作为类型参数名,选择<code>T</code>是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。<code>T</code>作为“type”的缩写是大部分 Rust 程序员的首选。</p>
|
||
<p>当需要在函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。</p>
|
||
<p>我们将要定义的泛型版本的<code>largest</code>函数的签名看起来像这样:</p>
|
||
<pre><code class="language-rust,ignore">fn largest<T>(list: &[T]) -> T {
|
||
</code></pre>
|
||
<p>这可以理解为:函数<code>largest</code>有泛型类型<code>T</code>。它有一个参数<code>list</code>,它的类型是一个<code>T</code>值的 slice。<code>largest</code>函数将会返回一个与<code>T</code>相同类型的值。</p>
|
||
<p>列表 10-5 展示一个在签名中使用了泛型的统一的<code>largest</code>函数定义,并向我们展示了如何对<code>i32</code>值的 slice 或<code>char</code>值的 slice 调用<code>largest</code>函数。注意这些代码还不能编译!</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn largest<T>(list: &[T]) -> T {
|
||
let mut largest = list[0];
|
||
|
||
for &item in list.iter() {
|
||
if item > largest {
|
||
largest = item;
|
||
}
|
||
}
|
||
|
||
largest
|
||
}
|
||
|
||
fn main() {
|
||
let numbers = vec![34, 50, 25, 100, 65];
|
||
|
||
let result = largest(&numbers);
|
||
println!("The largest number is {}", result);
|
||
|
||
let chars = vec!['y', 'm', 'a', 'q'];
|
||
|
||
let result = largest(&chars);
|
||
println!("The largest char is {}", result);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-5: A definition of the <code>largest</code> function that
|
||
uses generic type parameters but doesn't compile yet</span></p>
|
||
<p>如果现在就尝试编译这些代码,会出现如下错误:</p>
|
||
<pre><code>error[E0369]: binary operation `>` cannot be applied to type `T`
|
||
|
|
||
5 | if item > largest {
|
||
| ^^^^
|
||
|
|
||
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
|
||
</code></pre>
|
||
<p>注释中提到了<code>std::cmp::PartialOrd</code>,这是一个 <em>trait</em>。下一部分会讲到 trait,不过简单来说,这个错误表明<code>largest</code>的函数体不能适用于<code>T</code>的所有可能的类型;因为在函数体需要比较<code>T</code>类型的值,不过它只能用于我们知道如何排序的类型。标准库中定义的<code>std::cmp::PartialOrd</code> trait 可以实现类型的排序功能。在下一部分会再次回到 trait 并讲解如何为泛型指定一个 trait,不过让我们先把这个例子放在一边并探索其他那些可以使用泛型类型参数的地方。</p>
|
||
<!-- Liz: this is the reason we had the topics in the order we did in the first
|
||
draft of this chapter; it's hard to do anything interesting with generic types
|
||
in functions unless you also know about traits and trait bounds. I think this
|
||
ordering could work out okay, though, and keep a stronger thread with the
|
||
`longest` function going through the whole chapter, but we do pause with a
|
||
not-yet-compiling example here, which I know isn't ideal either. Let us know
|
||
what you think. /Carol -->
|
||
<a class="header" href="#结构体定义中的泛型" name="结构体定义中的泛型"><h3>结构体定义中的泛型</h3></a>
|
||
<p>同样也可以使用<code><></code>语法来定义拥有一个或多个泛型参数类型字段的结构体。列表 10-6 展示了如何定义和使用一个可以存放任何类型的<code>x</code>和<code>y</code>坐标值的结构体<code>Point</code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">struct Point<T> {
|
||
x: T,
|
||
y: T,
|
||
}
|
||
|
||
fn main() {
|
||
let integer = Point { x: 5, y: 10 };
|
||
let float = Point { x: 1.0, y: 4.0 };
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-6: A <code>Point</code> struct that holds <code>x</code> and <code>y</code>
|
||
values of type <code>T</code></span></p>
|
||
<p>其语法类似于函数定义中的泛型应用。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。</p>
|
||
<p>注意<code>Point</code>的定义中只使用了一个泛型类型,我们想要表达的是结构体<code>Point</code>对于一些类型<code>T</code>是泛型的,而且字段<code>x</code>和<code>y</code><strong>都是</strong>相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的<code>Point</code>的实例,像列表 10-7 中的代码就不能编译:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">struct Point<T> {
|
||
x: T,
|
||
y: T,
|
||
}
|
||
|
||
fn main() {
|
||
let wont_work = Point { x: 5, y: 4.0 };
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-7: The fields <code>x</code> and <code>y</code> must be the same
|
||
type because both have the same generic data type <code>T</code></span></p>
|
||
<p>尝试编译会得到如下错误:</p>
|
||
<pre><code>error[E0308]: mismatched types
|
||
-->
|
||
|
|
||
7 | let wont_work = Point { x: 5, y: 4.0 };
|
||
| ^^^ expected integral variable, found
|
||
floating-point variable
|
||
|
|
||
= note: expected type `{integer}`
|
||
= note: found type `{float}`
|
||
</code></pre>
|
||
<p>当我们将 5 赋值给<code>x</code>,编译器就知道这个<code>Point</code>实例的泛型类型<code>T</code>是一个整型。接着我们将<code>y</code>指定为 4.0,而它被定义为与<code>x</code>有着相同的类型,所以出现了类型不匹配的错误。</p>
|
||
<p>如果想要一个<code>x</code>和<code>y</code>可以有不同类型且仍然是泛型的<code>Point</code>结构体,我们可以使用多个泛型类型参数。在列表 10-8 中,我们修改<code>Point</code>的定义为拥有两个泛型类型<code>T</code>和<code>U</code>。其中字段<code>x</code>是<code>T</code>类型的,而字段<code>y</code>是<code>U</code>类型的:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">struct Point<T, U> {
|
||
x: T,
|
||
y: U,
|
||
}
|
||
|
||
fn main() {
|
||
let both_integer = Point { x: 5, y: 10 };
|
||
let both_float = Point { x: 1.0, y: 4.0 };
|
||
let integer_and_float = Point { x: 5, y: 4.0 };
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-8: A <code>Point</code> generic over two types so that
|
||
<code>x</code> and <code>y</code> may be values of different types</span></p>
|
||
<p>现在所有这些<code>Point</code>实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。如果你处于一个需要很多泛型类型的位置,这可能是一个需要重新组织代码并分隔成一些更小部分的信号。</p>
|
||
<a class="header" href="#枚举定义中的泛型数据类型" name="枚举定义中的泛型数据类型"><h3>枚举定义中的泛型数据类型</h3></a>
|
||
<p>类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的<code>Option<T></code>枚举,现在这个定义看起来就更容易理解了。让我们再看看:</p>
|
||
<pre><code class="language-rust">enum Option<T> {
|
||
Some(T),
|
||
None,
|
||
}
|
||
</code></pre>
|
||
<p>换句话说<code>Option<T></code>是一个拥有泛型<code>T</code>的枚举。它有两个成员:<code>Some</code>,它存放了一个类型<code>T</code>的值,和不存在任何值的<code>None</code>。标准库中只有这一个定义来支持创建任何具体类型的枚举值。“一个可能的值”是一个比具体类型的值更抽象的概念,而 Rust 允许我们不引入重复代码就能表现抽象的概念。</p>
|
||
<p>枚举也可以拥有多个泛型类型。第九章使用过的<code>Result</code>枚举定义就是一个这样的例子:</p>
|
||
<pre><code class="language-rust">enum Result<T, E> {
|
||
Ok(T),
|
||
Err(E),
|
||
}
|
||
</code></pre>
|
||
<p><code>Result</code>枚举有两个泛型类型,<code>T</code>和<code>E</code>。<code>Result</code>有两个成员:<code>Ok</code>,它存放一个类型<code>T</code>的值,而<code>Err</code>则存放一个类型<code>E</code>的值。这个定义使得<code>Result</code>枚举能很方便的表达任何可能成功(返回<code>T</code>类型的值)也可能失败(返回<code>E</code>类型的值)的操作。回忆一下列表 9-2 中打开一个文件的场景,当文件被成功打开<code>T</code>被放入了<code>std::fs::File</code>类型而当打开文件出现问题时<code>E</code>被放入了<code>std::io::Error</code>类型。</p>
|
||
<p>当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复代码。</p>
|
||
<a class="header" href="#方法定义中的枚举数据类型" name="方法定义中的枚举数据类型"><h3>方法定义中的枚举数据类型</h3></a>
|
||
<p>可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体<code>Point<T></code>。接着我们在<code>Point<T></code>上定义了一个叫做<code>x</code>的方法来返回字段<code>x</code>中数据的引用:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">struct Point<T> {
|
||
x: T,
|
||
y: T,
|
||
}
|
||
|
||
impl<T> Point<T> {
|
||
fn x(&self) -> &T {
|
||
&self.x
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
let p = Point { x: 5, y: 10 };
|
||
|
||
println!("p.x = {}", p.x());
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-9: Implementing a method named <code>x</code> on the
|
||
<code>Point<T></code> struct that will return a reference to the <code>x</code> field, which is of
|
||
type <code>T</code>.</span></p>
|
||
<p>注意必须在<code>impl</code>后面声明<code>T</code>,这样就可以在<code>Point<T></code>上实现的方法中使用它了。</p>
|
||
<p>结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。列表 10-10 中在列表 10-8 中的结构体<code>Point<T, U></code>上定义了一个方法<code>mixup</code>。这个方法获取另一个<code>Point</code>作为参数,而它可能与调用<code>mixup</code>的<code>self</code>是不同的<code>Point</code>类型。这个方法用<code>self</code>的<code>Point</code>类型的<code>x</code>值(类型<code>T</code>)和参数的<code>Point</code>类型的<code>y</code>值(类型<code>W</code>)来创建一个新<code>Point</code>类型的实例:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">struct Point<T, U> {
|
||
x: T,
|
||
y: U,
|
||
}
|
||
|
||
impl<T, U> Point<T, U> {
|
||
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
|
||
Point {
|
||
x: self.x,
|
||
y: other.y,
|
||
}
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
let p1 = Point { x: 5, y: 10.4 };
|
||
let p2 = Point { x: "Hello", y: 'c'};
|
||
|
||
let p3 = p1.mixup(p2);
|
||
|
||
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-10: Methods that use different generic types
|
||
than their struct's definition</span></p>
|
||
<p>在<code>main</code>函数中,定义了一个有<code>i32</code>类型的<code>x</code>(其值为<code>5</code>)和<code>f64</code>的<code>y</code>(其值为<code>10.4</code>)的<code>Point</code>。<code>p2</code>则是一个有着字符串 slice 类型的<code>x</code>(其值为<code>"Hello"</code>)和<code>char</code>类型的<code>y</code>(其值为<code>c</code>)的<code>Point</code>。在<code>p1</code>上以<code>p2</code>调用<code>mixup</code>会返回一个<code>p3</code>,它会有一个<code>i32</code>类型的<code>x</code>,因为<code>x</code>来自<code>p1</code>,并拥有一个<code>char</code>类型的<code>y</code>,因为<code>y</code>来自<code>p2</code>。<code>println!</code>会打印出<code>p3.x = 5, p3.y = c</code>。</p>
|
||
<p>注意泛型参数<code>T</code>和<code>U</code>声明于<code>impl</code>之后,因为他们于结构体定义相对应。而泛型参数<code>V</code>和<code>W</code>声明于<code>fn mixup</code>之后,因为他们只是相对于方法本身的。</p>
|
||
<a class="header" href="#泛型代码的性能" name="泛型代码的性能"><h3>泛型代码的性能</h3></a>
|
||
<p>在阅读本部分的内容的同时你可能会好奇使用泛型类型参数是否会有运行时消耗。好消息是:Rust 实现泛型泛型的方式意味着你的代码使用泛型类型参数相比指定具体类型并没有任何速度上的损失。</p>
|
||
<p>Rust 通过在编译时进行泛型代码的<strong>单态化</strong>(<em>monomorphization</em>)来保证效率。单态化是一个将泛型代码转变为实际放入的具体类型的特定代码的过程。</p>
|
||
<p>编译器所做的工作正好与列表 10-5 中我们创建泛型函数的步骤相反。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。</p>
|
||
<p>让我们看看一个使用标准库中<code>Option</code>枚举的例子:</p>
|
||
<pre><code class="language-rust">let integer = Some(5);
|
||
let float = Some(5.0);
|
||
</code></pre>
|
||
<p>当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给<code>Option</code>的值并发现有两种<code>Option<T></code>:一个对应<code>i32</code>另一个对应<code>f64</code>。为此,它会将泛型定义<code>Option<T></code>展开为<code>Option_i32</code>和<code>Option_f64</code>,接着将泛型定义替换为这两个具体的定义。</p>
|
||
<p>编译器生成的单态化版本的代码看起来像这样,并包含将泛型<code>Option</code>替换为编译器创建的具体定义后的用例代码:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">enum Option_i32 {
|
||
Some(i32),
|
||
None,
|
||
}
|
||
|
||
enum Option_f64 {
|
||
Some(f64),
|
||
None,
|
||
}
|
||
|
||
fn main() {
|
||
let integer = Option_i32::Some(5);
|
||
let float = Option_f64::Some(5.0);
|
||
}
|
||
</code></pre>
|
||
<p>我们可以使用泛型来编写不重复的代码,而 Rust 将会为每一个实例编译其特定类型的代码。这意味着在使用泛型时没有运行时开销;当代码运行,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是 Rust 泛型在运行时极其高效的原因。</p>
|
||
<a class="header" href="#trait定义共享的行为" name="trait定义共享的行为"><h2>trait:定义共享的行为</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-02-traits.md">ch10-02-traits.md</a>
|
||
<br>
|
||
commit e5a987f5da3fba24e55f5c7102ec63f9dc3bc360</p>
|
||
</blockquote>
|
||
<p>trait 允许我们进行另一种抽象:他们让我们可以抽象类型所通用的行为。<em>trait</em> 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。在使用泛型类型参数的场景中,可以使用 <em>trait bounds</em> 在编译时指定泛型可以是任何实现了某个 trait 的类型,并由此在这个场景下拥有我们希望的功能。</p>
|
||
<blockquote>
|
||
<p>注意:<em>trait</em> 类似于其他语言中的常被称为<strong>接口</strong>(<em>interfaces</em>)的功能,虽然有一些不同。</p>
|
||
</blockquote>
|
||
<a class="header" href="#定义-trait" name="定义-trait"><h3>定义 trait</h3></a>
|
||
<p>一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。</p>
|
||
<p>例如,这里有多个存放了不同类型和属性文本的结构体:结构体<code>NewsArticle</code>用于存放发生于世界各地的新闻故事,而结构体<code>Tweet</code>最多只能存放 140 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。</p>
|
||
<p>我们想要创建一个多媒体聚合库用来显示可能储存在<code>NewsArticle</code>或<code>Tweet</code>实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的<code>summary</code>方法来请求总结。列表 10-11 中展示了一个表现这个概念的<code>Summarizable</code> trait 的定义:</p>
|
||
<p><span class="filename">Filename: lib.rs</span></p>
|
||
<pre><code class="language-rust">pub trait Summarizable {
|
||
fn summary(&self) -> String;
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-11: Definition of a <code>Summarizable</code> trait that
|
||
consists of the behavior provided by a <code>summary</code> method</span></p>
|
||
<p>使用<code>trait</code>关键字来定义一个 trait,后面是 trait 的名字,在这个例子中是<code>Summarizable</code>。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是是<code>fn summary(&self) -> String</code>。在方法签名后跟分号而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现<code>Summarizable</code> trait 的类型都拥有与这个签名的定义完全一致的<code>summary</code>方法。</p>
|
||
<p>trait 体中可以有多个方法,一行一个方法签名且都以分号结尾。</p>
|
||
<a class="header" href="#为类型实现-trait" name="为类型实现-trait"><h3>为类型实现 trait</h3></a>
|
||
<p>现在我们定义了<code>Summarizable</code> trait,接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。列表 10-12 中展示了<code>NewsArticle</code>结构体上<code>Summarizable</code> trait 的一个实现,它使用标题、作者和创建的位置作为<code>summary</code>的返回值。对于<code>Tweet</code>结构体,我们选择将<code>summary</code>定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 140 字符以内。</p>
|
||
<p><span class="filename">Filename: lib.rs</span></p>
|
||
<pre><code class="language-rust"># pub trait Summarizable {
|
||
# fn summary(&self) -> String;
|
||
# }
|
||
#
|
||
pub struct NewsArticle {
|
||
pub headline: String,
|
||
pub location: String,
|
||
pub author: String,
|
||
pub content: String,
|
||
}
|
||
|
||
impl Summarizable for NewsArticle {
|
||
fn summary(&self) -> String {
|
||
format!("{}, by {} ({})", self.headline, self.author, self.location)
|
||
}
|
||
}
|
||
|
||
pub struct Tweet {
|
||
pub username: String,
|
||
pub content: String,
|
||
pub reply: bool,
|
||
pub retweet: bool,
|
||
}
|
||
|
||
impl Summarizable for Tweet {
|
||
fn summary(&self) -> String {
|
||
format!("{}: {}", self.username, self.content)
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-12: Implementing the <code>Summarizable</code> trait on
|
||
the <code>NewsArticle</code> and <code>Tweet</code> types</span></p>
|
||
<p>在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于<code>impl</code>关键字之后,我们提供需要实现 trait 的名称,接着是<code>for</code>和需要实现 trait 的类型的名称。在<code>impl</code>块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。</p>
|
||
<p>一旦实现了 trait,我们就可以用与<code>NewsArticle</code>和<code>Tweet</code>实例的非 trait 方法一样的方式调用 trait 方法了:</p>
|
||
<pre><code class="language-rust,ignore">let tweet = Tweet {
|
||
username: String::from("horse_ebooks"),
|
||
content: String::from("of course, as you probably already know, people"),
|
||
reply: false,
|
||
retweet: false,
|
||
};
|
||
|
||
println!("1 new tweet: {}", tweet.summary());
|
||
</code></pre>
|
||
<p>这会打印出<code>1 new tweet: horse_ebooks: of course, as you probably already know, people</code>。</p>
|
||
<p>注意因为列表 10-12 中我们在相同的<code>lib.rs</code>里定义了<code>Summarizable</code> trait 和<code>NewsArticle</code>与<code>Tweet</code>类型,所以他们是位于同一作用域的。如果这个<code>lib.rs</code>是对应<code>aggregator</code> crate 的,而别人想要利用我们 crate 的功能外加为其<code>WeatherForecast</code>结构体实现<code>Summarizable</code> trait,在实现<code>Summarizable</code> trait 之前他们首先就需要将其导入其作用域中,如列表 10-13 所示:</p>
|
||
<p><span class="filename">Filename: lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">extern crate aggregator;
|
||
|
||
use aggregator::Summarizable;
|
||
|
||
struct WeatherForecast {
|
||
high_temp: f64,
|
||
low_temp: f64,
|
||
chance_of_precipitation: f64,
|
||
}
|
||
|
||
impl Summarizable for WeatherForecast {
|
||
fn summary(&self) -> String {
|
||
format!("The high will be {}, and the low will be {}. The chance of
|
||
precipitation is {}%.", self.high_temp, self.low_temp,
|
||
self.chance_of_precipitation)
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-13: Bringing the <code>Summarizable</code> trait from our
|
||
<code>aggregator</code> crate into scope in another crate</span></p>
|
||
<p>另外这段代码假设<code>Summarizable</code>是一个公有 trait,这是因为列表 10-11 中<code>trait</code>之前使用了<code>pub</code>关键字。</p>
|
||
<p>trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说,不允许对外部类型实现外部 trait。例如,不能<code>Vec</code>上实现<code>Display</code> trait,因为<code>Display</code>和<code>Vec</code>都定义于标准库中。允许在像<code>Tweet</code>这样作为我们<code>aggregator</code>crate 部分功能的自定义类型上实现标准库中的 trait <code>Display</code>。也允许在<code>aggregator</code>crate中为<code>Vec</code>实现<code>Summarizable</code>,因为<code>Summarizable</code>定义与此。这个限制是我们称为 <em>orphan rule</em> 的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型是实现相同的 trait,因而这两个实现会相互冲突:Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule,其他人编写的代码不会破坏你代码,反之亦是如此。</p>
|
||
<a class="header" href="#默认实现" name="默认实现"><h3>默认实现</h3></a>
|
||
<p>有时为 trait 中的某些或全部提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。</p>
|
||
<p>列表 10-14 中展示了如何为<code>Summarize</code> trait 的<code>summary</code>方法指定一个默认的字符串值,而不是像列表 10-11 中那样只是定义方法签名:</p>
|
||
<p><span class="filename">Filename: lib.rs</span></p>
|
||
<pre><code class="language-rust">pub trait Summarizable {
|
||
fn summary(&self) -> String {
|
||
String::from("(Read more...)")
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-14: Definition of a <code>Summarizable</code> trait with
|
||
a default implementation of the <code>summary</code> method</span></p>
|
||
<p>如果想要对<code>NewsArticle</code>实例使用这个默认实现,而不是像列表 10-12 中那样定义一个自己的实现,则可以指定一个空的<code>impl</code>块:</p>
|
||
<pre><code class="language-rust,ignore">impl Summarizable for NewsArticle {}
|
||
</code></pre>
|
||
<p>即便选择不再直接为<code>NewsArticle</code>定义<code>summary</code>方法了,因为<code>summary</code>方法有一个默认实现而且<code>NewsArticle</code>被指定为实现了<code>Summarizable</code> trait,我们仍然可以对<code>NewsArticle</code>的实例调用<code>summary</code>方法:</p>
|
||
<pre><code class="language-rust,ignore">let article = NewsArticle {
|
||
headline: String::from("Penguins win the Stanley Cup Championship!"),
|
||
location: String::from("Pittsburgh, PA, USA"),
|
||
author: String::from("Iceburgh"),
|
||
content: String::from("The Pittsburgh Penguins once again are the best
|
||
hockey team in the NHL."),
|
||
};
|
||
|
||
println!("New article available! {}", article.summary());
|
||
</code></pre>
|
||
<p>这段代码会打印<code>New article available! (Read more...)</code>。</p>
|
||
<p>将<code>Summarizable</code> trait 改变为拥有默认<code>summary</code>实现并不要求对列表 10-12 中的<code>Tweet</code>和列表 10-13 中的<code>WeatherForecast</code>对<code>Summarizable</code>的实现做任何改变:重载一个默认实现的语法与实现没有默认实现的 trait 方法时完全一样的。</p>
|
||
<p>默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。通过这种方法,trait 可以实现很多有用的功能而只需实现一小部分特定内容。我们可以选择让<code>Summarizable</code> trait 也拥有一个要求实现的<code>author_summary</code>方法,接着<code>summary</code>方法则提供默认实现并调用<code>author_summary</code>方法:</p>
|
||
<pre><code class="language-rust">pub trait Summarizable {
|
||
fn author_summary(&self) -> String;
|
||
|
||
fn summary(&self) -> String {
|
||
format!("(Read more from {}...)", self.author_summary())
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>为了使用这个版本的<code>Summarizable</code>,只需在实现 trait 时定义<code>author_summary</code>即可:</p>
|
||
<pre><code class="language-rust,ignore">impl Summarizable for Tweet {
|
||
fn author_summary(&self) -> String {
|
||
format!("@{}", self.username)
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>一旦定义了<code>author_summary</code>,我们就可以对<code>Tweet</code>结构体的实例调用<code>summary</code>了,而<code>summary</code>的默认实现会调用我们提供的<code>author_summary</code>定义。</p>
|
||
<pre><code class="language-rust,ignore">let tweet = Tweet {
|
||
username: String::from("horse_ebooks"),
|
||
content: String::from("of course, as you probably already know, people"),
|
||
reply: false,
|
||
retweet: false,
|
||
};
|
||
|
||
println!("1 new tweet: {}", tweet.summary());
|
||
</code></pre>
|
||
<p>这会打印出<code>1 new tweet: (Read more from @horse_ebooks...)</code>。</p>
|
||
<p>注意在重载过的实现中调用默认实现是不可能的。</p>
|
||
<a class="header" href="#trait-bounds" name="trait-bounds"><h3>trait bounds</h3></a>
|
||
<p>现在我们定义了 trait 并在类型上实现了这些 trait,也可以对泛型类型参数使用 trait。我们可以限制泛型不再适用于任何类型,编译器会确保其被限制为那些实现了特定 trait 的类型,由此泛型就会拥有我们希望其类型所拥有的功能。这被称为指定泛型的 <em>trait bounds</em>。</p>
|
||
<p>例如在列表 10-12 中为<code>NewsArticle</code>和<code>Tweet</code>类型实现了<code>Summarizable</code> trait。我们可以定义一个函数<code>notify</code>来调用<code>summary</code>方法,它拥有一个泛型类型<code>T</code>的参数<code>item</code>。为了能够在<code>item</code>上调用<code>summary</code>而不出现错误,我们可以在<code>T</code>上使用 trait bounds 来指定<code>item</code>必须是实现了<code>Summarizable</code> trait 的类型:</p>
|
||
<pre><code class="language-rust,ignore">pub fn notify<T: Summarizable>(item: T) {
|
||
println!("Breaking news! {}", item.summary());
|
||
}
|
||
</code></pre>
|
||
<p>trait bounds 连同泛型类型参数声明一同出现,位于尖括号中的冒号后面。由于<code>T</code>上的 trait bounds,我们可以传递任何<code>NewsArticle</code>或<code>Tweet</code>的实例来调用<code>notify</code>函数。列表 10-13 中使用我们<code>aggregator</code> crate 的外部代码也可以传递一个<code>WeatherForecast</code>的实例来调用<code>notify</code>函数,因为<code>WeatherForecast</code>同样也实现了<code>Summarizable</code>。使用任何其他类型,比如<code>String</code>或<code>i32</code>,来调用<code>notify</code>的代码将不能编译,因为这些类型没有实现<code>Summarizable</code>。</p>
|
||
<p>可以通过<code>+</code>来为泛型指定多个 trait bounds。如果我们需要能够在函数中使用<code>T</code>类型的显示格式的同时也能使用<code>summary</code>方法,则可以使用 trait bounds <code>T: Summarizable + Display</code>。这意味着<code>T</code>可以是任何实现了<code>Summarizable</code>和<code>Display</code>的类型。</p>
|
||
<p>对于拥有多个泛型类型参数的函数,每一个泛型都可以有其自己的 trait bounds。在函数名和参数列表之间的尖括号中指定很多的 trait bound 信息将是难以阅读的,所以有另外一个指定 trait bounds 的语法,它将其移动到函数签名后的<code>where</code>从句中。所以相比这样写:</p>
|
||
<pre><code class="language-rust,ignore">fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
|
||
</code></pre>
|
||
<p>我们也可以使用<code>where</code>从句:</p>
|
||
<pre><code class="language-rust,ignore">fn some_function<T, U>(t: T, u: U) -> i32
|
||
where T: Display + Clone,
|
||
U: Clone + Debug
|
||
{
|
||
</code></pre>
|
||
<p>这就显得不那么杂乱,同时也使这个函数看起来更像没有很多 trait bounds 的函数。这时函数名、参数列表和返回值类型都离得很近。</p>
|
||
<a class="header" href="#使用-trait-bounds-来修复largest函数" name="使用-trait-bounds-来修复largest函数"><h3>使用 trait bounds 来修复<code>largest</code>函数</h3></a>
|
||
<p>所以任何想要对泛型使用 trait 定义的行为的时候,都需要在泛型参数类型上指定 trait bounds。现在我们就可以修复列表 10-5 中那个使用泛型类型参数的<code>largest</code>函数定义了!当我们将其放置不管的时候,它会出现这个错误:</p>
|
||
<pre><code>error[E0369]: binary operation `>` cannot be applied to type `T`
|
||
|
|
||
5 | if item > largest {
|
||
| ^^^^
|
||
|
|
||
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
|
||
</code></pre>
|
||
<p>在<code>largest</code>函数体中我们想要使用大于运算符比较两个<code>T</code>类型的值。这个运算符被定义为标准库中 trait <code>std::cmp::PartialOrd</code> 的一个默认方法。所以为了能够使用大于运算符,需要在<code>T</code>的 trait bounds 中指定<code>PartialOrd</code>,这样<code>largest</code>函数可以用于任何可以比较大小的类型的 slice。因为<code>PartialOrd</code>位于 prelude 中所以并不需要手动将其引入作用域。</p>
|
||
<pre><code class="language-rust,ignore">fn largest<T: PartialOrd>(list: &[T]) -> T {
|
||
</code></pre>
|
||
<p>但是如果编译代码的话,会出现不同的错误:</p>
|
||
<pre><code class="language-text">error[E0508]: cannot move out of type `[T]`, a non-copy array
|
||
--> src/main.rs:4:23
|
||
|
|
||
4 | let mut largest = list[0];
|
||
| ----------- ^^^^^^^ cannot move out of here
|
||
| |
|
||
| hint: to prevent move, use `ref largest` or `ref mut largest`
|
||
|
||
error[E0507]: cannot move out of borrowed content
|
||
--> src/main.rs:6:9
|
||
|
|
||
6 | for &item in list.iter() {
|
||
| ^----
|
||
| ||
|
||
| |hint: to prevent move, use `ref item` or `ref mut item`
|
||
| cannot move out of borrowed content
|
||
</code></pre>
|
||
<p>错误的核心是<code>cannot move out of type [T], a non-copy array</code>,对于非泛型版本的<code>largest</code>函数,我们只尝试了寻找最大的<code>i32</code>和<code>char</code>。正如第四章讨论过的,像<code>i32</code>和<code>char</code>这样的类型是已知大小的并可以储存在栈上,所以他们实现了<code>Copy</code> trait。当我们将<code>largest</code>函数改成使用泛型后,现在<code>list</code>参数的类型就有可能是没有实现<code>Copy</code> trait 的,这意味着我们可能不能将<code>list[0]</code>的值移动到<code>largest</code>变量中。</p>
|
||
<p>如果只想对实现了<code>Copy</code>的类型调用这些代码,可以在<code>T</code>的 trait bounds 中增加<code>Copy</code>!列表 10-15 中展示了一个可以编译的泛型版本的<code>largest</code>函数的完整代码,只要传递给<code>largest</code>的 slice 值的类型实现了<code>PartialOrd</code>和<code>Copy</code>这两个 trait,例如<code>i32</code>和<code>char</code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::cmp::PartialOrd;
|
||
|
||
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
|
||
let mut largest = list[0];
|
||
|
||
for &item in list.iter() {
|
||
if item > largest {
|
||
largest = item;
|
||
}
|
||
}
|
||
|
||
largest
|
||
}
|
||
|
||
fn main() {
|
||
let numbers = vec![34, 50, 25, 100, 65];
|
||
|
||
let result = largest(&numbers);
|
||
println!("The largest number is {}", result);
|
||
|
||
let chars = vec!['y', 'm', 'a', 'q'];
|
||
|
||
let result = largest(&chars);
|
||
println!("The largest char is {}", result);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-15: A working definition of the <code>largest</code>
|
||
function that works on any generic type that implements the <code>PartialOrd</code> and
|
||
<code>Copy</code> traits</span></p>
|
||
<p>如果并不希望限制<code>largest</code>函数只能用于实现了<code>Copy</code> trait 的类型,我们可以在<code>T</code>的 trait bounds 中指定<code>Clone</code>而不是<code>Copy</code>,并克隆 slice 的每一个值使得<code>largest</code>函数拥有其所有权。但是使用<code>clone</code>函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种<code>largest</code>的实现方式是返回 slice 中一个<code>T</code>值的引用。如果我们将函数返回值从<code>T</code>改为<code>&T</code>并改变函数体使其能够返回一个引用,我们将不需要任何<code>Clone</code>或<code>Copy</code>的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!</p>
|
||
<p>trait 和 trait bounds 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bounds 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。</p>
|
||
<p>这里还有一种泛型,我们一直在使用它甚至都没有察觉它的存在,这就是<strong>生命周期</strong>(<em>lifetimes</em>)。不同于其他泛型帮助我们确保类型拥有期望的行为,生命周期则有助于确保引用在我们需要他们的时候一直有效。让我们学习生命周期是如何做到这些的。</p>
|
||
<a class="header" href="#生命周期与引用有效性" name="生命周期与引用有效性"><h2>生命周期与引用有效性</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-03-lifetime-syntax.md">ch10-03-lifetime-syntax.md</a>
|
||
<br>
|
||
commit 9fbbfb23c2cd1686dbd3ce7950ae1eda300937f6</p>
|
||
</blockquote>
|
||
<p>当在第四章讨论引用时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其<strong>生命周期</strong>,也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以多种不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。</p>
|
||
<p>好吧,这有点不太寻常,而且也不同于其他语言中使用的工具。生命周期,从某种意义上说,是 Rust 最与众不同的功能。</p>
|
||
<p>生命周期是一个很广泛的话题,本章不可能涉及到它全部的内容,所以这里我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。第十九章会包含生命周期所有功能的更高级的内容。</p>
|
||
<a class="header" href="#生命周期避免了悬垂引用" name="生命周期避免了悬垂引用"><h3>生命周期避免了悬垂引用</h3></a>
|
||
<p>生命周期的主要目标是避免悬垂引用,它会导致程序引用了并非其期望引用的数据。考虑一下列表 10-16 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量<code>r</code>,而内部作用域声明了一个初值为 5 的变量<code>x</code>。在内部作用域中,我们尝试将<code>r</code>的值设置为一个<code>x</code>的引用。接着在内部作用域结束后,尝试打印出<code>r</code>的值:</p>
|
||
<pre><code class="language-rust,ignore">{
|
||
let r;
|
||
|
||
{
|
||
let x = 5;
|
||
r = &x;
|
||
}
|
||
|
||
println!("r: {}", r);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-16: An attempt to use a reference whose value
|
||
has gone out of scope</span></p>
|
||
<blockquote>
|
||
<a class="header" href="#未初始化变量不能被使用" name="未初始化变量不能被使用"><h3>未初始化变量不能被使用</h3></a>
|
||
<p>接下来的一些例子中声明了没有初始值的变量,以便这些变量存在于外部作用域。这看起来好像和 Rust 不允许存在空值相冲突。然而这是可以的,如果我们尝试在给它一个值之前使用这个变量,会出现一个编译时错误。请自行尝试!</p>
|
||
</blockquote>
|
||
<p>当编译这段代码时会得到一个错误:</p>
|
||
<pre><code>error: `x` does not live long enough
|
||
|
|
||
6 | r = &x;
|
||
| - borrow occurs here
|
||
7 | }
|
||
| ^ `x` dropped here while still borrowed
|
||
...
|
||
10 | }
|
||
| - borrowed value needs to live until here
|
||
</code></pre>
|
||
<p>变量<code>x</code>并没有“存在的足够久”。为什么呢?好吧,<code>x</code>在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过<code>r</code>在外部作用域也是有效的;作用域越大我们就说它“存在的越久”。如果 Rust 允许这段代码工作,<code>r</code>将会引用在<code>x</code>离开作用域时被释放的内存,这时尝试对<code>r</code>做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?</p>
|
||
<a class="header" href="#借用检查器" name="借用检查器"><h4>借用检查器</h4></a>
|
||
<p>编译器的这一部分叫做<strong>借用检查器</strong>(<em>borrow checker</em>),它比较作用域来确保所有的借用都是有效的。列表 10-17 展示了与列表 10-16 相同的例子不过带有变量声明周期的注释:</p>
|
||
<pre><code class="language-rust,ignore">{
|
||
let r; // -------+-- 'a
|
||
// |
|
||
{ // |
|
||
let x = 5; // -+-----+-- 'b
|
||
r = &x; // | |
|
||
} // -+ |
|
||
// |
|
||
println!("r: {}", r); // |
|
||
// |
|
||
// -------+
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-17: Annotations of the lifetimes of <code>r</code> and
|
||
<code>x</code>, named <code>'a</code> and <code>'b</code> respectively</span></p>
|
||
<!-- Just checking I'm reading this right: the inside block is the b lifetime,
|
||
correct? I want to leave a note for production, make sure we can make that
|
||
clear -->
|
||
<!-- Yes, the inside block for the `'b` lifetime starts with the `let x = 5;`
|
||
line and ends with the first closing curly brace on the 7th line. Do you think
|
||
the text art comments work or should we make an SVG diagram that has nicer
|
||
looking arrows and labels? /Carol -->
|
||
<p>我们将<code>r</code>的声明周期标记为<code>'a</code>而将<code>x</code>的生命周期标记为<code>'b</code>。如你所见,内部的<code>'b</code>块要比外部的生命周期<code>'a</code>小得多。在编译时,Rust 比较这两个生命周期的大小,并发现<code>r</code>拥有声明周期<code>'a</code>,不过它引用了一个拥有生命周期<code>'b</code>的对象。程序被拒绝编译,因为生命周期<code>'b</code>比生命周期<code>'a</code>要小:被引用的对象比它的引用者存活的时间更短。</p>
|
||
<p>让我们看看列表 10-18 中这个并没有产生悬垂引用且可以正常编译的例子:</p>
|
||
<pre><code class="language-rust">{
|
||
let x = 5; // -----+-- 'b
|
||
// |
|
||
let r = &x; // --+--+-- 'a
|
||
// | |
|
||
println!("r: {}", r); // | |
|
||
// --+ |
|
||
} // -----+
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-18: A valid reference because the data has a
|
||
longer lifetime than the reference</span></p>
|
||
<p><code>x</code>拥有生命周期 <code>'b</code>,在这里它比 <code>'a</code>要大。这就意味着<code>r</code>可以引用<code>x</code>:Rust 知道<code>r</code>中的引用在<code>x</code>有效的时候也会一直有效。</p>
|
||
<p>现在我们已经在一个具体的例子中展示了引用的声明周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的,接下来让我们聊聊在函数的上下文中参数和返回值的泛型生命周期。</p>
|
||
<a class="header" href="#函数中的泛型生命周期" name="函数中的泛型生命周期"><h3>函数中的泛型生命周期</h3></a>
|
||
<p>让我们来编写一个返回两个字符串 slice 中最长的那一个的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了<code>longest</code>函数,列表 10-19 中的代码应该会打印出<code>The longest string is abcd</code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let string1 = String::from("abcd");
|
||
let string2 = "xyz";
|
||
|
||
let result = longest(string1.as_str(), string2);
|
||
println!("The longest string is {}", result);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-19: A <code>main</code> function that calls the <code>longest</code>
|
||
function to find the longest of two string slices</span></p>
|
||
<p>注意函数期望获取字符串 slice(如第四章所讲到的这是引用)因为我们并不希望<code>longest</code>函数获取其参数的引用。我们希望函数能够接受<code>String</code>的 slice(也就是变量<code>string1</code>的类型)和字符串字面值(也就是变量<code>string2</code>包含的值)。</p>
|
||
<!-- why is `a` a slice and `b` a literal? You mean "a" from the string "abcd"? -->
|
||
<!-- I've changed the variable names to remove ambiguity between the variable
|
||
name `a` and the "a" from the string "abcd". `string1` is not a slice, it's a
|
||
`String`, but we're going to pass a slice that refers to that `String` to the
|
||
`longest` function (`string1.as_str()` creates a slice that references the
|
||
`String` stored in `string1`). We chose to have `string2` be a literal since
|
||
the reader might have code with both `String`s and string literals, and the way
|
||
most readers first get into problems with lifetimes is involving string slices,
|
||
so we wanted to demonstrate the flexibility of taking string slices as
|
||
arguments but the issues you might run into because string slices are
|
||
references.
|
||
All of the `String`/string slice/string literal concepts here are covered
|
||
thoroughly in Chapter 4, which is why we put two back references here (above
|
||
and below). If these topics are confusing you in this context, I'd be
|
||
interested to know if rereading Chapter 4 clears up that confusion.
|
||
/Carol -->
|
||
<p>参考之前第四章中的“字符串 slice 作为参数”部分中更多关于为什么上面例子中的参数正是我们想要的讨论。</p>
|
||
<p>如果尝试像列表 10-20 中那样实现<code>longest</code>函数,它并不能编译:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn longest(x: &str, y: &str) -> &str {
|
||
if x.len() > y.len() {
|
||
x
|
||
} else {
|
||
y
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-20: An implementation of the <code>longest</code>
|
||
function that returns the longest of two string slices, but does not yet
|
||
compile</span></p>
|
||
<p>将会出现如下有关生命周期的错误:</p>
|
||
<pre><code>error[E0106]: missing lifetime specifier
|
||
|
|
||
1 | fn longest(x: &str, y: &str) -> &str {
|
||
| ^ expected lifetime parameter
|
||
|
|
||
= help: this function's return type contains a borrowed value, but the
|
||
signature does not say whether it is borrowed from `x` or `y`
|
||
</code></pre>
|
||
<p>提示文本告诉我们返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向<code>x</code>或<code>y</code>。事实上我们也不知道,因为函数体中<code>if</code>块返回一个<code>x</code>的引用而<code>else</code>块返回一个<code>y</code>的引用。</p>
|
||
<p>虽然我们定义了这个函数,但是并不知道传递给函数的具体值,所以也不知道到底是<code>if</code>还是<code>else</code>会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像列表 10-17 和 10-18 那样通过观察作用域来确定返回的引用总是有效的。借用检查器自身同样也无法确定,因为它不知道<code>x</code>和<code>y</code>的生命周期是如何与返回值的生命周期相关联的。接下来我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行相关分析。</p>
|
||
<a class="header" href="#生命周期注解语法" name="生命周期注解语法"><h3>生命周期注解语法</h3></a>
|
||
<p>生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解所做的就是将多个引用的生命周期联系起来。</p>
|
||
<p>生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(<code>'</code>)开头。生命周期参数的名称通常全是小写,而且类似于泛型类型,其名称通常非常短。<code>'a</code>是大多数人默认使用的名称。生命周期参数注解位于引用的<code>&</code>之后,并有一个空格来将引用类型与生命周期注解分隔开。</p>
|
||
<p>这里有一些例子:我们有一个没有生命周期参数的<code>i32</code>的引用,一个有叫做<code>'a</code>的生命周期参数的<code>i32</code>的引用,和一个也有的生命周期参数<code>'a</code>的<code>i32</code>的可变引用:</p>
|
||
<pre><code class="language-rust,ignore">&i32 // a reference
|
||
&'a i32 // a reference with an explicit lifetime
|
||
&'a mut i32 // a mutable reference with an explicit lifetime
|
||
</code></pre>
|
||
<p>生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系。如果函数有一个生命周期<code>'a</code>的<code>i32</code>的引用的参数<code>first</code>,还有另一个同样是生命周期<code>'a</code>的<code>i32</code>的引用的参数<code>second</code>,这两个生命周期注解有相同的名称意味着<code>first</code>和<code>second</code>必须与这相同的泛型生命周期存在得一样久。</p>
|
||
<a class="header" href="#函数签名中的生命周期注解" name="函数签名中的生命周期注解"><h3>函数签名中的生命周期注解</h3></a>
|
||
<p>来看看我们编写的<code>longest</code>函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像列表 10-21 中在每个引用中都加上了<code>'a</code>那样:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||
if x.len() > y.len() {
|
||
x
|
||
} else {
|
||
y
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-21: The <code>longest</code> function definition that
|
||
specifies all the references in the signature must have the same lifetime,
|
||
<code>'a</code></span></p>
|
||
<p>这段代码能够编译并会产生我们想要使用列表 10-19 中的<code>main</code>函数得到的结果。</p>
|
||
<p>现在函数签名表明对于某些生命周期<code>'a</code>,函数会获取两个参数,他们都是与生命周期<code>'a</code>存在的一样长的字符串 slice。函数会返回一个同样也与生命周期<code>'a</code>存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的协议。</p>
|
||
<p>通过在函数签名中指定生命周期参数,我们不会改变任何参数或返回值的生命周期,不过我们说过任何不坚持这个协议的类型都将被借用检查器拒绝。这个函数并不知道(或需要知道)<code>x</code>和<code>y</code>具体会存在多久,不过只需要知道一些可以使用<code>'a</code>替代的作用域将会满足这个签名。</p>
|
||
<p>当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生惊人的消耗并且对于 Rust 来说经常都是不可能分析的。在这种情况下,我们需要自己标注生命周期。</p>
|
||
<p>当具体的引用被传递给<code>longest</code>时,具体被<code>'a</code>所替代的生命周期是<code>x</code>的作用域与<code>y</code>的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期<code>'a</code>的具体生命周期等同于<code>x</code>和<code>y</code>的生命周期中较小的那一个。因为我们用相同的生命周期参数标注了返回的引用值,所以返回的引用值就能保证在<code>x</code>和<code>y</code>中较短的那个生命周期结束之前保持有效。</p>
|
||
<p>让我们如何通过传递拥有不同具体生命周期的引用来观察他们是如何限制<code>longest</code>函数的使用的。列表 10-22 是一个应该在任何编程语言中都很直观的例子:<code>string1</code>直到外部作用域结束都是有效的,<code>string2</code>则在内部作用域中是有效的,而<code>result</code>则引用了一些直到内部作用域结束都是有效的值。借用检查器赞同这些代码;它能够编译和运行,并打印出<code>The longest string is long string is long</code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust"># fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||
# if x.len() > y.len() {
|
||
# x
|
||
# } else {
|
||
# y
|
||
# }
|
||
# }
|
||
#
|
||
fn main() {
|
||
let string1 = String::from("long string is long");
|
||
|
||
{
|
||
let string2 = String::from("xyz");
|
||
let result = longest(string1.as_str(), string2.as_str());
|
||
println!("The longest string is {}", result);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-22: Using the <code>longest</code> function with
|
||
references to <code>String</code> values that have different concrete lifetimes</span></p>
|
||
<p>接下来,让我们尝试一个<code>result</code>的引用的生命周期必须比两个参数的要短的例子。将<code>result</code>变量的声明从内部作用域中移动出来,不过将<code>result</code>和<code>string2</code>变量的赋值语句一同放在内部作用域里。接下来,我们将使用<code>result</code>的<code>println!</code>移动到内部作用域之外,就在其结束之后。注意列表 10-23 中的代码不能编译:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let string1 = String::from("long string is long");
|
||
let result;
|
||
{
|
||
let string2 = String::from("xyz");
|
||
result = longest(string1.as_str(), string2.as_str());
|
||
}
|
||
println!("The longest string is {}", result);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-23: Attempting to use <code>result</code> after <code>string2</code>
|
||
has gone out of scope won't compile</span></p>
|
||
<p>如果尝试编译会出现如下错误:</p>
|
||
<pre><code>error: `string2` does not live long enough
|
||
|
|
||
6 | result = longest(string1.as_str(), string2.as_str());
|
||
| ------- borrow occurs here
|
||
7 | }
|
||
| ^ `string2` dropped here while still borrowed
|
||
8 | println!("The longest string is {}", result);
|
||
9 | }
|
||
| - borrowed value needs to live until here
|
||
</code></pre>
|
||
<p>错误表明为了保证<code>println!</code>中的<code>result</code>是有效的,<code>string2</code>需要直到外部作用域结束都是有效的。Rust 知道这些是因为(<code>longest</code>)函数的参数和返回值都使用了相同的生命周期参数<code>'a</code>。</p>
|
||
<p>以我们的理解<code>string1</code>更长,因此<code>result</code>会包含指向<code>string1</code>的引用。因为<code>string1</code>还未离开作用域,对于<code>println!</code>来说<code>string1</code>的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是<code>longest</code>函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许列表 10-23 中的代码,因为它可能会存在无效的引用。</p>
|
||
<p>请尝试更多采用不同的值和不同生命周期的引用作为<code>longest</code>函数的参数和返回值的实验。并在开始编译前猜想你的实验能否通过借用检查器,接着编译一下看看你是否是正确的!</p>
|
||
<a class="header" href="#深入理解生命周期" name="深入理解生命周期"><h3>深入理解生命周期</h3></a>
|
||
<p>指定生命周期参数的正确方式依赖函数具体的功能。例如,如果将<code>longest</code>函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数<code>y</code>指定一个生命周期。如下代码将能够编译:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn longest<'a>(x: &'a str, y: &str) -> &'a str {
|
||
x
|
||
}
|
||
</code></pre>
|
||
<p>在这个例子中,我们为参数<code>x</code>和返回值指定了生命周期参数<code>'a</code>,不过没有为参数<code>y</code>指定,因为<code>y</code>的生命周期与参数<code>x</code>和返回值的生命周期没有任何关系。</p>
|
||
<p>当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用<strong>没有</strong>指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。尝试考虑这个并不能编译的<code>longest</code>函数实现:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn longest<'a>(x: &str, y: &str) -> &'a str {
|
||
let result = String::from("really long string");
|
||
result.as_str()
|
||
}
|
||
</code></pre>
|
||
<p>即便我们为返回值指定了生命周期参数<code>'a</code>,这个实现却编译失败了,因为返回值的生命周期与参数完全没有关联。这里是会出现的错误信息:</p>
|
||
<pre><code>error: `result` does not live long enough
|
||
|
|
||
3 | result.as_str()
|
||
| ^^^^^^ does not live long enough
|
||
4 | }
|
||
| - borrowed value only lives until here
|
||
|
|
||
note: borrowed value must be valid for the lifetime 'a as defined on the block
|
||
at 1:44...
|
||
|
|
||
1 | fn longest<'a>(x: &str, y: &str) -> &'a str {
|
||
| ^
|
||
</code></pre>
|
||
<p>出现的问题是<code>result</code>在函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个<code>result</code>的引用。无法指定生命周期参数来改变悬垂引用,而且 Rust 也不允许我们创建一个悬垂引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。</p>
|
||
<p>从结果上看,生命周期语法是关于如何联系函数不同参数和返回值的生命周期的。一旦他们形成了某种联系,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。</p>
|
||
<a class="header" href="#结构体定义中的生命周期注解" name="结构体定义中的生命周期注解"><h3>结构体定义中的生命周期注解</h3></a>
|
||
<p>目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。列表 10-24 中有一个存放了一个字符串 slice 的结构体<code>ImportantExcerpt</code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">struct ImportantExcerpt<'a> {
|
||
part: &'a str,
|
||
}
|
||
|
||
fn main() {
|
||
let novel = String::from("Call me Ishmael. Some years ago...");
|
||
let first_sentence = novel.split('.')
|
||
.next()
|
||
.expect("Could not find a '.'");
|
||
let i = ImportantExcerpt { part: first_sentence };
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-24: A struct that holds a reference, so its
|
||
definition needs a lifetime annotation</span></p>
|
||
<p>这个结构体有一个字段,<code>part</code>,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。</p>
|
||
<p>这里的<code>main</code>函数创建了一个<code>ImportantExcerpt</code>的实例,它存放了变量<code>novel</code>所拥有的<code>String</code>的第一个句子的引用。</p>
|
||
<a class="header" href="#生命周期省略" name="生命周期省略"><h3>生命周期省略</h3></a>
|
||
<p>在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的“字符串 slice”部分有一个函数,我们在列表 10-25 中再次展示它,没有生命周期注解却能成功编译:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">fn first_word(s: &str) -> &str {
|
||
let bytes = s.as_bytes();
|
||
|
||
for (i, &item) in bytes.iter().enumerate() {
|
||
if item == b' ' {
|
||
return &s[0..i];
|
||
}
|
||
}
|
||
|
||
&s[..]
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 10-25: A function we defined in Chapter 4 that
|
||
compiled without lifetime annotations, even though the parameter and return
|
||
type are references</span></p>
|
||
<p>这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 1.0 之前的版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:</p>
|
||
<pre><code class="language-rust,ignore">fn first_word<'a>(s: &'a str) -> &'a str {
|
||
</code></pre>
|
||
<p>在编写了很多 Rust 代码后,Rust 团队发现在特定情况下 Rust 程序员们总是重复地编写一模一样的生命周期注解。这些场景是可预测的并且遵循几个明确的模式。接着 Rust 团队就把这些模式编码进了 Rust 编译器中,如此借用检查器在这些情况下就能推断出生命周期而不再强制程序员显式的增加注解。</p>
|
||
<p>这里我们提到一些 Rust 的历史是因为更多的明确的模式将被合并和添加到编译器中是完全可能的。未来将会需要越来越少的生命周期注解。</p>
|
||
<p>被编码进 Rust 引用分析的模式被称为<strong>生命周期省略规则</strong>(<em>lifetime elision rules</em>)。这并不是需要程序员遵守的规则;这些规则是一系列特定的场景,此时编译器会考虑,如果代码符合这些场景,就不需要明确指定生命周期。</p>
|
||
<p>这些规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出一个错误,这可以通过增加对应引用之间相联系的生命周期注解来解决。</p>
|
||
<p>首先,介绍一些定义:函数或方法的参数的生命周期被称为<strong>输入生命周期</strong>(<em>input lifetimes</em>),而返回值的生命周期被称为<strong>输出生命周期</strong>(<em>output lifetimes</em>)。</p>
|
||
<p>现在介绍编译器用于判断引用何时不需要明确生命周期注解的规则。第一条规则适用于输入生命周期,而后两条规则则适用于输出生命周期。如果编译器检查完这三条规则并仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。</p>
|
||
<ol>
|
||
<li>
|
||
<p>每一个是引用的参数都有它自己的生命周期参数。话句话说就是,有一个引用参数的函数有一个生命周期参数:<code>fn foo<'a>(x: &'a i32)</code>,有两个引用参数的函数有两个不同的生命周期参数,<code>fn foo<'a, 'b>(x: &'a i32, y: &'b i32)</code>,依此类推。</p>
|
||
</li>
|
||
<li>
|
||
<p>如果只有一个输入生命周期参数,那么它被赋给所有输出生命周期参数:<code>fn foo<'a>(x: &'a i32) -> &'a i32</code>。</p>
|
||
</li>
|
||
<li>
|
||
<p>如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故是<code>&self</code>或<code>&mut self</code>,那么<code>self</code>的生命周期被赋给所有输出生命周期参数。这使得方法写起来更简洁。</p>
|
||
</li>
|
||
</ol>
|
||
<p>假设我们自己就是编译器并来计算列表 10-25 <code>first_word</code>函数的签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:</p>
|
||
<pre><code class="language-rust,ignore">fn first_word(s: &str) -> &str {
|
||
</code></pre>
|
||
<p>接着我们(作为编译器)应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为<code>'a</code>,所以现在签名看起来像这样:</p>
|
||
<pre><code class="language-rust,ignore">fn first_word<'a>(s: &'a str) -> &str {
|
||
</code></pre>
|
||
<p>对于第二条规则,因为这里正好只有一个输入生命周期参数所以是适用的。第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,所以现在签名看起来像这样:</p>
|
||
<pre><code class="language-rust,ignore">fn first_word<'a>(s: &'a str) -> &'a str {
|
||
</code></pre>
|
||
<p>现在这个函数签名中的所有引用都有了生命周期,而编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。</p>
|
||
<p>让我们再看看另一个例子,这次我们从列表 10-20 中没有生命周期参数的<code>longest</code>函数开始:</p>
|
||
<pre><code class="language-rust,ignore">fn longest(x: &str, y: &str) -> &str {
|
||
</code></pre>
|
||
<p>再次假设我们自己就是编译器并应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所有就有两个生命周期:</p>
|
||
<pre><code class="language-rust,ignore">fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
|
||
</code></pre>
|
||
<p>再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有<code>self</code>参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译列表 10-20 的代码时会出现错误的原因:编译器适用所有已知的生命周期省略规则,不过仍然不能计算出签名中所有引用的生命周期。</p>
|
||
<p>因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。</p>
|
||
<a class="header" href="#方法定义中的生命周期注解" name="方法定义中的生命周期注解"><h3>方法定义中的生命周期注解</h3></a>
|
||
<!-- Is this different to the reference lifetime annotations, or just a
|
||
finalized explanation? -->
|
||
<!-- This is about lifetimes on references in method signatures, which is where
|
||
the 3rd lifetime elision rule kicks in. It can also be confusing where lifetime
|
||
parameters need to be declared and used since the lifetime parameters could go
|
||
with the struct's fields or with references passed into or returned from
|
||
methods. /Carol -->
|
||
<p>当为带有生命周期的结构体实现方法时,其语法依然类似列表 10-10 中展示的泛型类型参数的语法:包括声明生命周期参数的位置和生命周期参数是否与结构体字段或方法的参数与返回值相关联。</p>
|
||
<p>(实现方法时)结构体字段的生命周期必须总是在<code>impl</code>关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。</p>
|
||
<p><code>impl</code>块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用列表 10-24 中定义的结构体<code>ImportantExcerpt</code>的例子。</p>
|
||
<p>首先,这里有一个方法<code>level</code>。其唯一的参数是<code>self</code>的引用,而且返回值只是一个<code>i32</code>,并不引用任何值:</p>
|
||
<pre><code class="language-rust"># struct ImportantExcerpt<'a> {
|
||
# part: &'a str,
|
||
# }
|
||
#
|
||
impl<'a> ImportantExcerpt<'a> {
|
||
fn level(&self) -> i32 {
|
||
3
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><code>impl</code>之后和类型名称之后的生命周期参数是必要的,不过因为第一条生命周期规则我们并不必须标注<code>self</code>引用的生命周期。</p>
|
||
<p>这里是一个适用于第三条生命周期省略规则的例子:</p>
|
||
<pre><code class="language-rust"># struct ImportantExcerpt<'a> {
|
||
# part: &'a str,
|
||
# }
|
||
#
|
||
impl<'a> ImportantExcerpt<'a> {
|
||
fn announce_and_return_part(&self, announcement: &str) -> &str {
|
||
println!("Attention please: {}", announcement);
|
||
self.part
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>这里有两个输入生命周期,所以 Rust 应用第一条生命周期省略规则并给予<code>&self</code>和<code>announcement</code>他们各自的生命周期。接着,因为其中一个参数是<code>&self</code>,返回值类型被赋予了<code>&self</code>的生命周期,这样所有的生命周期都被计算出来了。</p>
|
||
<a class="header" href="#静态生命周期" name="静态生命周期"><h3>静态生命周期</h3></a>
|
||
<p>这里有<strong>一种</strong>特殊的生命周期值得讨论:<code>'static</code>。<code>'static</code>生命周期存活于整个程序期间。所有的字符串字面值都拥有<code>'static</code>生命周期,我们也可以选择像下面这样标注出来:</p>
|
||
<pre><code class="language-rust">let s: &'static str = "I have a static lifetime.";
|
||
</code></pre>
|
||
<p>这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是<code>'static</code>的。</p>
|
||
<!-- How would you add a static lifetime (below)? -->
|
||
<!-- Just like you'd specify any lifetime, see above where it shows `&'static str`. /Carol -->
|
||
<p>你可能在错误信息的帮助文本中见过使用<code>'static</code>生命周期的建议,不过将引用指定为<code>'static</code>之前,思考一下这个引用是否真的在整个程序的生命周期里都有效(或者哪怕你希望它一直有效,如果可能的话)。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个<code>'static</code>的生命周期。</p>
|
||
<a class="header" href="#结合泛型类型参数trait-bounds-和生命周期" name="结合泛型类型参数trait-bounds-和生命周期"><h3>结合泛型类型参数、trait bounds 和生命周期</h3></a>
|
||
<p>让我们简单的看一下在同一函数中指定泛型类型参数、trait bounds 和生命周期的语法!</p>
|
||
<pre><code class="language-rust">use std::fmt::Display;
|
||
|
||
fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
|
||
where T: Display
|
||
{
|
||
println!("Announcement! {}", ann);
|
||
if x.len() > y.len() {
|
||
x
|
||
} else {
|
||
y
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>这个是列表 10-21 中那个返回两个字符串 slice 中最长者的<code>longest</code>函数,不过带有一个额外的参数<code>ann</code>。<code>ann</code>的类型是泛型<code>T</code>,它可以被放入任何实现了<code>where</code>从句中指定的<code>Display</code> trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么<code>Display</code> trait bound 是必须的。因为生命周期也是泛型,生命周期参数<code>'a</code>和泛型类型参数<code>T</code>都位于函数名后的同一尖括号列表中。</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>这一章介绍了很多的内容!现在你知道了泛型类型参数、trait 和 trait bounds 以及 泛型生命周期类型,你已经准备编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切发生在编译时所以不会影响运行时效率!</p>
|
||
<p>你可能不会相信,这个领域还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景。第二十章讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作!</p>
|
||
<a class="header" href="#测试" name="测试"><h1>测试</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-00-testing.md">ch11-00-testing.md</a>
|
||
<br>
|
||
commit b7ab6668bbcb73b93c6464d8354c94a8e6c90395</p>
|
||
</blockquote>
|
||
<blockquote>
|
||
<p>Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.</p>
|
||
<p>Edsger W. Dijkstra, "The Humble Programmer" (1972)</p>
|
||
<p>软件测试是证明 bug 存在的有效方法,而证明它们不存在时则显得令人绝望的不足。</p>
|
||
<p>Edsger W. Dijkstra,【谦卑的程序员】(1972)</p>
|
||
</blockquote>
|
||
<p>程序的正确性意味着代码如我们期望的那样运行。Rust 是一个非常注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有类型的错误。为此,Rust 也包含为语言自身编写软件测试的支持。</p>
|
||
<p>例如,我们可以编写一个叫做<code>add_two</code>的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的的类型检查和借用检查。例如,这些检查会确保我们不会传递<code>String</code>或无效的引用给这个函数。Rust 所<strong>不能</strong>检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。</p>
|
||
<p>我们可以编写测试断言,比如说,当传递<code>3</code>给<code>add_two</code>函数时,应该得到<code>5</code>。当对代码进行修改时可以运行测试来确保任何现存的正确行为没有被改变。</p>
|
||
<p>测试是一项复杂的技能,而且我们也不能期望在一本书的一个章节中就涉及到编写好的测试的所有内容,所以这里仅仅讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。</p>
|
||
<a class="header" href="#编写测试" name="编写测试"><h2>编写测试</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-01-writing-tests.md">ch11-01-writing-tests.md</a>
|
||
<br>
|
||
commit c6162d22288253b2f2a017cfe96cf1aa765c2955</p>
|
||
</blockquote>
|
||
<p>测试用来验证非测试的代码按照期望的方式运行的 Rust 函数。测试函数体通常包括一些设置,运行需要测试的代码,接着断言其结果是我们所期望的。让我们看看 Rust 提供的具体用来编写测试的功能:<code>test</code>属性、一些宏和<code>should_panic</code>属性。</p>
|
||
<a class="header" href="#测试函数剖析" name="测试函数剖析"><h3>测试函数剖析</h3></a>
|
||
<p>作为最简单例子,Rust 中的测试就是一个带有<code>test</code>属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据:第五章中结构体中用到的<code>derive</code>属性就是一个例子。为了将一个函数变成测试函数,需要在<code>fn</code>行之前加上<code>#[test]</code>。当使用<code>cargo test</code>命令运行测试函数时,Rust 会构建一个测试执行者二进制文件用来运行标记了<code>test</code>属性的函数并报告每一个测试是通过还是失败。</p>
|
||
<!-- is it annotated with `test` by the user, or only automatically? I think
|
||
it's the latter, and has edited with a more active tone to make that clear, but
|
||
please change if I'm wrong -->
|
||
<!-- What do you mean by "only automatically"? The reader should be typing in
|
||
`#[test] on their own when they add new test functions; there's nothing special
|
||
about that text. I'm not sure what part of this chapter implied "only
|
||
automatically", can you point out where that's happening if we haven't taken
|
||
care of it? /Carol -->
|
||
<p>第七章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。同时可以额外增加任意多的测试函数以及测试模块!</p>
|
||
<p>我们将先通过对自动生成的测试模板做一些试验来探索测试如何工作的一些方面内容,而不实际测试任何代码。接着会写一些真实的测试来调用我们编写的代码并断言他们的行为是正确的。</p>
|
||
<p>让我们创建一个新的库项目<code>adder</code>:</p>
|
||
<pre><code>$ cargo new adder
|
||
Created library `adder` project
|
||
$ cd adder
|
||
</code></pre>
|
||
<p>adder 库中<code>src/lib.rs</code>的内容应该看起来像这样:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">#[cfg(test)]
|
||
mod tests {
|
||
#[test]
|
||
fn it_works() {
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-1: The test module and function generated
|
||
automatically for us by <code>cargo new</code> </span></p>
|
||
<p>现在让我们暂时忽略<code>tests</code>模块和<code>#[cfg(test)]</code>注解并只关注函数。注意<code>fn</code>行之前的<code>#[test]</code>:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。也可以在<code>tests</code>模块中拥有非测试的函数来帮助我们建立通用场景或进行常见操作,所以需要使用<code>#[test]</code>属性标明哪些函数是测试。</p>
|
||
<p>这个函数目前没有任何内容,这意味着没有代码会使测试失败;一个空的测试是可以通过的!让我们运行一下看看它是否通过了。</p>
|
||
<p><code>cargo test</code>命令会运行项目中所有的测试,如列表 11-2 所示:</p>
|
||
<pre><code>$ cargo test
|
||
Compiling adder v0.1.0 (file:///projects/adder)
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.22 secs
|
||
Running target/debug/deps/adder-ce99bcc2479f4607
|
||
|
||
running 1 test
|
||
test tests::it_works ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
|
||
Doc-tests adder
|
||
|
||
running 0 tests
|
||
|
||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-2: The output from running the one
|
||
automatically generated test </span></p>
|
||
<p>Cargo 编译并运行了测试。在<code>Compiling</code>、<code>Finished</code>和<code>Running</code>这几行之后,可以看到<code>running 1 test</code>这一行。下一行显示了生成的测试函数的名称,它是<code>it_works</code>,以及测试的运行结果,<code>ok</code>。接着可以看到全体测试运行结果的总结:<code>test result: ok.</code>意味着所有测试都通过了。<code>1 passed; 0 failed</code>表示通过或失败的测试数量。</p>
|
||
<p>这里并没有任何被标记为忽略的测试,所以总结表明<code>0 ignored</code>。在下一部分关于运行测试的不同方式中会讨论忽略测试。<code>0 measured</code>统计是针对测试性能的性能测试的。性能测试(benchmark tests)在编写本书时,仍只属于开发版 Rust(nightly Rust)。请查看附录 D 来了解更多开发版 Rust 的信息。</p>
|
||
<p>测试输出中以<code>Doc-tests adder</code>开头的下一部分是所有文档测试的结果。现在并没有任何文档测试,不过 Rust 会编译任何出现在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的“文档注释”部分会讲到如何编写文档测试。现在我们将忽略<code>Doc-tests</code>部分的输出。</p>
|
||
<!-- I might suggest changing the name of the function, could be misconstrued
|
||
as part of the test output! -->
|
||
<!-- `it_works` is always the name that `cargo new` generates for the first
|
||
test function, though. We wanted to show the reader what happens when you run
|
||
the tests immediately after generating a new project; they pass without you
|
||
needing to change anything. I've added a bit to walk through changing the
|
||
function name and seeing how the output changes; I hope that's sufficient.
|
||
/Carol -->
|
||
<p>让我们改变测试的名称并看看这如何改变测试的输出。给<code>it_works</code>函数起个不同的名字,比如<code>exploration</code>,像这样:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">#[cfg(test)]
|
||
mod tests {
|
||
#[test]
|
||
fn exploration() {
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>并再次运行<code>cargo test</code>。现在输出中将出现<code>exploration</code>而不是<code>it_works</code>:</p>
|
||
<pre><code>running 1 test
|
||
test tests::exploration ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。第九章讲到了最简单的造成 panic 的方法:调用<code>panic!</code>宏!写入新函数后 <code>src/lib.rs</code> 现在看起来如列表 11-3 所示:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">#[cfg(test)]
|
||
mod tests {
|
||
#[test]
|
||
fn exploration() {
|
||
}
|
||
|
||
#[test]
|
||
fn another() {
|
||
panic!("Make this test fail");
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-3: Adding a second test; one that will fail
|
||
since we call the <code>panic!</code> macro </span></p>
|
||
<p>再次<code>cargo test</code>运行测试。输出应该看起来像列表 11-4,它表明<code>exploration</code>测试通过了而<code>another</code>失败了:</p>
|
||
<pre><code class="language-text">running 2 tests
|
||
test tests::exploration ... ok
|
||
test tests::another ... FAILED
|
||
|
||
failures:
|
||
|
||
---- tests::another stdout ----
|
||
thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:9
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
|
||
failures:
|
||
tests::another
|
||
|
||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
|
||
|
||
error: test failed
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-4: Test results when one test passes and one
|
||
test fails </span></p>
|
||
<p><code>test tests::another</code>这一行是<code>FAILED</code>而不是<code>ok</code>了。在单独测试结果和总结之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,<code>another</code>因为<code>panicked at 'Make this test fail'</code>而失败,这位于 <em>src/lib.rs</em> 的第 9 行。下一部分仅仅列出了所有失败的测试,这在很有多测试和很多失败测试的详细输出时很有帮助。可以使用失败测试的名称来只运行这个测试,这样比较方便调试;下一部分会讲到更多运行测试的方法。</p>
|
||
<p>最后是总结行:总体上讲,一个测试结果是<code>FAILED</code>的。有一个测试通过和一个测试失败。</p>
|
||
<p>现在我们见过不同场景中测试结果是什么样子的了,再来看看除了<code>panic!</code>之外一些在测试中有帮助的宏吧。</p>
|
||
<a class="header" href="#使用assert宏来检查结果" name="使用assert宏来检查结果"><h3>使用<code>assert!</code>宏来检查结果</h3></a>
|
||
<p><code>assert!</code>宏由标准库提供,在希望确保测试中一些条件为<code>true</code>时非常有用。需要向<code>assert!</code>宏提供一个计算为布尔值的参数。如果值是<code>true</code>,<code>assert!</code>什么也不做同时测试会通过。如果值为<code>false</code>,<code>assert!</code>调用<code>panic!</code>宏,这会导致测试失败。这是一个帮助我们检查代码是否以期望的方式运行的宏。</p>
|
||
<!-- what kind of thing can be passed as an argument? Presumably when we use it
|
||
for real we won't pass it `true` or `false` as an argument, but some condition
|
||
that will evaluate to true or false? In which case, should below be phrased "If
|
||
the argument evaluates to true" and an explanation of that? Or maybe even a
|
||
working example would be better, this could be misleading -->
|
||
<!-- We were trying to really break it down, to show just how the `assert!`
|
||
macro works and what it looks like for it to pass or fail, before we got into
|
||
calling actual code. We've changed this section to move a bit faster and just
|
||
write actual tests instead. /Carol -->
|
||
<p>回忆一下第五章中,列表 5-9 中有一个<code>Rectangle</code>结构体和一个<code>can_hold</code>方法,在列表 11-5 中再次使用他们。将他们放进 <em>src/lib.rs</em> 而不是 <em>src/main.rs</em> 并使用<code>assert!</code>宏编写一些测试。</p>
|
||
<!-- Listing 5-9 wasn't marked as such; I'll fix it the next time I get Chapter
|
||
5 for editing. /Carol -->
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">#[derive(Debug)]
|
||
pub struct Rectangle {
|
||
length: u32,
|
||
width: u32,
|
||
}
|
||
|
||
impl Rectangle {
|
||
pub fn can_hold(&self, other: &Rectangle) -> bool {
|
||
self.length > other.length && self.width > other.width
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-5: The <code>Rectangle</code> struct and its <code>can_hold</code>
|
||
method from Chapter 5 </span></p>
|
||
<p><code>can_hold</code>方法返回一个布尔值,这意味着它完美符合<code>assert!</code>宏的使用场景。在列表 11-6 中,让我们编写一个<code>can_hold</code>方法的测试来作为练习,这里创建一个长为 8 宽为 7 的<code>Rectangle</code>实例,并假设它可以放得下另一个长为5 宽为 1 的<code>Rectangle</code>实例:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn larger_can_hold_smaller() {
|
||
let larger = Rectangle { length: 8, width: 7 };
|
||
let smaller = Rectangle { length: 5, width: 1 };
|
||
|
||
assert!(larger.can_hold(&smaller));
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-6: A test for <code>can_hold</code> that checks that a
|
||
larger rectangle indeed holds a smaller rectangle </span></p>
|
||
<p>注意在<code>tests</code>模块中新增加了一行:<code>use super::*;</code>。<code>tests</code>是一个普通的模块,它遵循第七章介绍的通常的可见性规则。因为这是一个内部模块,需要将外部模块中被测试的代码引入到内部模块的作用域中。这里选择使用全局导入使得外部模块定义的所有内容在<code>tests</code>模块中都是可用的。</p>
|
||
<p>我们将测试命名为<code>larger_can_hold_smaller</code>,并创建所需的两个<code>Rectangle</code>实例。接着调用<code>assert!</code>宏并传递<code>larger.can_hold(&smaller)</code>调用的结果作为参数。这个表达式预期会返回<code>true</code>,所以测试应该通过。让我们拭目以待!</p>
|
||
<pre><code>running 1 test
|
||
test tests::larger_can_hold_smaller ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>它确实通过了!再来增加另一个测试,这一回断言一个更小的矩形不能放下一个更大的矩形:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn larger_can_hold_smaller() {
|
||
let larger = Rectangle { length: 8, width: 7 };
|
||
let smaller = Rectangle { length: 5, width: 1 };
|
||
|
||
assert!(larger.can_hold(&smaller));
|
||
}
|
||
|
||
#[test]
|
||
fn smaller_can_hold_larger() {
|
||
let larger = Rectangle { length: 8, width: 7 };
|
||
let smaller = Rectangle { length: 5, width: 1 };
|
||
|
||
assert!(!smaller.can_hold(&larger));
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>因为这里<code>can_hold</code>函数的正确结果是<code>false</code>,我们需要将这个结果取反后传递给<code>assert!</code>宏。这样的话,测试就会通过而<code>can_hold</code>将返回<code>false</code>:</p>
|
||
<pre><code>running 2 tests
|
||
test tests::smaller_can_hold_larger ... ok
|
||
test tests::larger_can_hold_smaller ... ok
|
||
|
||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>这个通过的测试!现在让我们看看如果引入一个 bug 的话测试结果会发生什么。将<code>can_hold</code>方法中比较长度时本应使用大于号的地方改成小于号:</p>
|
||
<pre><code class="language-rust">#[derive(Debug)]
|
||
pub struct Rectangle {
|
||
length: u32,
|
||
width: u32,
|
||
}
|
||
|
||
impl Rectangle {
|
||
pub fn can_hold(&self, other: &Rectangle) -> bool {
|
||
self.length < other.length && self.width > other.width
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>现在运行测试会产生:</p>
|
||
<pre><code>running 2 tests
|
||
test tests::smaller_can_hold_larger ... ok
|
||
test tests::larger_can_hold_smaller ... FAILED
|
||
|
||
failures:
|
||
|
||
---- tests::larger_can_hold_smaller stdout ----
|
||
thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed:
|
||
larger.can_hold(&smaller)', src/lib.rs:22
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
|
||
failures:
|
||
tests::larger_can_hold_smaller
|
||
|
||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>我们的测试捕获了 bug!因为<code>larger.length</code>是 8 而<code>smaller.length</code> 是 5,<code>can_hold</code>中的长度比较现在返回<code>false</code>因为 8 不小于 5。</p>
|
||
<a class="header" href="#使用assert_eq和assert_ne宏来测试相等" name="使用assert_eq和assert_ne宏来测试相等"><h3>使用<code>assert_eq!</code>和<code>assert_ne!</code>宏来测试相等</h3></a>
|
||
<p>测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向<code>assert!</code>宏传递一个使用<code>==</code>宏的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来方便处理这些操作:<code>assert_eq!</code>和<code>assert_ne!</code>。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试<strong>为什么</strong>失败,而<code>assert!</code>只会打印出它从<code>==</code>表达式中得到了<code>false</code>值,而不是导致<code>false</code>值的原因。</p>
|
||
<p>列表 11-7 中,让我们编写一个对其参数加二并返回结果的函数<code>add_two</code>。接着使用<code>assert_eq!</code>宏测试这个函数:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">pub fn add_two(a: i32) -> i32 {
|
||
a + 2
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn it_adds_two() {
|
||
assert_eq!(4, add_two(2));
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-7: Testing the function <code>add_two</code> using the
|
||
<code>assert_eq!</code> macro </span></p>
|
||
<p>测试通过了!</p>
|
||
<pre><code>running 1 test
|
||
test tests::it_adds_two ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>传递给<code>assert_eq!</code>宏的第一个参数,4,等于调用<code>add_two(2)</code>的结果。我们将会看到这个测试的那一行说<code>test tests::it_adds_two ... ok</code>,<code>ok</code>表明测试通过了!</p>
|
||
<p>在代码中引入一个 bug 来看看使用<code>assert_eq!</code>的测试失败是什么样的。修改<code>add_two</code>函数的实现使其加 3:</p>
|
||
<pre><code class="language-rust">pub fn add_two(a: i32) -> i32 {
|
||
a + 3
|
||
}
|
||
</code></pre>
|
||
<p>再次运行测试:</p>
|
||
<pre><code>running 1 test
|
||
test tests::it_adds_two ... FAILED
|
||
|
||
failures:
|
||
|
||
---- tests::it_adds_two stdout ----
|
||
thread 'tests::it_adds_two' panicked at 'assertion failed: `(left ==
|
||
right)` (left: `4`, right: `5`)', src/lib.rs:11
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
|
||
failures:
|
||
tests::it_adds_two
|
||
|
||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>测试捕获到了 bug!<code>it_adds_two</code>测试失败并显示信息<code>assertion failed: `(left == right)` (left: `4`, right: `5`)</code>。这个信息有助于我们开始调试:它说<code>assert_eq!</code>的<code>left</code>参数是 4,而<code>right</code>参数,也就是<code>add_two(2)</code>的结果,是 5。</p>
|
||
<p>注意在一些语言和测试框架中,断言两个值相等的函数的参数叫做<code>expected</code>和<code>actual</code>,而且指定参数的顺序是需要注意的。然而在 Rust 中,他们则叫做<code>left</code>和<code>right</code>,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成<code>assert_eq!(add_two(2), 4)</code>,这时错误信息会变成<code>assertion failed: `(left == right)` (left: `5`, right: `4`)</code>。</p>
|
||
<p><code>assert_ne!</code>宏在传递给它的两个值不相等时通过而在相等时失败。这个宏在代码按照我们期望运行时不确定值<strong>应该</strong>是什么,不过知道他们绝对<strong>不应该</strong>是什么的时候最有用处。例如,如果一个函数确定会以某种方式改变其输出,不过这种方式由运行测试是星期几来决定,这时最好的断言可能就是函数的输出不等于其输入。</p>
|
||
<p><code>assert_eq!</code>和<code>assert_ne!</code>宏在底层分别使用了<code>==</code>和<code>!=</code>。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了<code>PartialEq</code>和<code>Debug</code> trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 <code>PartialEq</code>才能断言他们的值是否相等。需要实现 <code>Debug</code>才能在断言失败时打印他们的值。因为这两个 trait 都是可推导 trait,如第五章所提到的,通常可以直接在结构体或枚举上添加<code>#[derive(PartialEq, Debug)]</code>注解。附录 C 中有更多关于这些和其他可推导 trait 的详细信息。</p>
|
||
<a class="header" href="#自定义错误信息" name="自定义错误信息"><h3>自定义错误信息</h3></a>
|
||
<p>也可以向<code>assert!</code>、<code>assert_eq!</code>和<code>assert_ne!</code>宏传递一个可选的参数来增加用于打印的自定义错误信息。任何在<code>assert!</code>必需的一个参数和<code>assert_eq!</code>和<code>assert_ne!</code>必需的两个参数之后指定的参数都会传递给第八章讲到的<code>format!</code>宏,所以可以传递一个包含<code>{}</code>占位符的格式字符串和放入占位符的值。自定义信息有助于记录断言的意义,这样到测试失败时,就能更好的例子代码出了什么问题。</p>
|
||
<p>例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">pub fn greeting(name: &str) -> String {
|
||
format!("Hello {}!", name)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn greeting_contains_name() {
|
||
let result = greeting("Carol");
|
||
assert!(result.contains("Carol"));
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>这个程序的需求还没有被确定,而我们非常确定问候开始的<code>Hello</code>文本不会改变。我们决定并不想在人名改变时
|
||
不得不更新测试,所以相比检查<code>greeting</code>函数返回的确切的值,我们将仅仅断言输出的文本中包含输入参数。</p>
|
||
<p>让我们通过改变<code>greeting</code>不包含<code>name</code>来在代码中引入一个 bug 来测试失败时是怎样的,</p>
|
||
<pre><code class="language-rust">pub fn greeting(name: &str) -> String {
|
||
String::from("Hello!")
|
||
}
|
||
</code></pre>
|
||
<p>运行测试会产生:</p>
|
||
<pre><code class="language-text">running 1 test
|
||
test tests::greeting_contains_name ... FAILED
|
||
|
||
failures:
|
||
|
||
---- tests::greeting_contains_name stdout ----
|
||
thread 'tests::greeting_contains_name' panicked at 'assertion failed:
|
||
result.contains("Carol")', src/lib.rs:12
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
|
||
failures:
|
||
tests::greeting_contains_name
|
||
</code></pre>
|
||
<p>这仅仅告诉了我们断言失败了和失败的行号。一个更有用的错误信息应该打印出从<code>greeting</code>函数得到的值。让我们改变测试函数来使用一个由包含占位符的格式字符串和从<code>greeting</code>函数取得的值组成的自定义错误信息:</p>
|
||
<pre><code class="language-rust,ignore">#[test]
|
||
fn greeting_contains_name() {
|
||
let result = greeting("Carol");
|
||
assert!(
|
||
result.contains("Carol"),
|
||
"Greeting did not contain name, value was `{}`", result
|
||
);
|
||
}
|
||
</code></pre>
|
||
<p>现在如果再次运行测试,将会看到更有价值的错误信息:</p>
|
||
<pre><code>---- tests::greeting_contains_name stdout ----
|
||
thread 'tests::greeting_contains_name' panicked at 'Greeting did not contain
|
||
name, value was `Hello`', src/lib.rs:12
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
</code></pre>
|
||
<p>可以在测试输出中看到所取得的确切的值,这会帮助我们理解发生了什么而不是期望发生什么。</p>
|
||
<a class="header" href="#使用should_panic检查-panic" name="使用should_panic检查-panic"><h3>使用<code>should_panic</code>检查 panic</h3></a>
|
||
<p>除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重要的。例如,考虑第九章列表 9-8 创建的<code>Guess</code>类型。其他使用<code>Guess</code>的代码依赖于<code>Guess</code>实例只会包含 1 到 100 的值的保证。可以编写一个测试来确保创建一个超出范围的值的<code>Guess</code>实例会 panic。</p>
|
||
<p>可以通过对函数增加另一个属性<code>should_panic</code>来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。</p>
|
||
<p>列表 11-8 展示了如何编写一个测试来检查<code>Guess::new</code>按照我们的期望出现的错误情况:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">struct Guess {
|
||
value: u32,
|
||
}
|
||
|
||
impl Guess {
|
||
pub fn new(value: u32) -> Guess {
|
||
if value < 1 || value > 100 {
|
||
panic!("Guess value must be between 1 and 100, got {}.", value);
|
||
}
|
||
|
||
Guess {
|
||
value: value,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
#[should_panic]
|
||
fn greater_than_100() {
|
||
Guess::new(200);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-8: Testing that a condition will cause a
|
||
<code>panic!</code> </span></p>
|
||
<p><code>#[should_panic]</code>属性位于<code>#[test]</code>之后和对应的测试函数之前。让我们看看测试通过时它时什么样子:</p>
|
||
<pre><code>running 1 test
|
||
test tests::greater_than_100 ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>看起来不错!现在在代码中引入 bug,通过移除<code>new</code>函数在值大于 100 时会 panic 的条件:</p>
|
||
<pre><code class="language-rust"># struct Guess {
|
||
# value: u32,
|
||
# }
|
||
#
|
||
impl Guess {
|
||
pub fn new(value: u32) -> Guess {
|
||
if value < 1 {
|
||
panic!("Guess value must be between 1 and 100, got {}.", value);
|
||
}
|
||
|
||
Guess {
|
||
value: value,
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>如果运行列表 11-8 的测试,它会失败:</p>
|
||
<pre><code>running 1 test
|
||
test tests::greater_than_100 ... FAILED
|
||
|
||
failures:
|
||
|
||
failures:
|
||
tests::greater_than_100
|
||
|
||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了<code>#[should_panic]</code>。这个错误意味着代码中函数<code>Guess::new(200)</code>并没有产生 panic。</p>
|
||
<p>然而<code>should_panic</code>测试可能是非常含糊不清的,因为他们只是告诉我们代码并没有产生 panic。<code>should_panic</code>甚至在测试因为其他不同的原因而不是我们期望发生的那个而 panic 时也会通过。为了使<code>should_panic</code>测试更精确,可以给<code>should_panic</code>属性增加一个可选的<code>expected</code>参数。测试工具会确保错误信息中包含其提供的文本。例如,考虑列表 11-9 中修改过的<code>Guess</code>,这里<code>new</code>函数更具其值是过大还或者过小而提供不同的 panic 信息:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">struct Guess {
|
||
value: u32,
|
||
}
|
||
|
||
impl Guess {
|
||
pub fn new(value: u32) -> Guess {
|
||
if value < 1 {
|
||
panic!("Guess value must be greater than or equal to 1, got {}.",
|
||
value);
|
||
} else if value > 100 {
|
||
panic!("Guess value must be less than or equal to 100, got {}.",
|
||
value);
|
||
}
|
||
|
||
Guess {
|
||
value: value,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
#[should_panic(expected = "Guess value must be less than or equal to 100")]
|
||
fn greater_than_100() {
|
||
Guess::new(200);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-9: Testing that a condition will cause a
|
||
<code>panic!</code> with a particular panic message </span></p>
|
||
<p>这个测试会通过,因为<code>should_panic</code>属性中<code>expected</code>参数提供的值是<code>Guess::new</code>函数 panic 信息的子字符串。我们可以指定期望的整个 panic 信息,在这个例子中是<code>Guess value must be less than or equal to 100, got 200.</code>。这依赖于 panic 有多独特或动态和你希望测试有多准确。在这个例子中,错误信息的子字符串足以确保函数在<code>else if value > 100</code>的情况下运行。</p>
|
||
<p>为了观察带有<code>expected</code>信息的<code>should_panic</code>测试失败时会发生什么,让我们再次引入一个 bug 来将<code>if value < 1</code>和<code>else if value > 100</code>的代码块对换:</p>
|
||
<pre><code class="language-rust,ignore">if value < 1 {
|
||
panic!("Guess value must be less than or equal to 100, got {}.", value);
|
||
} else if value > 100 {
|
||
panic!("Guess value must be greater than or equal to 1, got {}.", value);
|
||
}
|
||
</code></pre>
|
||
<p>这一次运行<code>should_panic</code>测试,它会失败:</p>
|
||
<pre><code>running 1 test
|
||
test tests::greater_than_100 ... FAILED
|
||
|
||
failures:
|
||
|
||
---- tests::greater_than_100 stdout ----
|
||
thread 'tests::greater_than_100' panicked at 'Guess value must be greater
|
||
than or equal to 1, got 200.', src/lib.rs:10
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
note: Panic did not include expected string 'Guess value must be less than or
|
||
equal to 100'
|
||
|
||
failures:
|
||
tests::greater_than_100
|
||
|
||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>错误信息表明测试确实如期望 panic 了,不过 panic 信息<code>did not include expected string 'Guess value must be less than or equal to 100'</code>。可以看到我们的到的 panic 信息,在这个例子中是<code>Guess value must be greater than or equal to 1, got 200.</code>。这样就可以开始寻找 bug 在哪了!</p>
|
||
<p>现在我们讲完了编写测试的方法,让我们看看运行测试时会发生什么并讨论可以用于<code>cargo test</code>的不同选项。</p>
|
||
<a class="header" href="#运行测试" name="运行测试"><h2>运行测试</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-02-running-tests.md">ch11-02-running-tests.md</a>
|
||
<br>
|
||
commit 55b294f20fc846a13a9be623bf322d8b364cee77</p>
|
||
</blockquote>
|
||
<p>就像<code>cargo run</code>会编译代码并运行生成的二进制文件,<code>cargo test</code>在测试模式下编译代码并运行生成的测试二进制文件。这里有一些选项可以用来改变<code>cargo test</code>的默认行为。例如,<code>cargo test</code>生成的二进制文件的默认行为是并行的运行所有测试,并捕获测试运行过程中产生的输出避免他们被显示出来使得阅读测试结果相关的内容变得更容易。可以指定命令行参数来改变这些默认行为。</p>
|
||
<p>这些选项的一部分可以传递给<code>cargo test</code>,而另一些则需要传递给生成的测试二进制文件。为了分隔两种类型的参数,首先列出传递给<code>cargo test</code>的参数,接着是分隔符<code>--</code>,再之后是传递给测试二进制文件的参数。运行<code>cargo test --help</code>会告诉你<code>cargo test</code>的相关参数,而运行<code>cargo test -- --help</code>则会告诉你位于分隔符<code>--</code>之后的相关参数。</p>
|
||
<a class="header" href="#并行或连续的运行测试" name="并行或连续的运行测试"><h3>并行或连续的运行测试</h3></a>
|
||
<!-- Are we safe assuming the reader will know enough about threads in this
|
||
context? -->
|
||
<!-- Yes /Carol -->
|
||
<p>当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该小心测试不能相互依赖或任何共享状态,包括类似于当前工作目录或者环境变量这样的共享环境。</p>
|
||
<p>例如,每一个测试都运行一些代码在硬盘上创建一个<code>test-output.txt</code>文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中覆盖了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干涉。一个解决方案是使每一个测试读写不同的文件;另一个是一次运行一个测试。</p>
|
||
<p>如果你不希望测试并行运行,或者想要更加精确的控制使用线程的数量,可以传递<code>--test-threads</code>参数和希望使用线程的数量给测试二进制文件。例如:</p>
|
||
<pre><code>$ cargo test -- --test-threads=1
|
||
</code></pre>
|
||
<p>这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过测试就不会在存在共享状态时潜在的相互干涉了。</p>
|
||
<a class="header" href="#显示测试输出" name="显示测试输出"><h3>显示测试输出</h3></a>
|
||
<p>如果测试通过了,Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试中调用<code>println!</code>而测试通过了,我们将不会在终端看到<code>println!</code>的输出:只会看到说明测试通过的行。如果测试失败了,就会看到任何标准输出和其他错误信息。</p>
|
||
<p>例如,列表 11-20 有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">fn prints_and_returns_10(a: i32) -> i32 {
|
||
println!("I got the value {}", a);
|
||
10
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn this_test_will_pass() {
|
||
let value = prints_and_returns_10(4);
|
||
assert_eq!(10, value);
|
||
}
|
||
|
||
#[test]
|
||
fn this_test_will_fail() {
|
||
let value = prints_and_returns_10(8);
|
||
assert_eq!(5, value);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-10: Tests for a function that calls <code>println!</code>
|
||
</span></p>
|
||
<p>运行<code>cargo test</code>将会看到这些测试的输出:</p>
|
||
<pre><code>running 2 tests
|
||
test tests::this_test_will_pass ... ok
|
||
test tests::this_test_will_fail ... FAILED
|
||
|
||
failures:
|
||
|
||
---- tests::this_test_will_fail stdout ----
|
||
I got the value 8
|
||
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left ==
|
||
right)` (left: `5`, right: `10`)', src/lib.rs:19
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
|
||
failures:
|
||
tests::this_test_will_fail
|
||
|
||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>注意输出中哪里也不会出现<code>I got the value 4</code>,这是当测试通过时打印的内容。这些输出被捕获。失败测试的输出,<code>I got the value 8</code>,则出现在输出的测试总结部分,它也显示了测试失败的原因。</p>
|
||
<p>如果你希望也能看到通过的测试中打印的值,捕获输出的行为可以通过<code>--nocapture</code>参数来禁用:</p>
|
||
<pre><code>$ cargo test -- --nocapture
|
||
</code></pre>
|
||
<p>使用<code>--nocapture</code>参数再次运行列表 11-10 中的测试会显示:</p>
|
||
<pre><code>running 2 tests
|
||
I got the value 4
|
||
I got the value 8
|
||
test tests::this_test_will_pass ... ok
|
||
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left ==
|
||
right)` (left: `5`, right: `10`)', src/lib.rs:19
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
test tests::this_test_will_fail ... FAILED
|
||
|
||
failures:
|
||
|
||
failures:
|
||
tests::this_test_will_fail
|
||
|
||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>注意测试的输出和测试结果的输出是相互交叉的;这是由于上一部分讲到的测试是并行运行的。尝试一同使用<code>--test-threads=1</code>和<code>--nocapture</code>功能来看看输出是什么样子!</p>
|
||
<a class="header" href="#通过名称来运行测试的子集" name="通过名称来运行测试的子集"><h3>通过名称来运行测试的子集</h3></a>
|
||
<p>有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。可以向<code>cargo test</code>传递希望运行的测试的(部分)名称作为参数来选择运行哪些测试。</p>
|
||
<p>为了展示如何运行测试的子集,列表 11-11 使用<code>add_two</code>函数创建了三个测试来供我们选择运行哪一个:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">pub fn add_two(a: i32) -> i32 {
|
||
a + 2
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn add_two_and_two() {
|
||
assert_eq!(4, add_two(2));
|
||
}
|
||
|
||
#[test]
|
||
fn add_three_and_two() {
|
||
assert_eq!(5, add_two(3));
|
||
}
|
||
|
||
#[test]
|
||
fn one_hundred() {
|
||
assert_eq!(102, add_two(100));
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-11: Three tests with a variety of names</span></p>
|
||
<p>如果没有传递任何参数就运行测试,如你所见,所有测试都会并行运行:</p>
|
||
<pre><code>running 3 tests
|
||
test tests::add_two_and_two ... ok
|
||
test tests::add_three_and_two ... ok
|
||
test tests::one_hundred ... ok
|
||
|
||
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<a class="header" href="#运行单个测试" name="运行单个测试"><h4>运行单个测试</h4></a>
|
||
<p>可以向<code>cargo test</code>传递任意测试的名称来只运行这个测试:</p>
|
||
<pre><code>$ cargo test one_hundred
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running target/debug/deps/adder-06a75b4a1f2515e9
|
||
|
||
running 1 test
|
||
test tests::one_hundred ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>不能像这样指定多个测试名称,只有传递给<code>cargo test</code>的第一个值才会被使用。</p>
|
||
<a class="header" href="#过滤运行多个测试" name="过滤运行多个测试"><h4>过滤运行多个测试</h4></a>
|
||
<p>然而,可以指定测试的部分名称,这样任何名称匹配这个值的测试会被运行。例如,因为头两个测试的名称包含<code>add</code>,可以通过<code>cargo test add</code>来运行这两个测试:</p>
|
||
<pre><code>$ cargo test add
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running target/debug/deps/adder-06a75b4a1f2515e9
|
||
|
||
running 2 tests
|
||
test tests::add_two_and_two ... ok
|
||
test tests::add_three_and_two ... ok
|
||
|
||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>这运行了所有名字中带有<code>add</code>的测试。同时注意测试所在的模块作为测试名称的一部分,所以可以通过模块名来过滤运行一个模块中的所有测试。</p>
|
||
<!-- in what kind of situation might you need to run only some tests, when you
|
||
have lots and lots in a program? -->
|
||
<!-- We covered this in the first paragraph of the "Running a Subset of Tests
|
||
by Name" section, do you think it should be repeated so soon? Most people who
|
||
use tests have sufficient motivation for wanting to run a subset of the tests,
|
||
they just need to know how to do it with Rust, so we don't think this is a
|
||
point that needs to be emphasized multiple times. /Carol -->
|
||
<a class="header" href="#除非指定否则忽略某些测试" name="除非指定否则忽略某些测试"><h3>除非指定否则忽略某些测试</h3></a>
|
||
<p>有时一些特定的测试执行起来是非常耗费时间的,所以在运行大多数<code>cargo test</code>的时候希望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用<code>ignore</code>属性来标记耗时的测试来排除他们:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">#[test]
|
||
fn it_works() {
|
||
assert!(true);
|
||
}
|
||
|
||
#[test]
|
||
#[ignore]
|
||
fn expensive_test() {
|
||
// code that takes an hour to run
|
||
}
|
||
</code></pre>
|
||
<p>我们对想要排除的测试的<code>#[test]</code>之后增加了<code>#[ignore]</code>行。现在如果运行测试,就会发现<code>it_works</code>运行了,而<code>expensive_test</code>没有运行:</p>
|
||
<pre><code>$ cargo test
|
||
Compiling adder v0.1.0 (file:///projects/adder)
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.24 secs
|
||
Running target/debug/deps/adder-ce99bcc2479f4607
|
||
|
||
running 2 tests
|
||
test expensive_test ... ignored
|
||
test it_works ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured
|
||
|
||
Doc-tests adder
|
||
|
||
running 0 tests
|
||
|
||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p><code>expensive_test</code>被列为<code>ignored</code>,如果只希望运行被忽略的测试,可以使用<code>cargo test -- --ignored</code>来请求运行他们:</p>
|
||
<!-- what does the double `-- --` mean? That seems interesting -->
|
||
<!-- We covered that in the second paragraph after the "Controlling How Tests
|
||
are Run" heading, and this section is beneath that heading, so I don't think a
|
||
back reference is needed /Carol -->
|
||
<!-- is that right, this way the program knows to run only the test with
|
||
`ignore` if we add this, or it knows to run all tests? -->
|
||
<!-- Is this unclear from the output that shows `expensive_test` was run and
|
||
the `it_works` test does not appear? I'm not sure how to make this clearer.
|
||
/Carol -->
|
||
<pre><code class="language-text">$ cargo test -- --ignored
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running target/debug/deps/adder-ce99bcc2479f4607
|
||
|
||
running 1 test
|
||
test expensive_test ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>通过控制运行哪些测试,可以确保运行<code>cargo test</code>的结果是快速的。当某个时刻需要检查<code>ignored</code>测试的结果而且你也有时间等待这个结果的话,可以选择执行<code>cargo test -- --ignored</code>。</p>
|
||
<a class="header" href="#测试的组织结构" name="测试的组织结构"><h2>测试的组织结构</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-03-test-organization.md">ch11-03-test-organization.md</a>
|
||
<br>
|
||
commit 55b294f20fc846a13a9be623bf322d8b364cee77</p>
|
||
</blockquote>
|
||
<p>正如之前提到的,测试是一个很广泛的学科,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:<strong>单元测试</strong>(<em>unit tests</em>)与<strong>集成测试</strong>(<em>integration tests</em>)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对公有接口而且每个测试都会测试多个模块。</p>
|
||
<p>这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。</p>
|
||
<a class="header" href="#单元测试" name="单元测试"><h3>单元测试</h3></a>
|
||
<p>单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 <em>src</em> 目录中,与他们要测试的代码存在于相同的文件中。传统做法是在每个文件中创建包含测试函数的<code>tests</code>模块,并使用<code>cfg(test)</code>标注模块。</p>
|
||
<a class="header" href="#测试模块和cfgtest" name="测试模块和cfgtest"><h4>测试模块和<code>cfg(test)</code></h4></a>
|
||
<p>测试模块的<code>#[cfg(test)]</code>注解告诉 Rust 只在执行<code>cargo test</code>时才编译和运行测试代码,而在运行<code>cargo build</code>时不这么做。这在只希望构建库的时候可以节省编译时间,并能节省编译产物的空间因为他们并没有包含测试。我们将会看到因为集成测试位于另一个文件夹,他们并不需要<code>#[cfg(test)]</code>注解。但是因为单元测试位于与源码相同的文件中,所以使用<code>#[cfg(test)]</code>来指定他们不应该被包含进编译产物中。</p>
|
||
<p>还记得本章第一部分新建的<code>adder</code>项目吗?Cargo 为我们生成了如下代码:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">#[cfg(test)]
|
||
mod tests {
|
||
#[test]
|
||
fn it_works() {
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>这里自动生成了测试模块。<code>cfg</code>属性代表 <em>configuration</em> ,它告诉 Rust 其之后的项只被包含进特定配置中。在这个例子中,配置是<code>test</code>,Rust 所提供的用于编译和运行测试的配置。通过使用这个属性,Cargo 只会在我们主动使用<code>cargo test</code>运行测试时才编译测试代码。除了标注为<code>#[test]</code>的函数之外,这还包括测试模块中可能存在的帮助函数。</p>
|
||
<a class="header" href="#测试私有函数" name="测试私有函数"><h4>测试私有函数</h4></a>
|
||
<p>测试社区中一直存在关于是否应该对私有函数进行单元测试的论战,而其他语言中难以甚至不可能测试私有函数。不过无论你坚持哪种测试意识形态,Rust 的私有性规则确实允许你测试私有函数,由于私有性规则。考虑列表 11-12 中带有私有函数<code>internal_adder</code>的代码:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">pub fn add_two(a: i32) -> i32 {
|
||
internal_adder(a, 2)
|
||
}
|
||
|
||
fn internal_adder(a: i32, b: i32) -> i32 {
|
||
a + b
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn internal() {
|
||
assert_eq!(4, internal_adder(2, 2));
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-12: Testing a private function</span></p>
|
||
<!-- I'm not clear on why we would assume this might not be fine, why are we
|
||
highlighting this specifically? -->
|
||
<!-- We're addressing experience that the reader might bring with them from
|
||
other languages where this is not allowed; I added a sentence mentioning "other
|
||
languages" at the beginning of this section. Also testing private functions
|
||
from integration tests is not allowed, so if you did want to do this, you'd
|
||
have to do it in unit tests. /Carol -->
|
||
<p>注意<code>internal_adder</code>函数并没有标记为<code>pub</code>,不过因为测试也不过是 Rust 代码而<code>tests</code>也仅仅是另一个模块,我们完全可以在测试中导入和调用<code>internal_adder</code>。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。</p>
|
||
<a class="header" href="#集成测试" name="集成测试"><h3>集成测试</h3></a>
|
||
<p>在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件,这意味着他们只能调用作为库公有 API 的一部分的函数。他们的目的是测试库的多个部分能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 <em>tests</em> 目录。</p>
|
||
<a class="header" href="#tests-目录" name="tests-目录"><h4><em>tests</em> 目录</h4></a>
|
||
<p>为了编写集成测试,需要在项目根目录创建一个 <em>tests</em> 目录,与 <em>src</em> 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个文件夹中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。</p>
|
||
<p>让我们试一试吧!保留列表 11-12 中 <em>src/lib.rs</em> 的代码。创建一个 <em>tests</em> 目录,新建一个文件 <em>tests/integration_test.rs</em>,并输入列表 11-13 中的代码。</p>
|
||
<p><span class="filename">Filename: tests/integration_test.rs</span></p>
|
||
<pre><code class="language-rust,ignore">extern crate adder;
|
||
|
||
#[test]
|
||
fn it_adds_two() {
|
||
assert_eq!(4, adder::add_two(2));
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 11-13: An integration test of a function in the
|
||
<code>adder</code> crate </span></p>
|
||
<p>我们在顶部增加了<code>extern crate adder</code>,这在单元测试中是不需要的。这是因为每一个<code>tests</code>目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。集成测试就像其他使用者那样通过导入 crate 并只使用公有 API 来使用库文件。</p>
|
||
<p>并不需要将 <em>tests/integration_test.rs</em> 中的任何代码标注为<code>#[cfg(test)]</code>。Cargo 对<code>tests</code>文件夹特殊处理并只会在运行<code>cargo test</code>时编译这个目录中的文件。现在就试试运行<code>cargo test</code>:</p>
|
||
<pre><code>cargo test
|
||
Compiling adder v0.1.0 (file:///projects/adder)
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.31 secs
|
||
Running target/debug/deps/adder-abcabcabc
|
||
|
||
running 1 test
|
||
test tests::internal ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
|
||
Running target/debug/deps/integration_test-ce99bcc2479f4607
|
||
|
||
running 1 test
|
||
test it_adds_two ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
|
||
Doc-tests adder
|
||
|
||
running 0 tests
|
||
|
||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<!-- what are the doc tests? How do we tell the difference between unit and
|
||
integration tests here? -->
|
||
<!-- We mentioned documentation tests in the beginning of this chapter /Carol
|
||
-->
|
||
<p>现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每一个单元测试一行(列表 11-12 中有一个叫做<code>internal</code>的测试),接着是一个单元测试的总结行。</p>
|
||
<p>集成测试部分以行<code>Running target/debug/deps/integration-test-ce99bcc2479f4607</code>(输出最后的哈希值可能不同)开头。接着是每一个集成测试中的测试函数一行,以及一个就在<code>Doc-tests adder</code>部分开始之前的集成测试的总结行。</p>
|
||
<p>注意在任意 <em>src</em> 文件中增加更多单元测试函数会增加更多单元测试部分的测试结果行。在我们创建的集成测试文件中增加更多测试函数会增加更多集成测试部分的行。每一个集成测试文件有其自己的部分,所以如果在 <em>tests</em> 目录中增加更多文件,这里就会有更多集成测试部分。</p>
|
||
<p>我们仍然可以通过指定测试函数的名称作为<code>cargo test</code>的参数来运行特定集成测试。为了运行某个特定集成测试文件中的所有测试,使用<code>cargo test</code>的<code>--test</code>后跟文件的名称:</p>
|
||
<pre><code>$ cargo test --test integration_test
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running target/debug/integration_test-952a27e0126bb565
|
||
|
||
running 1 test
|
||
test it_adds_two ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>这些只是 <em>tests</em> 目录中我们指定的文件中的测试。</p>
|
||
<a class="header" href="#集成测试中的子模块" name="集成测试中的子模块"><h4>集成测试中的子模块</h4></a>
|
||
<p>随着集成测试的增加,你可能希望在 <code>tests</code> 目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 <em>tests</em> 目录中的文件都被编译为单独的 crate。</p>
|
||
<p>将每个集成测试文件当作其自己的 crate 来对待有助于创建更类似与终端用户使用 crate 那样的单独的作用域。然而,这意味着考虑到像第七章学习的如何将代码分隔进模块和文件那样,<em>tests</em> 目录中的文件不能像 <em>src</em> 中的文件那样共享相同的行为。</p>
|
||
<p>对于 <em>tests</em> 目录中文件的不同行为,通常在如果有一系列有助于多个集成测试文件的帮助函数,而你尝试遵循第七章的步骤将他们提取到一个通用的模块中时显得很明显。例如,如果我们创建了 <em>tests/common.rs</em> 并将<code>setup</code>函数放入其中,这里将放入一些希望能够在多个测试文件的多个测试函数中调用的代码:</p>
|
||
<p><span class="filename">Filename: tests/common.rs</span></p>
|
||
<pre><code class="language-rust">pub fn setup() {
|
||
// setup code specific to your library's tests would go here
|
||
}
|
||
</code></pre>
|
||
<p>如果再次运行测试,将会在测试结果中看到一个对应 <em>common.rs</em> 文件的新部分,即便这个文件并没有包含任何测试函数,或者没有任何地方调用了<code>setup</code>函数:</p>
|
||
<pre><code>running 1 test
|
||
test tests::internal ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
|
||
Running target/debug/deps/common-b8b07b6f1be2db70
|
||
|
||
running 0 tests
|
||
|
||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||
|
||
Running target/debug/deps/integration_test-d993c68b431d39df
|
||
|
||
running 1 test
|
||
test it_adds_two ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
|
||
Doc-tests adder
|
||
|
||
running 0 tests
|
||
|
||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<!-- The new section is lines 6-10, will ghost everything else in libreoffice
|
||
/Carol -->
|
||
<p><code>common</code>出现在测试结果中并显示<code>running 0 tests</code>,这不是我们想要的;我们只是希望能够在其他集成测试文件中分享一些代码罢了。</p>
|
||
<p>为了使<code>common</code>不出现在测试输出中,需要使用第七章学习到的另一个将代码提取到文件的方式:不再创建<em>tests/common.rs</em>,而是创建 <em>tests/common/mod.rs</em>。当将<code>setup</code>代码移动到 <em>tests/common/mod.rs</em> 并去掉 <em>tests/common.rs</em> 文件之后,测试输出中将不会出现这一部分。<em>tests</em> 目录中的子目录不会被作为单独的 crate 编译或作为一部分出现在测试输出中。</p>
|
||
<p>一旦拥有了 <em>tests/common/mod.rs</em>,就可以将其作为模块来在任何集成测试文件中使用。这里是一个 <em>tests/integration_test.rs</em> 中调用<code>setup</code>函数的<code>it_adds_two</code>测试的例子:</p>
|
||
<p><span class="filename">Filename: tests/integration_test.rs</span></p>
|
||
<pre><code class="language-rust,ignore">extern crate adder;
|
||
|
||
mod common;
|
||
|
||
#[test]
|
||
fn it_adds_two() {
|
||
common::setup();
|
||
assert_eq!(4, adder::add_two(2));
|
||
}
|
||
</code></pre>
|
||
<p>注意<code>mod common;</code>声明与第七章中的模块声明相同。接着在测试函数中就可以调用<code>common::setup()</code>了。</p>
|
||
<a class="header" href="#二进制-crate-的集成测试" name="二进制-crate-的集成测试"><h4>二进制 crate 的集成测试</h4></a>
|
||
<p>如果项目是二进制 crate 并且只包含 <em>src/main.rs</em> 而没有 <em>src/lib.rs</em>,这样就不可能在 <em>tests</em> 创建集成测试并使用 <code>extern crate</code> 导入 <em>src/main.rs</em> 中的函数了。只有库 crate 向其他 crate 暴露了可以调用和使用的函数;二进制 crate 只意在单独运行。</p>
|
||
<p>这也是 Rust 二进制项目明确采用 <em>src/main.rs</em> 调用 <em>src/lib.rs</em> 中逻辑这样的结构的原因之一。通过这种结构,集成测试<strong>就可以</strong>使用<code>extern crate</code>测试库 crate 中的主要功能,而如果这些重要的功能没有问题的话,<em>src/main.rs</em> 中的少量代码也就会正常工作且不需要测试。</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>Rust 的测试功能提供了一个确保即使做出改变函数也能继续以指定方式运行的途径。单元测试独立的验证库的不同部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来工作时的用例,并像其他代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望相关的逻辑 bug 是很重要的。</p>
|
||
<p>接下来让我们结合本章所学和其他之前章节的知识,在下一章一起编写一个项目!</p>
|
||
<a class="header" href="#一个-io-项目构建一个小巧的-grep" name="一个-io-项目构建一个小巧的-grep"><h1>一个 I/O 项目:构建一个小巧的 grep</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-00-an-io-project.md">ch12-00-an-io-project.md</a>
|
||
<br>
|
||
commit 1f432fc231cfbc310433ab2a354d77058444288c</p>
|
||
</blockquote>
|
||
<!-- We might need a more descriptive title, something that captures the new
|
||
elements we're introducing -- are we going to cover things like environment
|
||
variables more in later chapters, or is this the only place we explain how to
|
||
use them? -->
|
||
<!-- This is the only place we were planning on explaining both environment
|
||
variables and printing to standard error. These are things that people commonly
|
||
want to know how to do in Rust, but there's not much more than what we've said
|
||
here about them, people just want to know how to do them in Rust. We realize
|
||
that those sections make this chapter long, but think it's worth it to include
|
||
information that people want. We've gotten really positive feedback from people
|
||
who have read this chapter online; people who like learning through projects
|
||
have really enjoyed this chapter. /Carol-->
|
||
<p>本章既是一个目前所学的很多技能的概括,也是一个更多标准库功能的探索。我们将构建一个与文件和命令行输入/输出交互的命令行工具来练习现在一些你已经掌握的 Rust 技能。</p>
|
||
<p>Rust 的运行速度、安全性、“单二进制文件”输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:<code>grep</code>。grep 是“Globally search a Regular Expression and Print.”的首字母缩写。<code>grep</code>最简单的使用场景是使用如下步骤在特定文件中搜索指定字符串:</p>
|
||
<ul>
|
||
<li>获取一个文件和一个字符串作为参数。</li>
|
||
<li>读取文件</li>
|
||
<li>寻找文件中包含字符串参数的行</li>
|
||
<li>打印出这些行</li>
|
||
</ul>
|
||
<p>我们还会展示如何使用环境变量和打印到标准错误而不是标准输出;这些功能在命令行工具中是很常用的。</p>
|
||
<p>一位 Rust 社区的成员,Andrew Gallant,已经创建了一个功能完整且非常快速的<code>grep</code>版本,叫做<code>ripgrep</code>。相比之下,我们的<code>grep</code>将非常简单,本章将交给你一些帮助你理解像<code>ripgrep</code>这样真实项目的背景知识。</p>
|
||
<p>这个项目将会结合之前所学的一些内容:</p>
|
||
<ul>
|
||
<li>代码组织(使用第七章学习的模块)</li>
|
||
<li>vector 和字符串(第八章,集合)</li>
|
||
<li>错误处理(第九章)</li>
|
||
<li>合理的使用 trait 和生命周期(第十章)</li>
|
||
<li>测试(第十一章)</li>
|
||
</ul>
|
||
<p>另外,我还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在第十三章和第十七章中详细介绍。</p>
|
||
<p>让我们一如既往的使用<code>cargo new</code>创建一个新项目。我们称之为<code>greprs</code>以便与可能已经安装在系统上的<code>grep</code>工具相区别:</p>
|
||
<pre><code>$ cargo new --bin greprs
|
||
Created binary (application) `greprs` project
|
||
$ cd greprs
|
||
</code></pre>
|
||
<a class="header" href="#接受命令行参数" name="接受命令行参数"><h2>接受命令行参数</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-01-accepting-command-line-arguments.md">ch12-01-accepting-command-line-arguments.md</a>
|
||
<br>
|
||
commit b8e4fcbf289b82c12121b282747ce05180afb1fb</p>
|
||
</blockquote>
|
||
<p>第一个任务是让<code>greprs</code>能够接受两个命令行参数:文件名和要搜索的字符串。也就是说希望能够使用<code>cargo run</code>,要搜索的字符串和被搜索的文件的路径来运行程序,像这样:</p>
|
||
<pre><code>$ cargo run searchstring example-filename.txt
|
||
</code></pre>
|
||
<p>现在<code>cargo new</code>生成的程序忽略任何传递给它的参数。crates.io 上有一些现存的可以帮助我们接受命令行参数的库,不过因为我们正在学习,让我们实现一个。</p>
|
||
<!--Below -- I'm not clear what we need the args function for, yet, can you set
|
||
it out more concretely? Otherwise, will it make more sense in context of the
|
||
code later? Is this function needed to allow our function to accept arguments,
|
||
is that was "args" is for? -->
|
||
<!-- We mentioned in the intro to this chapter that grep takes as arguments a
|
||
filename and a string. I've added an example of how we want to run our
|
||
resulting tool and what we want the behavior to be, please let me know if this
|
||
doesn't clear it up. /Carol-->
|
||
<a class="header" href="#读取参数值" name="读取参数值"><h3>读取参数值</h3></a>
|
||
<p>为了能够获取传递给程序的命令行参数的值,我们需要调用一个 Rust 标准库提供的函数:<code>std::env::args</code>。这个函数返回一个传递给程序的命令行参数的<strong>迭代器</strong>(<em>iterator</em>)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们现在的目的来说只需要明白两点:</p>
|
||
<ol>
|
||
<li>迭代器生成一系列的值。</li>
|
||
<li>在迭代器上调用<code>collect</code>方法可以将其生成的元素转换为一个 vector。</li>
|
||
</ol>
|
||
<p>让我们尝试一下:使用列表 12-1 中的代码来读取任何传递给<code>greprs</code>的命令行参数并将其收集到一个 vector 中。</p>
|
||
<!-- Give what a try, here, what are we making? Can you lay that out? I've
|
||
tried above but I'm not sure it's complete -->
|
||
<!-- We're not creating anything, we're just reading. I'm not sure if I've made
|
||
this clearer. /Carol -->
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::env;
|
||
|
||
fn main() {
|
||
let args: Vec<String> = env::args().collect();
|
||
println!("{:?}", args);
|
||
}
|
||
</code></pre>
|
||
<p>Listing 12-1: Collect the command line arguments into a vector and print them
|
||
out</p>
|
||
<!-- Will add wingdings in libreoffice /Carol -->
|
||
<p>首先使用<code>use</code>语句来将<code>std::env</code>模块引入作用域以便可以使用它的<code>args</code>函数。注意<code>std::env::args</code>函数嵌套进了两层模块中。如第七章讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用<code>std::env</code>中的其他函数。这比增加了<code>use std::env::args;</code>后仅仅使用<code>args</code>调用函数要更明确一些;这样看起来好像一个定义于当前模块的函数。</p>
|
||
<!-- We realized that we need to add the following caveat to fully specify
|
||
the behavior of `std::env::args` /Carol -->
|
||
<!-- PROD: START BOX -->
|
||
<blockquote>
|
||
<p>注意:<code>std::env::args</code>在其任何参数包含无效 Unicode 字符时会 panic。如果你需要接受包含无效 Unicode 字符的参数,使用<code>std::env::args_os</code>代替。这个函数返回<code>OsString</code>值而不是<code>String</code>值。出于简单考虑这里使用<code>std::env::args</code>,因为<code>OsString</code>值每个平台都不一样而且比<code>String</code>值处理起来更复杂。</p>
|
||
</blockquote>
|
||
<!-- PROD: END BOX -->
|
||
<!--what is it we're making into a vector here, the arguments we pass?-->
|
||
<!-- The iterator of the arguments. /Carol -->
|
||
<p>在<code>main</code>函数的第一行,我们调用了<code>env::args</code>,并立即使用<code>collect</code>来创建了一个包含迭代器所有值的 vector。<code>collect</code>可以被用来创建很多类型的集合,所以这里显式注明的<code>args</code>类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,<code>collect</code>就是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。</p>
|
||
<p>最后,我们使用调试格式<code>:?</code>打印出 vector。让我们尝试不用参数运行代码,接着用两个参数:</p>
|
||
<pre><code>$ cargo run
|
||
["target/debug/greprs"]
|
||
|
||
$ cargo run needle haystack
|
||
...snip...
|
||
["target/debug/greprs", "needle", "haystack"]
|
||
</code></pre>
|
||
<!--Below --- This initially confused me, do you mean that the argument at
|
||
index 0 is taken up by the name of the binary, so we start arguments at 1 when
|
||
setting them? It seems like it's something like that, reading on, and I've
|
||
edited as such, can you check? -->
|
||
<!-- Mentioning the indexes here seemed repetitive with the text after Listing
|
||
12-2. We're not "setting" arguments here, we're saving the value in variables.
|
||
I've hopefully cleared this up without needing to introduce repetition.
|
||
/Carol-->
|
||
<p>你可能注意到了 vector 的第一个值是"target/debug/greprs",它是我们二进制文件的名称。其原因超出了本章介绍的范围,不过需要记住的是我们保存了所需的两个参数。</p>
|
||
<a class="header" href="#将参数值保存进变量" name="将参数值保存进变量"><h3>将参数值保存进变量</h3></a>
|
||
<p>打印出参数 vector 中的值仅仅展示了可以访问程序中指定为命令行参数的值。但是这并不是我们想要做的,我们希望将这两个参数的值保存进变量这样就可以在程序使用这些值。让我们如列表 12-2 这样做:</p>
|
||
<!-- By 'find the ones we care about' did you mean set particular arguments so
|
||
the user knows what to enter? I'm a little confused about what we are doing,
|
||
I've tried to clarify above -->
|
||
<!-- We're incrementally adding features and adding some code that helps the
|
||
reader be able to see and experience what the code is doing rather than just
|
||
taking our word for it. I've hopefully clarified below. /Carol -->
|
||
<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 query = &args[1];
|
||
let filename = &args[2];
|
||
|
||
println!("Searching for {}", query);
|
||
println!("In file {}", filename);
|
||
}
|
||
</code></pre>
|
||
<p>Listing 12-2: Create variables to hold the query argument and filename argument</p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>正如我们在打印出 vector 时所看到的,程序的名称占据了 vector 的第一个值<code>args[0]</code>,所以我们从索引<code>1</code>开始。第一个参数<code>greprs</code>是需要搜索的字符串,所以将其将第一个参数的引用存放在变量<code>query</code>中。第二个参数将是文件名,所以将第二个参数的引用放入变量<code>filename</code>中。</p>
|
||
<p>我们将临时打印出出这些变量的值,再一次证明代码如我们期望的那样工作。让我们使用参数<code>test</code>和<code>sample.txt</code>再次运行这个程序:</p>
|
||
<pre><code>$ cargo run test sample.txt
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running `target/debug/greprs test sample.txt`
|
||
Searching for test
|
||
In file sample.txt
|
||
</code></pre>
|
||
<p>好的,它可以工作!我们将所需的参数值保存进了对应的变量中。之后会增加一些错误处理来应对类似用户没有提供参数的情况,不过现在我们将忽略他们并开始增加读取文件功能。</p>
|
||
<a class="header" href="#读取文件" name="读取文件"><h2>读取文件</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-02-reading-a-file.md">ch12-02-reading-a-file.md</a>
|
||
<br>
|
||
commit b8e4fcbf289b82c12121b282747ce05180afb1fb</p>
|
||
</blockquote>
|
||
<p>接下来我们将读取由命令行文件名参数指定的文件。首先,需要一个用来测试的示例文件——用来确保<code>greprs</code>正常工作的最好的文件是拥有少量文本和多个行且有一些重复单词的文件。列表 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件<code>poem.txt</code>,并输入诗 "I'm nobody! Who are you?":</p>
|
||
<p><span class="filename">Filename: poem.txt</span></p>
|
||
<pre><code class="language-text">I'm nobody! Who are you?
|
||
Are you nobody, too?
|
||
Then there's a pair of us — don't tell!
|
||
They'd banish us, you know.
|
||
|
||
How dreary to be somebody!
|
||
How public, like a frog
|
||
To tell your name the livelong day
|
||
To an admiring bog!
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-3: The poem "I'm nobody! Who are you?" by
|
||
Emily Dickinson that will make a good test case</span></p>
|
||
<!-- Public domain Emily Dickinson poem. This will work best with something
|
||
short, but that has multiple lines and some repetition. We could search through
|
||
code; that gets a bit meta and possibly confusing... Changes to this are most
|
||
welcome. /Carol -->
|
||
<!-- :D I like it! I'm all for keeping -->
|
||
<!-- Great! /Carol -->
|
||
<p>创建完这个文件之后,修改 <em>src/main.rs</em> 并增加如列表 12-4 所示的打开文件的代码:</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;
|
||
use std::io::prelude::*;
|
||
|
||
fn main() {
|
||
let args: Vec<String> = env::args().collect();
|
||
|
||
let query = &args[1];
|
||
let filename = &args[2];
|
||
|
||
println!("Searching for {}", query);
|
||
println!("In file {}", filename);
|
||
|
||
let mut f = File::open(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);
|
||
}
|
||
</code></pre>
|
||
<p>Listing 12-4: Reading the contents of the file specified by the second argument</p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>首先,增加了更多的<code>use</code>语句来引入标准库中的相关部分:需要<code>std::fs::File</code>来处理文件,而<code>std::io::prelude::*</code>则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,<code>std::io</code>也有其自己的 prelude 来引入处理 I/O 时所需的通用内容。不同于默认的 prelude,必须显式<code>use</code>位于<code>std::io</code>中的 prelude。</p>
|
||
<p>在<code>main</code>中,我们增加了三点内容:第一,通过传递变量<code>filename</code>的值调用<code>File::open</code>函数的值来获取文件的可变句柄。创建了叫做<code>contents</code>的变量并将其设置为一个可变的,空的<code>String</code>。它将会存放之后读取的文件的内容。第三,对文件句柄调用<code>read_to_string</code>并传递<code>contents</code>的可变引用作为参数。</p>
|
||
<p>在这些代码之后,我们再次增加了临时的<code>println!</code>打印出读取文件后<code>contents</code>的值,这样就可以检查目前为止的程序能否工作。</p>
|
||
<p>尝试运行这些代码,随意指定一个字符串作为第一个命令行参数(因为还未实现搜索功能的部分)而将 <em>poem.txt</em> 文件将作为第二个参数:</p>
|
||
<pre><code>$ cargo run the poem.txt
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running `target/debug/greprs the poem.txt`
|
||
Searching for the
|
||
In file poem.txt
|
||
With text:
|
||
I'm nobody! Who are you?
|
||
Are you nobody, too?
|
||
Then there's a pair of us — don't tell!
|
||
They'd banish us, you know.
|
||
|
||
How dreary to be somebody!
|
||
How public, like a frog
|
||
To tell your name the livelong day
|
||
To an admiring bog!
|
||
</code></pre>
|
||
<p>好的!代码读取并打印出了文件的内容。虽然它还有一些瑕疵:<code>main</code>函数有着多个功能,同时也没有处理可能出现的错误。虽然我们的程序还很小,这些瑕疵并不是什么大问题。不过随着程序功能的丰富,将会越来越难以用简单的方法修复他们。在开发程序时,及早开始重构是一个最佳实践,因为重构少量代码时要容易的多,所以让我们现在就开始吧。</p>
|
||
<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>
|
||
<a class="header" href="#测试库的功能" name="测试库的功能"><h2>测试库的功能</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-04-testing-the-librarys-functionality.md">ch12-04-testing-the-librarys-functionality.md</a>
|
||
<br>
|
||
commit b8e4fcbf289b82c12121b282747ce05180afb1fb</p>
|
||
</blockquote>
|
||
<p>现在我们将逻辑提取到了 <em>src/lib.rs</em> 并将所有的参数解析和错误处理留在了 <em>src/main.rs</em> 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。</p>
|
||
<p>在这一部分,我们将遵循测试驱动开发(Test Driven Development, TTD)的模式。这是一个软件开发技术,它遵循如下步骤:</p>
|
||
<ol>
|
||
<li>编写一个会失败的测试,并运行它以确保其因为你期望的原因失败。</li>
|
||
<li>编写或修改刚好足够的代码来使得新的测试通过。</li>
|
||
<li>重构刚刚增加或修改的代码,并确保测试仍然能通过。</li>
|
||
<li>重复上述步骤!</li>
|
||
</ol>
|
||
<p>这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测测试有助于在开发过程中保持高测试覆盖率。</p>
|
||
<p>我们将测试驱动实现<code>greprs</code>实际在文件内容中搜索查询字符串并返回匹配的行列表的部分。我们将在一个叫做<code>search</code>的函数中增加这些功能。</p>
|
||
<a class="header" href="#编写失败测试" name="编写失败测试"><h3>编写失败测试</h3></a>
|
||
<p>首先,去掉 <em>src/lib.rs</em> 和 <em>src/main.rs</em> 中的<code>println!</code>语句,因为不再真的需要他们了。接着我们会像第十一章那样增加一个<code>test</code>模块和一个测试函数。测试函数指定了我们希望<code>search</code>函数拥有的行为:它会获取一个需要查询的字符串和用来查询的文本。列表 12-15 展示了这个测试:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||
# vec![]
|
||
# }
|
||
#
|
||
#[cfg(test)]
|
||
mod test {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn one_result() {
|
||
let query = "duct";
|
||
let contents = "\
|
||
Rust:
|
||
safe, fast, productive.
|
||
Pick three.";
|
||
|
||
assert_eq!(
|
||
vec!["safe, fast, productive."],
|
||
search(query, contents)
|
||
);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-15: Creating a failing test for the <code>search</code>
|
||
function we wish we had</span></p>
|
||
<p>这里选择使用 "duct" 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 "duct"。我们断言<code>search</code>函数的返回值只包含期望的那一行。</p>
|
||
<p>我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的<code>search</code>函数定义,如列表 12-16 所示。一旦有了它,这个测试应该能够编译并因为空 vector 并不匹配一个包含一行<code>"safe, fast, productive."</code>的 vector 而失败。</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||
vec![]
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-16: Defining just enough of the <code>search</code>
|
||
function that our test will compile</span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>注意需要在<code>search</code>的签名中显式定义一个显式生命周期<code>'a</code>并用于<code>contents</code>参数和返回值。回忆一下第十章中生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明返回的 vector 中应该包含引用参数<code>contents</code>(而不是参数<code>query</code>) slice 的字符串 slice。</p>
|
||
<p>换句话说,我们告诉 Rust 函数<code>search</code>返回的数据将与<code>search</code>函数中的参数<code>contents</code>的数据存在的一样久。这是非常重要的!为了使这个引用有效那么<strong>被</strong>slice 引用的数据也需要保持有效;如果编译器认为我们是在创建<code>query</code>而不是<code>contents</code>的字符串 slice,那么安全检查将是不正确的。</p>
|
||
<p>如果尝试不用生命周期编译的话,我们将得到如下错误:</p>
|
||
<pre><code>error[E0106]: missing lifetime specifier
|
||
--> src/lib.rs:5:47
|
||
|
|
||
5 | fn search(query: &str, contents: &str) -> Vec<&str> {
|
||
| ^ expected lifetime parameter
|
||
|
|
||
= help: this function's return type contains a borrowed value, but the
|
||
signature does not say whether it is borrowed from `query` or `contents`
|
||
</code></pre>
|
||
<p>Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数<code>contents</code>包含了所有的文本而且我们希望返回匹配的那部分文本,而我们知道<code>contents</code>是应该要使用生命周期语法来与返回值相关联的参数。</p>
|
||
<p>其他语言中并不需要你在函数签名中将参数与返回值相关联,所以这么做可能仍然感觉有些陌生,随着时间的推移会越来越容易。你可能想要将这个例子与第十章中生命周期语法部分做对比。</p>
|
||
<p>现在试尝试运行测试:</p>
|
||
<pre><code>$ cargo test
|
||
...warnings...
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs
|
||
Running target/debug/deps/greprs-abcabcabc
|
||
|
||
running 1 test
|
||
test test::one_result ... FAILED
|
||
|
||
failures:
|
||
|
||
---- test::one_result stdout ----
|
||
thread 'test::one_result' panicked at 'assertion failed: `(left == right)`
|
||
(left: `["safe, fast, productive."]`, right: `[]`)', src/lib.rs:16
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
|
||
|
||
failures:
|
||
test::one_result
|
||
|
||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
|
||
|
||
error: test failed
|
||
</code></pre>
|
||
<p>好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!</p>
|
||
<a class="header" href="#编写使测试通过的代码" name="编写使测试通过的代码"><h3>编写使测试通过的代码</h3></a>
|
||
<p>目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现<code>search</code>,我们的程序需要遵循如下步骤:</p>
|
||
<ol>
|
||
<li>遍历每一行文本。</li>
|
||
<li>查看这一行是否包含要搜索的字符串。
|
||
<ul>
|
||
<li>如果有,将这一行加入返回列表中。</li>
|
||
<li>如果没有,什么也不做。</li>
|
||
</ul>
|
||
</li>
|
||
<li>返回匹配到的列表</li>
|
||
</ol>
|
||
<p>让我们一步一步的来,从遍历每行开始。</p>
|
||
<a class="header" href="#使用lines方法遍历每一行" name="使用lines方法遍历每一行"><h4>使用<code>lines</code>方法遍历每一行</h4></a>
|
||
<p>Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被命名为<code>lines</code>,它如列表 12-17 这样工作:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||
for line in contents.lines() {
|
||
// do something with line
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-17: Iterating through each line in
|
||
<code>contents</code></span></p>
|
||
<!-- Will add wingdings in libreoffice /Carol -->
|
||
<p><code>lines</code>方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个<code>for</code>循环和迭代器在一个集合的每一项上运行一些代码。</p>
|
||
<!-- so what does `lines` do on its own, if we need to use it in a for loop to
|
||
work? -->
|
||
<!-- It does nothing on its own, it returns an iterator for you to do something
|
||
with. Here, the thing we're doing with it is using it with a `for` loop. I'm
|
||
not sure exactly what you're asking or how to make the text clearer, but I
|
||
added a reference to where we've done this in the book previously. /Carol -->
|
||
<a class="header" href="#用查询字符串搜索每一行" name="用查询字符串搜索每一行"><h4>用查询字符串搜索每一行</h4></a>
|
||
<p>接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个有用的方法叫做<code>contains</code>!如列表 12-18 所示在<code>search</code>函数中加入<code>contains</code>方法:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||
for line in contents.lines() {
|
||
if line.contains(query) {
|
||
// do something with line
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-18: Adding functionality to see if the line
|
||
contains the string in <code>query</code></span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<a class="header" href="#存储匹配的行" name="存储匹配的行"><h4>存储匹配的行</h4></a>
|
||
<p>最后我们需要一个方法来存储包含查询字符串的行。为此可以在<code>for</code>循环之前创建一个可变的 vector 并调用<code>push</code>方法在 vector 中存放一个<code>line</code>。在<code>for</code>循环之后,返回这个 vector,如列表 12-19 所示:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||
let mut results = Vec::new();
|
||
|
||
for line in contents.lines() {
|
||
if line.contains(query) {
|
||
results.push(line);
|
||
}
|
||
}
|
||
|
||
results
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-19: Storing the lines that match so that we
|
||
can return them</span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>现在<code>search</code>函数应该返回只包含<code>query</code>的那些行,而测试应该会通过。让我们运行测试:</p>
|
||
<pre><code>$ cargo test
|
||
running 1 test
|
||
test test::one_result ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
|
||
Running target/debug/greprs-2f55ee8cd1721808
|
||
|
||
running 0 tests
|
||
|
||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||
|
||
Doc-tests greprs
|
||
|
||
running 0 tests
|
||
|
||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>测试通过了,很好,它可以工作了!</p>
|
||
<p>现在测试通过了,我们可以考虑一下重构<code>search</code>的实现并时刻保持测试通过来保持其功能不变的机会了。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。</p>
|
||
<!-- If we aren't going into this here, maybe just keep it focused, there's a
|
||
lot going on here as is -->
|
||
<!-- The reason we mention refactoring here is that it's a key step in the TDD
|
||
method that we were implicitly using before. Now that we've added text to the
|
||
beginning of this section to explicitly mention that we're doing TDD and what
|
||
the steps are, we want to address the "refactor" step. People who have some
|
||
experience with Rust might also look at this example and wonder why we're not
|
||
doing this in a different way, and be concerned that we're not teaching the
|
||
best way possible. This paragraph reassures them that we know what we're doing
|
||
and we're getting to the better way in Chapter 13. /Carol -->
|
||
<a class="header" href="#在run函数中使用search函数" name="在run函数中使用search函数"><h4>在<code>run</code>函数中使用<code>search</code>函数</h4></a>
|
||
<p>现在<code>search</code>函数是可以工作并测试通过了的,我们需要实际在<code>run</code>函数中调用<code>search</code>。需要将<code>config.query</code>值和<code>run</code>从文件中读取的<code>contents</code>传递给<code>search</code>函数。接着<code>run</code>会打印出<code>search</code>返回的每一行:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">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)?;
|
||
|
||
for line in search(&config.query, &contents) {
|
||
println!("{}", line);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
</code></pre>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>这里再一次使用了<code>for</code>循环获取了<code>search</code>返回的每一行,而对每一行运行的代码将他们打印了出来。</p>
|
||
<p>现在整个程序应该可以工作了!让我们试一试,首先使用一个只会在艾米莉·狄金森的诗中返回一行的单词 "frog":</p>
|
||
<pre><code>$ cargo run frog poem.txt
|
||
Compiling greprs v0.1.0 (file:///projects/greprs)
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.38 secs
|
||
Running `target/debug/greprs frog poem.txt`
|
||
How public, like a frog
|
||
</code></pre>
|
||
<p>好的!接下来,像 "the" 这样会匹配多行的单词会怎么样呢:</p>
|
||
<pre><code>$ cargo run the poem.txt
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running `target/debug/greprs the poem.txt`
|
||
Then there's a pair of us — don't tell!
|
||
To tell your name the livelong day
|
||
</code></pre>
|
||
<p>最后,让我们确保搜索一个在诗中哪里都没有的单词时不会得到任何行,比如 "monomorphization":</p>
|
||
<pre><code>$ cargo run monomorphization poem.txt
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running `target/debug/greprs monomorphization poem.txt`
|
||
</code></pre>
|
||
<p>非常好!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。</p>
|
||
<p>现在如果你希望的话请随意移动到第十三章。为了使这个项目章节更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。</p>
|
||
<a class="header" href="#处理环境变量" name="处理环境变量"><h2>处理环境变量</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-05-working-with-environment-variables.md">ch12-05-working-with-environment-variables.md</a>
|
||
<br>
|
||
commit 0db6a0a34886bf02feabcab8b430b5d332a8bdf5</p>
|
||
</blockquote>
|
||
<p>我们将用一个额外的功能来改进我们的工具:一个通过环境变量启用的大小写不敏感搜索的选项。我们将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。</p>
|
||
<a class="header" href="#编写一个大小写不敏感search函数的失败测试" name="编写一个大小写不敏感search函数的失败测试"><h3>编写一个大小写不敏感<code>search</code>函数的失败测试</h3></a>
|
||
<p>首先,增加一个新函数,当设置了环境变量时会调用它。</p>
|
||
<!-- You mean, to turn the environment variable on? I'm not sure what we're
|
||
doing here-->
|
||
<!-- No, I'm not sure how this is unclear. We're adding a new function. We will
|
||
call the new function when the user turns on the environment variable. Can you
|
||
elaborate on what part of the above statement leads to the conclusion that the
|
||
new function is going to turn the environment variable on? Can you suggest a
|
||
rewording that makes the causality direction clearer? /Carol -->
|
||
<p>这里将继续遵循上一部分开始使用的 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从<code>one_result</code>改名为<code>case_sensitive</code>来更清除的表明这两个测试的区别,如列表 12-20 所示:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">#[cfg(test)]
|
||
mod test {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn case_sensitive() {
|
||
let query = "duct";
|
||
let contents = "\
|
||
Rust:
|
||
safe, fast, productive.
|
||
Pick three.
|
||
Duct tape.";
|
||
|
||
assert_eq!(
|
||
vec!["safe, fast, productive."],
|
||
search(query, contents)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn case_insensitive() {
|
||
let query = "rUsT";
|
||
let contents = "\
|
||
Rust:
|
||
safe, fast, productive.
|
||
Pick three.
|
||
Trust me.";
|
||
|
||
assert_eq!(
|
||
vec!["Rust:", "Trust me."],
|
||
search_case_insensitive(query, contents)
|
||
);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p><span class="caption">Listing 12-20: Adding a new failing test for the case
|
||
insensitive function we're about to add</span></p>
|
||
<p>注意我们也改变了老测试中<code>query</code>和<code>contents</code>的值:将查询字符串改变为 "duct",它将会匹配带有单词 productive" 的行。还新增了一个含有文本 "Duct tape" 的行,它有一个大写的 D,这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。</p>
|
||
<p>大小写不敏感搜索的新测试使用带有一些大写字母的 "rUsT" 作为其查询字符串。我们将要增加的<code>search_case_insensitive</code>的期望返回值是包含查询字符串 "rust" 的两行,"Rust:" 包含一个大写的 R 还有"Trust me."包含一个小写的 r。这个测试现在会编译失败因为还没有定义<code>search_case_insensitive</code>函数;请随意增加一个总是返回空 vector 的骨架实现,正如列表 12-16 中<code>search</code>函数那样为了使测试编译并失败时所做的那样。</p>
|
||
<a class="header" href="#实现search_case_insensitive函数" name="实现search_case_insensitive函数"><h3>实现<code>search_case_insensitive</code>函数</h3></a>
|
||
<p><code>search_case_insensitive</code>函数,如列表 12-21 所示,将与<code>search</code>函数基本相同。区别是它会将<code>query</code>变量和每一<code>line</code>都变为小写,这样不管输入参数是大写还是小写,在检查该行是否包含查询字符串时都会是小写。</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||
let query = query.to_lowercase();
|
||
let mut results = Vec::new();
|
||
|
||
for line in contents.lines() {
|
||
if line.to_lowercase().contains(&query) {
|
||
results.push(line);
|
||
}
|
||
}
|
||
|
||
results
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-21: Defining the <code>search_case_insensitive</code>
|
||
function to lowercase both the query and the line before comparing them</span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<!-- why do we lowercase the search string? and why does it need to be a string
|
||
rather than a slice? -->
|
||
<!-- We explained this above, that in order to make the search case
|
||
insensitive, we need to lowercase everything so that searches will always match
|
||
no matter what case either the query or each line uses. It needs to be a
|
||
`String` because we're creating new data, not referencing existing data, when
|
||
we call `to_lowercase`. I've tried to make both of these points clearer, but
|
||
I'm not sure exactly what was unclear about it before, so I'm not sure if I've
|
||
helped. /Carol -->
|
||
<p>首先我们将<code>query</code>字符串转换为小写,并将其储存(覆盖)到同名的变量中。对查询字符串调用<code>to_lowercase</code>是必需的这样不管用户的查询是"rust"、"RUST"、"Rust"或者"rUsT",我们都将其当作"rust"处理并对大小写不敏感。</p>
|
||
<p>注意<code>query</code>现在是一个<code>String</code>而不是字符串 slice,因为调用<code>to_lowercase</code>是在创建新数据,而不是引用现有数据。如果查询字符串是"rUsT",这个字符串 slice 并不包含可供我们使用的小写的 u,所以必需分配一个包含"rust"的新<code>String</code>。因为<code>query</code>现在是一个<code>String</code>,当我们将<code>query</code>作为一个参数传递给<code>contains</code>方法时,需要增加一个 & 因为<code>contains</code>的签名被定义为获取一个字符串 slice。</p>
|
||
<p>接下来在检查每个<code>line</code>是否包含<code>search</code>之前增加了一个<code>to_lowercase</code>调用。这会将"Rust:"变为"rust:"并将"Trust me."变为"trust me."。现在我们将<code>line</code>和<code>query</code>都转换成了小写,这样就可以不管大小写的匹配文件中的文本和用户输入的查询了。</p>
|
||
<p>让我们看看这个实现能否通过测试:</p>
|
||
<pre><code> Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running target/debug/deps/greprs-e58e9b12d35dc861
|
||
|
||
running 2 tests
|
||
test test::case_insensitive ... ok
|
||
test test::case_sensitive ... ok
|
||
|
||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
|
||
|
||
Running target/debug/greprs-8a7faa2662b5030a
|
||
|
||
running 0 tests
|
||
|
||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||
|
||
Doc-tests greprs
|
||
|
||
running 0 tests
|
||
|
||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>好的!现在,让我们在<code>run</code>函数中调用真正的新<code>search_case_insensitive</code>函数。首先,我们将在<code>Config</code>结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索。</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">pub struct Config {
|
||
pub query: String,
|
||
pub filename: String,
|
||
pub case_sensitive: bool,
|
||
}
|
||
</code></pre>
|
||
<!-- Will add ghosting in libreoffice /Carol -->
|
||
<p>这里增加了<code>case_sensitive</code>字符来存放一个布尔值。接着我们需要<code>run</code>函数检查<code>case_sensitive</code>字段的值并使用它来决定是否调用<code>search</code>函数或<code>search_case_insensitive</code>函数,如列表 12-22所示:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># use std::error::Error;
|
||
# use std::fs::File;
|
||
# use std::io::prelude::*;
|
||
#
|
||
# fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||
# vec![]
|
||
# }
|
||
#
|
||
# fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||
# vec![]
|
||
# }
|
||
#
|
||
# struct Config {
|
||
# query: String,
|
||
# filename: String,
|
||
# case_sensitive: bool,
|
||
# }
|
||
#
|
||
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)?;
|
||
|
||
let results = if config.case_sensitive {
|
||
search(&config.query, &contents)
|
||
} else {
|
||
search_case_insensitive(&config.query, &contents)
|
||
};
|
||
|
||
for line in results {
|
||
println!("{}", line);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-22: Calling either <code>search</code> or
|
||
<code>search_case_insensitive</code> based on the value in <code>config.case_sensitive</code></span></p>
|
||
<!-- Will add ghosting in libreoffice /Carol -->
|
||
<p>最后需要实际检查环境变量。处理环境变量的函数位于标准库的<code>env</code>模块中,所以我们需要在 <em>src/lib.rs</em> 的开头增加一个<code>use std::env;</code>行将这个模块引入作用域中。接着在<code>Config::new</code>中使用<code>env</code>模块的<code>var</code>方法检查一个叫做<code>CASE_INSENSITIVE</code>的环境变量,如列表 12-23 所示:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">use std::env;
|
||
# struct Config {
|
||
# query: String,
|
||
# filename: String,
|
||
# case_sensitive: bool,
|
||
# }
|
||
|
||
// ...snip...
|
||
|
||
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();
|
||
|
||
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
|
||
|
||
Ok(Config {
|
||
query: query,
|
||
filename: filename,
|
||
case_sensitive: case_sensitive,
|
||
})
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-23: Checking for an environment variable named
|
||
<code>CASE_INSENSITIVE</code></span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>这里创建了一个新变量<code>case_sensitive</code>。为了设置它的值,需要调用<code>env::var</code>函数并传递我们需要寻找的环境变量名称,<code>CASE_INSENSITIVE</code>。<code>env::var</code>返回一个<code>Result</code>,它在环境变量被设置时返回包含其值的<code>Ok</code>成员,并在环境变量未被设置时返回<code>Err</code>成员。我们使用<code>Result</code>的<code>is_err</code>方法来检查其是否是一个 error(也就是环境变量未被设置的情况),这也就意味着我们<strong>需要</strong>进行一个大小写敏感搜索。如果<code>CASE_INSENSITIVE</code>环境变量被设置为任何值,<code>is_err</code>会返回 false 并将进行大小写不敏感搜索。我们并不关心环境变量所设置的值,只关心它是否被设置了,所以检查<code>is_err</code>而不是<code>unwrap</code>、<code>expect</code>或任何我们已经见过的<code>Result</code>的方法。我们将变量<code>case_sensitive</code>的值传递给<code>Config</code>实例这样<code>run</code>函数可以读取其值并决定是否调用<code>search</code>或者列表 12-22 中实现的<code>search_case_insensitive</code>。</p>
|
||
<p>让我们试一试吧!首先不设置环境变量并使用查询"to"运行程序,这应该会匹配任何全小写的单词"to"的行:</p>
|
||
<pre><code class="language-text">$ cargo run to poem.txt
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running `target/debug/greprs to poem.txt`
|
||
Are you nobody, too?
|
||
How dreary to be somebody!
|
||
</code></pre>
|
||
<p>看起来程序仍然能够工作!现在将<code>CASE_INSENSITIVE</code>设置为 1 并仍使用相同的查询"to",这回应该得到包含可能有大写字母的"to"的行:</p>
|
||
<pre><code>$ CASE_INSENSITIVE=1 cargo run to poem.txt
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running `target/debug/greprs to poem.txt`
|
||
Are you nobody, too?
|
||
How dreary to be somebody!
|
||
To tell your name the livelong day
|
||
To an admiring bog!
|
||
</code></pre>
|
||
<p>好极了,我们也得到了包含"To"的行!现在<code>greprs</code>程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了!</p>
|
||
<p>一些程序允许对相同配置同时使用参数<strong>和</strong>环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试同时通过一个命令行参数来控制大小写不敏感搜索,并在程序遇到矛盾值时决定其优先级。</p>
|
||
<p><code>std::env</code>模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。</p>
|
||
<a class="header" href="#输出到stderr而不是stdout" name="输出到stderr而不是stdout"><h2>输出到<code>stderr</code>而不是<code>stdout</code></h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-06-writing-to-stderr-instead-of-stdout.md">ch12-06-writing-to-stderr-instead-of-stdout.md</a>
|
||
<br>
|
||
commit d09cfb51a239c0ebfc056a64df48fe5f1f96b207</p>
|
||
</blockquote>
|
||
<p>目前为止,我们将所有的输出都<code>println!</code>到了终端。大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这种区别是命令行程序所期望拥有的行为:例如它允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。但是<code>println!</code>只能够打印到标准输出,所以我们必需使用其他方法来打印到标准错误。</p>
|
||
<p>我们可以验证,目前所编写的<code>greprs</code>,所有内容都被打印到了标准输出,包括应该被写入标准错误的错误信息。可以通过故意造成错误来做到这一点,一个发生这种情况的方法是不使用任何参数运行程序。我们准备将标准输出重定向到一个文件中,不过不是标准错误。命令行程序期望以这种方式工作,因为如果输出是错误信息,它应该显示在屏幕上而不是被重定向到文件中。可以看出我们的程序目前并没有满足这个期望,通过使用<code>></code>并指定一个文件名,<em>output.txt</em>,这是期望将标注输出重定向的文件:</p>
|
||
<pre><code>$ cargo run > output.txt
|
||
</code></pre>
|
||
<!-- why do we get an error here? Was that intentional? Does that mean it can't
|
||
print stdout to a file? -->
|
||
<!-- Yes, we're intentionally causing an error here to show that errors are
|
||
currently going to the wrong place. It's showing that `println!` only prints
|
||
to standard out, even when we're printing error messages that should go
|
||
to standard error. /Carol-->
|
||
<p><code>></code>语法告诉 shell 将标准输出的内容写入到 <em>output.txt</em> 文件中而不是打印到屏幕上。我们并没有看到期望的错误信息打印到屏幕上,所以这意味着它一定被写入了文件中。让我们看看 <em>output.txt</em> 包含什么:</p>
|
||
<pre><code>Application error: No search string or filename found
|
||
</code></pre>
|
||
<!-- I don't understand why we send this output to a file to then just say we
|
||
want it to the screen, won't it do that by default? And what has this got to do
|
||
with our use of println? I'm finding the motives here hard to follow -->
|
||
<!-- The point of showing this is to demonstrate that our program is NOT doing
|
||
the correct thing by default, we need to change the places we're calling
|
||
`println!` with error messages to print to standard error instead. When to use
|
||
stdout vs. stderr, and why you might want to redirect stdout but not stderr,
|
||
is something our readers will be familiar with. /Carol -->
|
||
<p>是的,这就是错误信息,这意味着它被打印到了标准输出。这并不是命令行程序所期望拥有的。像这样的错误信息被打印到标准错误,并当以这种方式重定向标注输出时只将运行成功时的数据打印到文件中。让我们像列表 12-23 所示改变错误信息如何被打印的。因为本章早些时候的进行的重构,所有打印错误信息的代码都在一个位置,在<code>main</code>中:</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 std::io::prelude::*;
|
||
|
||
use greprs::Config;
|
||
|
||
fn main() {
|
||
let args: Vec<String> = env::args().collect();
|
||
let mut stderr = std::io::stderr();
|
||
|
||
let config = Config::new(&args).unwrap_or_else(|err| {
|
||
writeln!(
|
||
&mut stderr,
|
||
"Problem parsing arguments: {}",
|
||
err
|
||
).expect("Could not write to stderr");
|
||
process::exit(1);
|
||
});
|
||
|
||
if let Err(e) = greprs::run(config) {
|
||
writeln!(
|
||
&mut stderr,
|
||
"Application error: {}",
|
||
e
|
||
).expect("Could not write to stderr");
|
||
|
||
process::exit(1);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 12-23: Writing error messages to <code>stderr</code> instead
|
||
of <code>stdout</code> using <code>writeln!</code></span></p>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>Rust 并没有类似<code>println!</code>这样的方便写入标准错误的函数。相反,我们使用<code>writeln!</code>宏,它有点像<code>println!</code>,不过它获取一个额外的参数。第一个参数是希望写入内容的位置。可以通过<code>std::io::stderr</code>函数获取一个标准错误的句柄。我们将一个<code>stderr</code>的可变引用传递给<code>writeln!</code>;它需要是可变的因为这样才能写入信息!第二个和第三个参数就像<code>println!</code>的第一个和第二参数:一个格式化字符串和任何需要插入的变量。</p>
|
||
<p>再次用相同方式运行程序,不带任何参数并用<code>></code>重定向<code>stdout</code>:</p>
|
||
<pre><code>$ cargo run > output.txt
|
||
Application error: No search string or filename found
|
||
</code></pre>
|
||
<p>现在我们看到了屏幕上的错误信息,不过<code>output.txt</code>里什么也没有,这也就是命令行程序所期望的行为。</p>
|
||
<p>如果使用不会造成错误的参数再次运行程序,不过仍然将标准输出重定向到一个文件:</p>
|
||
<pre><code>$ cargo run to poem.txt > output.txt
|
||
</code></pre>
|
||
<p>我们并不会在终端看到任何输出,同时<code>output.txt</code>将会包含其结果:</p>
|
||
<p><span class="filename">Filename: output.txt</span></p>
|
||
<pre><code>Are you nobody, too?
|
||
How dreary to be somebody!
|
||
</code></pre>
|
||
<p>这一部分展示了现在我们使用的成功时产生的标准输出和错误时产生的标准错误是恰当的。</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>在这一章中,我们回顾了目前为止的一些主要章节并涉及了如何在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和<code>writeln!</code>宏与<code>stderr</code>,现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。</p>
|
||
<p>接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。</p>
|
||
<a class="header" href="#rust-中的函数式语言功能--迭代器和闭包" name="rust-中的函数式语言功能--迭代器和闭包"><h1>Rust 中的函数式语言功能 —— 迭代器和闭包</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-00-functional-features.md">ch13-00-functional-features.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p>Rust 的设计灵感来源于很多前人的成果。影响 Rust 的其中之一就是函数式编程,在这里函数也是值并可以被用作参数或其他函数的返回值、赋值给变量等等。我们将回避解释函数式编程的具体是什么以及其优缺点,而是突出展示 Rust 中那些类似被认为是函数式的编程语言中的功能。</p>
|
||
<p>更具体的,我们将要涉及:</p>
|
||
<ul>
|
||
<li><strong>闭包</strong>(<em>Closures</em>),一个可以储存在变量里的类似函数的结构</li>
|
||
<li><strong>迭代器</strong>(<em>Iterators</em>),一种处理元素序列的方式。。</li>
|
||
<li>如何使用这些功能来改进上一章的项目</li>
|
||
<li>这些功能的性能。**剧透高能:**他们的速度超乎想象!</li>
|
||
</ul>
|
||
<p>这并不是一个 Rust 受函数式风格影响的完整功能列表:还有模式匹配、枚举和很多其他功能。不过掌握闭包和迭代器则是编写符合语言风格的快速的 Rust 代码的重要一环。</p>
|
||
<a class="header" href="#闭包" name="闭包"><h2>闭包</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-01-closures.md">ch13-01-closures.md</a>
|
||
<br>
|
||
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56</p>
|
||
</blockquote>
|
||
<p>Rust 提供了定义<strong>闭包</strong>的能力,它类似于函数。让我们先不从技术上的定义开始,而是看看闭包语句结构,然后再返回他们的定义。列表 13-1 展示了一个被赋值给变量<code>add_one</code>的小的闭包定义,之后可以用这个变量来调用闭包:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let add_one = |x| x + 1;
|
||
|
||
let five = add_one(4);
|
||
|
||
assert_eq!(5, five);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 13-1: A closure that takes one parameter and adds
|
||
one to it, assigned to the variable <code>add_one</code></span></p>
|
||
<p>闭包的定义位于第一行,展示了闭包获取了一个叫做<code>x</code>的参数。闭包的参数位于竖线之间(<code>|</code>)。</p>
|
||
<p>这是一个很小的闭包,它只包含一个表达式。列表 13-2 展示了一个稍微复杂一点的闭包:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let calculate = |a, b| {
|
||
let mut result = a * 2;
|
||
|
||
result += b;
|
||
|
||
result
|
||
};
|
||
|
||
assert_eq!(7, calculate(2, 3)); // 2 * 2 + 3 == 7
|
||
assert_eq!(13, calculate(4, 5)); // 4 * 2 + 5 == 13
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 13-2: A closure with two parameters and multiple
|
||
expressions in its body</span></p>
|
||
<p>可以通过大括号来定义多于一个表达式的闭包体。</p>
|
||
<p>你会注意到一些闭包不同于<code>fn</code>关键字定义的函数的地方。第一个不同是并不需要声明闭包的参数和返回值的类型。也可以选择加上类型注解;列表 13-3 展示了列表 13-1 中闭包带有参数和返回值类型注解的版本:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let add_one = |x: i32| -> i32 { x + 1 };
|
||
|
||
assert_eq!(2, add_one(1));
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 13-3: A closure definition with optional
|
||
parameter and return value type annotations</span></p>
|
||
<p>在带有类型注解的情况下闭包的语法于函数就更接近了。让我们来更直接的比较一下不同闭包的语法与函数的语法。这里增加了一些空格来对齐相关的部分:</p>
|
||
<pre><code class="language-rust,ignore">fn add_one_v1 (x: i32) -> i32 { x + 1 } // a function
|
||
let add_one_v2 = |x: i32| -> i32 { x + 1 }; // the full syntax for a closure
|
||
let add_one_v3 = |x| { x + 1 }; // a closure eliding types
|
||
let add_one_v4 = |x| x + 1 ; // without braces
|
||
</code></pre>
|
||
<p>定义闭包时不要求类型注解而在定义函数时要求的原因在于函数是显式暴露给用户的接口的一部分,所以为了严格的定义接口确保所有人都同意函数使用和返回的值类型是很重要的。但是闭包并不像函数那样用于暴露接口:他们存在于绑定中并直接被调用。强制标注类型就等于为了很小的优点而显著的降低了工程性(本末倒置)。</p>
|
||
<p>不过闭包的定义确实会推断每一个参数和返回值的类型。例如,如果用<code>i8</code>调用列表 13-1 中没有类型注解的闭包,如果接着用<code>i32</code>调用同一闭包则会得到一个错误:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">let add_one = |x| x + 1;
|
||
|
||
let five = add_one(4i8);
|
||
assert_eq!(5i8, five);
|
||
|
||
let three = add_one(2i32);
|
||
</code></pre>
|
||
<p>编译器给出如下错误:</p>
|
||
<pre><code>error[E0308]: mismatched types
|
||
-->
|
||
|
|
||
7 | let three = add_one(2i32);
|
||
| ^^^^ expected i8, found i32
|
||
</code></pre>
|
||
<p>因为闭包是直接被调用的所以能可靠的推断出其类型,再强制要求标注类型就显得有些冗余了。</p>
|
||
<p>闭包与函数语法不同还有另一个原因是,它与函数有着不同的行为:闭包拥有其<strong>环境(上下文)</strong>。</p>
|
||
<a class="header" href="#闭包可以引用其环境" name="闭包可以引用其环境"><h3>闭包可以引用其环境</h3></a>
|
||
<p>我们知道函数只能使用其作用域内的变量,或者要么是<code>const</code>的要么是被声明为参数的。闭包则可以做的更多:闭包允许使用包含他们的作用域的变量。列表 13-4 是一个在<code>equal_to_x</code>变量中并使用其周围环境中变量<code>x</code>的闭包的例子:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let x = 4;
|
||
|
||
let equal_to_x = |z| z == x;
|
||
|
||
let y = 4;
|
||
|
||
assert!(equal_to_x(y));
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 13-4: Example of a closure that refers to a
|
||
variable in its enclosing scope</span></p>
|
||
<p>这里。即便<code>x</code>并不是<code>equal_to_x</code>的一个参数,<code>equal_to_x</code>闭包也被允许使用它,因为变量<code>x</code>定义于同样定义<code>equal_to_x</code>的作用域中。并不允许在函数中进行与列表 13-4 相同的操作;尝试这么做看看会发生什么:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let x = 4;
|
||
|
||
fn equal_to_x(z: i32) -> bool { z == x }
|
||
|
||
let y = 4;
|
||
|
||
assert!(equal_to_x(y));
|
||
}
|
||
</code></pre>
|
||
<p>我们会得到一个错误:</p>
|
||
<pre><code>error[E0434]: can't capture dynamic environment in a fn item; use the || { ... }
|
||
closure form instead
|
||
-->
|
||
|
|
||
4 | fn equal_to_x(z: i32) -> bool { z == x }
|
||
| ^
|
||
</code></pre>
|
||
<p>编译器甚至提醒我们这只能用于闭包!</p>
|
||
<p>获取他们环境中值的闭包主要用于开始新线程的场景。我们也可以定义以闭包作为参数的函数,通过使用<code>Fn</code> trait。这里是一个函数<code>call_with_one</code>的例子,它的签名有一个闭包参数:</p>
|
||
<pre><code class="language-rust">fn call_with_one<F>(some_closure: F) -> i32
|
||
where F: Fn(i32) -> i32 {
|
||
|
||
some_closure(1)
|
||
}
|
||
|
||
let answer = call_with_one(|x| x + 2);
|
||
|
||
assert_eq!(3, answer);
|
||
</code></pre>
|
||
<p>我们将<code>|x| x + 2</code>传递给了<code>call_with_one</code>,而<code>call_with_one</code>用<code>1</code>作为参数调用了这个闭包。<code>some_closure</code>调用的返回值接着被<code>call_with_one</code>返回。</p>
|
||
<p><code>call_with_one</code>的签名使用了第十章 trait 部分讨论到的<code>where</code>语法。<code>some_closure</code>参数有一个泛型类型<code>F</code>,它在<code>where</code>从句中被定义为拥有<code>Fn(i32) -> i32</code> trait bound。<code>Fn</code> trait 代表了一个闭包,而且可以给<code>Fn</code> trait 增加类型来代表一个特定类型的闭包。在这种情况下,闭包拥有一个<code>i32</code>的参数并返回一个<code>i32</code>,所以泛型的 trait bound 被指定为<code>Fn(i32) -> i32</code>。</p>
|
||
<p>在函数签名中指定闭包要求使用泛型和 trait bound。每一个闭包都有一个独特的类型,所以不能写出闭包的类型而必须使用泛型。</p>
|
||
<p><code>Fn</code>并不是唯一可以指定闭包的 trait bound,事实上有三个:<code>Fn</code>、<code>FnMut</code>和<code>FnOnce</code>。这是在 Rust 中经常见到的三种模式的延续:借用、可变借用和获取所有权。用<code>Fn</code>来指定可能只会借用其环境中值的闭包。用<code>FnMut</code>来指定会修改环境中值的闭包,而如果闭包会获取环境值的所有权则使用<code>FnOnce</code>。大部分情况可以从<code>Fn</code>开始,而编译器会根据调用闭包时会发生什么来告诉你是否需要<code>FnMut</code>或<code>FnOnce</code>。</p>
|
||
<p>为了展示拥有闭包作为参数的函数的应用场景,让我们继续下一主题:迭代器。</p>
|
||
<a class="header" href="#迭代器" name="迭代器"><h2>迭代器</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-02-iterators.md">ch13-02-iterators.md</a>
|
||
<br>
|
||
commit 431116f5c696000b9fd6780e5fde90392cef6812</p>
|
||
</blockquote>
|
||
<p>迭代器是 Rust 中的一个模式,它允许你对一个项的序列进行某些处理。例如。列表 13-5 中对 vecctor 中的每一个数加一:</p>
|
||
<pre><code class="language-rust">let v1 = vec![1, 2, 3];
|
||
|
||
let v2: Vec<i32> = v1.iter().map(|x| x + 1).collect();
|
||
|
||
assert_eq!(v2, [2, 3, 4]);
|
||
</code></pre>
|
||
<p><span class="caption">Listing 13-5: Using an iterator, <code>map</code>, and <code>collect</code> to
|
||
add one to each number in a vector</span></p>
|
||
<!-- Will add wingdings in libreoffice /Carol -->
|
||
<p>vector 的<code>iter</code>方法允许从 vector 创建一个<strong>迭代器</strong>(<em>iterator</em>)。接着迭代器上的<code>map</code>方法调用允许我们处理每一个元素:在这里,我们向<code>map</code>传递了一个对每一个元素<code>x</code>加一的闭包。<code>map</code>是最基本的与比较交互的方法之一,因为依次处理每一个元素是非常有用的!最后<code>collect</code>方法消费了迭代器并将其元素存放到一个新的数据结构中。在这个例子中,因为我们指定<code>v2</code>的类型是<code>Vec<i32></code>,<code>collect</code>将会创建一个<code>i32</code>的 vector。</p>
|
||
<p>像<code>map</code>这样的迭代器方法有时被称为<strong>迭代器适配器</strong>(<em>iterator adaptors</em>),因为他们获取一个迭代器并产生一个新的迭代器。也就是说,<code>map</code>在之前迭代器的基础上通过调用传递给它的闭包来创建了一个新的值序列的迭代器。</p>
|
||
<p>概括一下,这行代码进行了如下工作:</p>
|
||
<ol>
|
||
<li>从 vector 中创建了一个迭代器。</li>
|
||
<li>使用<code>map</code>适配器和一个闭包参数对每一个元素加一。</li>
|
||
<li>使用<code>collect</code>适配器来消费迭代器并生成了一个新的 vector。</li>
|
||
</ol>
|
||
<p>这就是如何产生结果<code>[2, 3, 4]</code>的。如你所见,闭包是使用迭代器的很重要的一部分:他们提供了一个自定义类似<code>map</code>这样的迭代器适配器的行为的方法。</p>
|
||
<a class="header" href="#迭代器是惰性的" name="迭代器是惰性的"><h3>迭代器是惰性的</h3></a>
|
||
<p>在上一部分,你可能已经注意到了一个微妙的用词区别:我们说<code>map</code><strong>适配</strong>(<em>adapts</em>)了一个迭代器,而<code>collect</code><strong>消费</strong>(<em>consumes</em>)了一个迭代器。这是有意为之的。单独的迭代器并不会做任何工作;他们是惰性的。也就是说,像列表 13-5 的代码但是不调用<code>collect</code>的话:</p>
|
||
<pre><code class="language-rust">let v1: Vec<i32> = vec![1, 2, 3];
|
||
|
||
v1.iter().map(|x| x + 1); // without collect
|
||
</code></pre>
|
||
<p>这可以编译,不过会给出一个警告:</p>
|
||
<pre><code>warning: unused result which must be used: iterator adaptors are lazy and do
|
||
nothing unless consumed, #[warn(unused_must_use)] on by default
|
||
--> src/main.rs:4:1
|
||
|
|
||
4 | v1.iter().map(|x| x + 1); // without collect
|
||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
</code></pre>
|
||
<p>这个警告是因为迭代器适配器实际上并不自己进行处理。他们需要一些其他方法来触发迭代器链的计算。我们称之为<strong>消费适配器</strong>(<em>consuming adaptors</em>),而<code>collect</code>就是其中之一。</p>
|
||
<p>那么如何知道迭代器方法是否消费了迭代器呢?还有哪些适配器是可用的呢?为此,让我们看看<code>Iterator</code> trait。</p>
|
||
<a class="header" href="#iterator-trait" name="iterator-trait"><h3><code>Iterator</code> trait</h3></a>
|
||
<p>迭代器都实现了一个标准库中叫做<code>Iterator</code>的 trait。其定义看起来像这样:</p>
|
||
<pre><code class="language-rust">trait Iterator {
|
||
type Item;
|
||
|
||
fn next(&mut self) -> Option<Self::Item>;
|
||
}
|
||
</code></pre>
|
||
<p>这里有一些还未讲到的新语法:<code>type Item</code>和<code>Self::Item</code>定义了这个 trait 的<strong>关联类型</strong>(<em>associated type</em>),第十九章会讲到关联类型。现在所有你需要知道就是这些代码表示<code>Iterator</code> trait 要求你也定义一个<code>Item</code>类型,而这个<code>Item</code>类型用作<code>next</code>方法的返回值。换句话说,<code>Item</code>类型将是迭代器返回的元素的类型。</p>
|
||
<p>让我们使用<code>Iterator</code> trait 来创建一个从一数到五的迭代器<code>Counter</code>。首先,需要创建一个结构体来存放迭代器的当前状态,它有一个<code>u32</code>的字段<code>count</code>。我们也定义了一个<code>new</code>方法,当然这并不是必须的。因为我们希望<code>Counter</code>能从一数到五,所以它总是从零开始:</p>
|
||
<pre><code class="language-rust">struct Counter {
|
||
count: u32,
|
||
}
|
||
|
||
impl Counter {
|
||
fn new() -> Counter {
|
||
Counter { count: 0 }
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>接下来,我们将通过定义<code>next</code>方法来为<code>Counter</code>类型实现<code>Iterator</code> trait。我们希望迭代器的工作方式是对当前状态加一(这就是为什么将<code>count</code>初始化为零,这样迭代器首先就会返回一)。如果<code>count</code>仍然小于六,将返回当前状态,不过如果<code>count</code>大于等于六,迭代器将返回<code>None</code>,如列表 13-6 所示:</p>
|
||
<pre><code class="language-rust"># struct Counter {
|
||
# count: u32,
|
||
# }
|
||
#
|
||
impl Iterator for Counter {
|
||
// Our iterator will produce u32s
|
||
type Item = u32;
|
||
|
||
fn next(&mut self) -> Option<Self::Item> {
|
||
// increment our count. This is why we started at zero.
|
||
self.count += 1;
|
||
|
||
// check to see if we've finished counting or not.
|
||
if self.count < 6 {
|
||
Some(self.count)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 13-6: Implementing the <code>Iterator</code> trait on our
|
||
<code>Counter</code> struct</span></p>
|
||
<!-- I will add wingdings in libreoffice /Carol -->
|
||
<p><code>type Item = u32</code>这一行表明迭代器中<code>Item</code>的关联类型将是<code>u32</code>。同样无需担心关联类型,因为第XX章会涉及他们。</p>
|
||
<p><code>next</code>方法是迭代器的主要接口,它返回一个<code>Option</code>。如果它是<code>Some(value)</code>,相当于可以迭代器中获取另一个值。如果它是<code>None</code>,迭代器就结束了。在<code>next</code>方法中可以进行任何迭代器需要的计算。在这个例子中,我们对当前状态加一,接着检查其是否仍然小于六。如果是,返回<code>Some(self.count)</code>来产生下一个值。如果大于等于六,迭代结束并返回<code>None</code>。</p>
|
||
<p>迭代器 trait 指定当其返回<code>None</code>,就代表迭代结束。该 trait 并不强制任何在<code>next</code>方法返回<code>None</code>后再次调用时必须有的行为。在这个情况下,在第一次返回<code>None</code>后每一次调用<code>next</code>仍然返回<code>None</code>,不过其内部<code>count</code>字段会依次增长到<code>u32</code>的最大值,接着<code>count</code>会溢出(在调试模式会<code>panic!</code>而在发布模式则会折叠从最小值开始)。有些其他的迭代器则选择再次从头开始迭代。如果需要确保迭代器在返回第一个<code>None</code>之后所有的<code>next</code>方法调用都返回<code>None</code>,可以使用<code>fuse</code>方法来创建不同于任何其他的迭代器。</p>
|
||
<p>一旦实现了<code>Iterator</code> trait,我们就有了一个迭代器!可以通过不停的调用<code>Counter</code>结构体的<code>next</code>方法来使用迭代器的功能:</p>
|
||
<pre><code class="language-rust,ignore">let mut counter = Counter::new();
|
||
|
||
let x = counter.next();
|
||
println!("{:?}", x);
|
||
|
||
let x = counter.next();
|
||
println!("{:?}", x);
|
||
|
||
let x = counter.next();
|
||
println!("{:?}", x);
|
||
|
||
let x = counter.next();
|
||
println!("{:?}", x);
|
||
|
||
let x = counter.next();
|
||
println!("{:?}", x);
|
||
|
||
let x = counter.next();
|
||
println!("{:?}", x);
|
||
</code></pre>
|
||
<p>这会一次一行的打印出从<code>Some(1)</code>到<code>Some(5)</code>,之后就全是<code>None</code>。</p>
|
||
<a class="header" href="#各种iterator适配器" name="各种iterator适配器"><h3>各种<code>Iterator</code>适配器</h3></a>
|
||
<p>在列表 13-5 中有一个迭代器并调用了其像<code>map</code>和<code>collect</code>这样的方法。然而在列表 13-6 中,只实现了<code>Counter</code>的<code>next</code>方法。<code>Counter</code>如何才能得到像<code>map</code>和<code>collect</code>这样的方法呢?</p>
|
||
<p>好吧,当讲到<code>Iterator</code>的定义时,我们故意省略一个小的细节。<code>Iterator</code>定义了一系列默认实现,他们会调用<code>next</code>方法。因为<code>next</code>是唯一一个<code>Iterator</code> trait 没有默认实现的方法,一旦实现之后,<code>Iterator</code>的所有其他的适配器就都可用了。这些适配器可不少!</p>
|
||
<p>例如,处于某种原因我们希望获取一个<code>Counter</code>实例产生的值,与另一个<code>Counter</code>实例忽略第一个值之后的值相组合,将每组数相乘,并只保留能被三整除的相乘结果,最后将所有保留的结果相加,我们可以这么做:</p>
|
||
<pre><code class="language-rust"># struct Counter {
|
||
# count: u32,
|
||
# }
|
||
#
|
||
# impl Counter {
|
||
# fn new() -> Counter {
|
||
# Counter { count: 0 }
|
||
# }
|
||
# }
|
||
#
|
||
# impl Iterator for Counter {
|
||
# // Our iterator will produce u32s
|
||
# type Item = u32;
|
||
#
|
||
# fn next(&mut self) -> Option<Self::Item> {
|
||
# // increment our count. This is why we started at zero.
|
||
# self.count += 1;
|
||
#
|
||
# // check to see if we've finished counting or not.
|
||
# if self.count < 6 {
|
||
# Some(self.count)
|
||
# } else {
|
||
# None
|
||
# }
|
||
# }
|
||
# }
|
||
let sum: u32 = Counter::new().zip(Counter::new().skip(1))
|
||
.map(|(a, b)| a * b)
|
||
.filter(|x| x % 3 == 0)
|
||
.sum();
|
||
assert_eq!(18, sum);
|
||
</code></pre>
|
||
<p>注意<code>zip</code>只生成四对值;理论上的第五对值并不会产生,因为<code>zip</code>在任一输入返回<code>None</code>时也会返回<code>None</code>(这个迭代器最多就生成 5)。</p>
|
||
<p>因为实现了<code>Iterator</code>的<code>next</code>方法,所有这些方法调用都是可能的。请查看标准库文档来寻找迭代器可能会用得上的方法。</p>
|
||
<a class="header" href="#改进-io-项目" name="改进-io-项目"><h2>改进 I/O 项目</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-03-improving-our-io-project.md">ch13-03-improving-our-io-project.md</a>
|
||
<br>
|
||
commit 0608e2d0743951d8e628b6e130c6b5744775a783</p>
|
||
</blockquote>
|
||
<p>在我们上一章实现的<code>grep</code> I/O 项目中,其中有一些地方的代码可以使用迭代器来变得更清楚简洁一些。让我们看看迭代器如何能够改进<code>Config::new</code>函数和<code>search</code>函数的实现。</p>
|
||
<a class="header" href="#使用迭代器并去掉clone" name="使用迭代器并去掉clone"><h3>使用迭代器并去掉<code>clone</code></h3></a>
|
||
<p>回到列表 12-8 中,这些代码获取一个<code>String</code> slice 并创建一个<code>Config</code>结构体的实例,它检查参数的数量、索引 slice 中的值、并克隆这些值以便<code>Config</code>可以拥有他们的所有权:</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>当时我们说不必担心这里的<code>clone</code>调用,因为将来会移除他们。好吧,就是现在了!所以,为什么这里需要<code>clone</code>呢?这里的问题是参数<code>args</code>中有一个<code>String</code>元素的 slice,而<code>new</code>函数并不拥有<code>args</code>。为了能够返回<code>Config</code>实例的所有权,我们需要克隆<code>Config</code>中字段<code>query</code>和<code>filename</code>的值,这样<code>Config</code>就能拥有这些值了。</p>
|
||
<p>现在在认识了迭代器之后,我们可以将<code>new</code>函数改为获取一个有所有权的迭代器作为参数。可以使用迭代器来代替之前必要的 slice 长度检查和特定位置的索引。因为我们获取了迭代器的所有权,就不再需要借用所有权的索引操作了,我们可以直接将迭代器中的<code>String</code>值移动到<code>Config</code>中,而不用调用<code>clone</code>来创建一个新的实例。</p>
|
||
<p>首先,让我们看看列表 12-6 中的<code>main</code>函数,将<code>env::args</code>的返回值改为传递给<code>Config::new</code>,而不是调用<code>collect</code>并传递一个 slice:</p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let config = Config::new(env::args());
|
||
// ...snip...
|
||
</code></pre>
|
||
<!-- Will add ghosting in libreoffice /Carol -->
|
||
<p>如果参看标准库中<code>env::args</code>函数的文档,我们会发现它的返回值类型是<code>std::env::Args</code>。所以下一步就是更新<code>Config::new</code>的签名使得参数<code>args</code>拥有<code>std::env::Args</code>类型而不是<code>&[String]</code>:</p>
|
||
<pre><code class="language-rust,ignore">impl Config {
|
||
fn new(args: std::env::Args) -> Result<Config, &'static str> {
|
||
// ...snip...
|
||
</code></pre>
|
||
<!-- Will add ghosting in libreoffice /Carol -->
|
||
<p>之后我们将修复<code>Config::new</code>的函数体。因为标准库文档也表明,<code>std::env::Args</code>实现了<code>Iterator</code> trait,所以我们知道可以调用其<code>next</code>方法!如下就是新的代码:</p>
|
||
<pre><code class="language-rust"># struct Config {
|
||
# query: String,
|
||
# filename: String,
|
||
# }
|
||
#
|
||
impl Config {
|
||
fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
|
||
args.next();
|
||
|
||
let query = match args.next() {
|
||
Some(arg) => arg,
|
||
None => return Err("Didn't get a query string"),
|
||
};
|
||
|
||
let filename = match args.next() {
|
||
Some(arg) => arg,
|
||
None => return Err("Didn't get a file name"),
|
||
};
|
||
|
||
Ok(Config {
|
||
query: query,
|
||
filename: filename,
|
||
})
|
||
}
|
||
}
|
||
</code></pre>
|
||
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
|
||
<p>还记得<code>env::args</code>返回值的第一个值是程序的名称吗。我们希望忽略它,所以首先调用<code>next</code>并不处理其返回值。第二次调用<code>next</code>的返回值应该是希望放入<code>Config</code>中<code>query</code>字段的值。使用<code>match</code>来在<code>next</code>返回<code>Some</code>时提取值,而在因为没有足够的参数(这会造成<code>next</code>调用返回<code>None</code>)而提早返回<code>Err</code>值。</p>
|
||
<p>对<code>filename</code>值也进行相同处理。稍微有些可惜的是<code>query</code>和<code>filename</code>的<code>match</code>表达式是如此的相似。如果可以对<code>next</code>返回的<code>Option</code>使用<code>?</code>就好了,不过目前<code>?</code>只能用于<code>Result</code>值。即便我们可以像<code>Result</code>一样对<code>Option</code>使用<code>?</code>,得到的值也是借用的,而我们希望能够将迭代器中的<code>String</code>移动到<code>Config</code>中。</p>
|
||
<a class="header" href="#使用迭代器适配器来使代码更简明" name="使用迭代器适配器来使代码更简明"><h3>使用迭代器适配器来使代码更简明</h3></a>
|
||
<p>另一部分可以利用迭代器的代码位于列表 12-15 中实现的<code>search</code>函数中:</p>
|
||
<!-- We hadn't had a listing number for this code sample when we submitted
|
||
chapter 12; we'll fix the listing numbers in that chapter after you've
|
||
reviewed it. /Carol -->
|
||
<pre><code class="language-rust">fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||
let mut results = Vec::new();
|
||
|
||
for line in contents.lines() {
|
||
if line.contains(query) {
|
||
results.push(line);
|
||
}
|
||
}
|
||
|
||
results
|
||
}
|
||
</code></pre>
|
||
<p>我们可以用一种更简短的方式来编写这些代码,并避免使用了一个作为可变中间值的<code>results</code> vector,像这样使用迭代器适配器方法来实现:</p>
|
||
<pre><code class="language-rust">fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||
contents.lines()
|
||
.filter(|line| line.contains(query))
|
||
.collect()
|
||
}
|
||
</code></pre>
|
||
<p>这里使用了<code>filter</code>适配器来只保留<code>line.contains(query)</code>为真的那些行。接着使用<code>collect</code>将他们放入另一个 vector 中。这就简单多了!</p>
|
||
<p>也可以对列表 12-16 中定义的<code>search_case_insensitive</code>函数使用如下同样的技术:</p>
|
||
<!-- Similarly, the code snippet that will be 12-16 didn't have a listing
|
||
number when we sent you chapter 12, we will fix it. /Carol -->
|
||
<pre><code class="language-rust">fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||
let query = query.to_lowercase();
|
||
|
||
contents.lines()
|
||
.filter(|line| {
|
||
line.to_lowercase().contains(&query)
|
||
}).collect()
|
||
}
|
||
</code></pre>
|
||
<p>看起来还不坏!那么到底该用哪种风格呢?大部分 Rust 程序员倾向于使用迭代器风格。开始这有点难以理解,不过一旦你对不同迭代器的工作方式有了直觉上的理解之后,他们将更加容易理解。相比使用很多看起来大同小异的循环并创建一个 vector,抽象出这些老生常谈的代码将使得我们更容易看清代码所特有的概念,比如迭代器中用于过滤每个元素的条件。</p>
|
||
<p>不过他们真的完全等同吗?当然更底层的循环会更快一些。让我们聊聊性能吧。</p>
|
||
<a class="header" href="#性能" name="性能"><h2>性能</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-04-performance.md">ch13-04-performance.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p>哪一个版本的<code>grep</code>函数会更快一些呢:是直接使用<code>for</code>循环的版本还是使用迭代器的版本呢?我们将运行一个性能测试,通过将阿瑟·柯南·道尔的“福尔摩斯探案集”的全部内容加载进<code>String</code>并寻找其中的单词 "the"。如下是<code>for</code>循环版本和迭代器版本的 grep 函数的性能测试结果:</p>
|
||
<pre><code>test bench_grep_for ... bench: 19,620,300 ns/iter (+/- 915,700)
|
||
test bench_grep_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
|
||
</code></pre>
|
||
<p>结果迭代器版本还要稍微快一点!这里我们将不会查看性能测试的代码,我们的目的并不是为了证明他们是完全等同的,而是得出一个怎样比较这两种实现方式的基本思路。对于<strong>真正</strong>的性能测试,将会检查不同长度的文本、不同的搜索单词、不同长度的单词和所有其他的可变情况。这里所要表达的是:迭代器,作为一个高级的抽象,被编译成了与手写的底层代码大体一致性能代码。迭代器是 Rust 的<strong>零成本抽象</strong>(<em>zero-cost abstractions</em>)之一,它意味着抽象并不会强加运行时开销,它与本贾尼·斯特劳斯特卢普,C++ 的设计和实现者所定义的<strong>零开销</strong>(<em>zero-overhead</em>)如出一辙:</p>
|
||
<blockquote>
|
||
<p>In general, C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.</p>
|
||
<ul>
|
||
<li>Bjarne Stroustrup "Foundations of C++"</li>
|
||
</ul>
|
||
<p>从整体来说,C++ 的实现遵循了零开销原则:你不需要的,无需为他们买单。更有甚者的是:你需要的时候,也不可能找到其他更好的代码了。</p>
|
||
<ul>
|
||
<li>本贾尼·斯特劳斯特卢普 "Foundations of C++"</li>
|
||
</ul>
|
||
</blockquote>
|
||
<p>作为另一个例子,这里有一些来自于音频解码器的代码。这些代码使用迭代器链来对作用域中的三个变量进行了某种数学计算:一个叫<code>buffer</code>的数据 slice、一个有12个元素的数组<code>coefficients</code>、和一个代表移位位数的<code>qlp_shift</code>。例子中声明了这些变量但并没有提供任何值;虽然这些代码在其上下文之外没有什么意义,不过仍是一个简洁的现实中的例子,来展示 Rust 如何将高级概念转换为底层代码:</p>
|
||
<pre><code class="language-rust,ignore">let buffer: &mut [i32];
|
||
let coefficients: [i64; 12];
|
||
let qlp_shift: i16;
|
||
|
||
for i in 12..buffer.len() {
|
||
let prediction = coefficients.iter()
|
||
.zip(&buffer[i - 12..i])
|
||
.map(|(&c, &s)| c * s as i64)
|
||
.sum::<i64>() >> qlp_shift;
|
||
let delta = buffer[i];
|
||
buffer[i] = prediction as i32 + delta;
|
||
}
|
||
</code></pre>
|
||
<p>为了计算<code>prediction</code>的值,这些代码遍历了<code>coefficients</code>中的 12 个值,使用<code>zip</code>方法将系数与<code>buffer</code>的前 12 个值组合在一起。接着将每一对值相乘,再将所有结果相加,然后将总和右移<code>qlp_shift</code>位。</p>
|
||
<p>像音频解码器这样的程序通常非常看重计算的性能。这里,我们创建了一个迭代器,使用了两个适配器,接着消费了其值。Rust 代码将会被编译为什么样的汇编代码呢?好吧,在编写本书的这个时候,它被编译成与手写的相同的汇编代码。遍历<code>coefficients</code>的值完全用不到循环:Rust 知道这里会迭代 12 次,所以它“展开”了循环。所有的系数都被储存在了寄存器中(这意味着访问他们非常快)。也没有数组访问边界检查。这是极端有效率的。</p>
|
||
<p>现在知道这些了,请放心大胆的使用迭代器和闭包吧!他们使得代码看起来更高级,但并不为此引入运行时性能损失。</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>闭包和迭代器是 Rust 受函数式编程语言观念所启发的功能。他们对 Rust 直白的表达高级概念的能力有很大贡献。闭包和迭代器的实现,以及 Rust 的零成本抽象,也使得运行时性能不受影响。</p>
|
||
<p>现在我们改进了我们 I/O 项目的(代码)表现力,让我们看一看更多<code>cargo</code>的功能,他们是如何将项目准备好分享给世界的。</p>
|
||
<a class="header" href="#更多关于-cargo-和-cratesio" name="更多关于-cargo-和-cratesio"><h1>更多关于 Cargo 和 Crates.io</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-00-more-about-cargo.md">ch14-00-more-about-cargo.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p>目前为止本书已经使用过一些 Cargo 的功能了,不过这是最基本的那些。我们使用 Cargo 构建、运行和测试代码,不过它还可以做更多。现在就让我们来了解这些其他功能。Cargo 所能做的比本章所涉及的内容还要多;作为一个完整的参考,请查看文档。</p>
|
||
<p>我们将要涉及到:</p>
|
||
<ul>
|
||
<li>使用发布配置来自定义构建</li>
|
||
<li>将库发布到 crates.io</li>
|
||
<li>使用工作空间来组织更大的项目</li>
|
||
<li>从 crates.io 安装二进制文件</li>
|
||
<li>使用自定义的命令来扩展 Cargo</li>
|
||
</ul>
|
||
<a class="header" href="#发布配置" name="发布配置"><h2>发布配置</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-01-release-profiles.md">ch14-01-release-profiles.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p>Cargo 支持一个叫做<strong>发布配置</strong>(<em>release profiles</em>)的概念。这些配置控制各种代码编译参数而且彼此相互独立。在构建的输出中你已经见过了这个功能的影子:</p>
|
||
<pre><code>$ cargo build
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
$ cargo build --release
|
||
Finished release [optimized] target(s) in 0.0 secs
|
||
</code></pre>
|
||
<p>这里的 "debug" 和 "release" 提示表明编译器在使用不同的配置。Cargo 支持四种配置:</p>
|
||
<ul>
|
||
<li><code>dev</code>:用于<code>cargo build</code></li>
|
||
<li><code>release</code>:用于<code>cargo build --release</code></li>
|
||
<li><code>test</code>:用于<code>cargo test</code></li>
|
||
<li><code>doc</code>:<code>cargo doc</code></li>
|
||
</ul>
|
||
<p>可以通过自定义<code>Cargo.toml</code>文件中的<code>[profile.*]</code>部分来调整这些配置的编译器参数。例如,这里是<code>dev</code>和<code>release</code>配置的默认参数:</p>
|
||
<pre><code class="language-toml">[profile.dev]
|
||
opt-level = 0
|
||
|
||
[profile.release]
|
||
opt-level = 3
|
||
</code></pre>
|
||
<p><code>opt-level</code>设置控制 Rust 会进行何种程度的优化。这个配置的值从 0 到 3。越高的优化级别需要更多的时间。当开发时经常需要编译,你通常希望能在牺牲一些代码性能的情况下编译得快一些。当准备发布时,一次花费更多时间编译来换取每次都要运行的更快的编译结果将更好一些。</p>
|
||
<p>可以在<code>Cargo.toml</code>中覆盖这些默认设置。例如,如果你想在开发时开启一级优化:</p>
|
||
<pre><code class="language-toml">[profile.dev]
|
||
opt-level = 1
|
||
</code></pre>
|
||
<p>这将覆盖默认的设置<code>0</code>,而现在开发构建将获得更多的优化。虽然不如发布构建,但也多少有一些。</p>
|
||
<p>对于每个配置的设置和其默认值的完整列表,请查看<a href="http://doc.crates.io/">Cargo 的 文档</a>。</p>
|
||
<a class="header" href="#将-crate-发布到-cratesio" name="将-crate-发布到-cratesio"><h2>将 crate 发布到 Crates.io</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-02-publishing-to-crates-io.md">ch14-02-publishing-to-crates-io.md</a>
|
||
<br>
|
||
commit f2eef19b3a39ee68dd363db2fcba173491ba9dc4</p>
|
||
</blockquote>
|
||
<p>我们曾经在项目中增加 crates.io 上的 crate 作为依赖。也可以选择将代码分享给其他人。Crates.io 用来分发包的源代码,所以它主要用于分发开源代码。</p>
|
||
<p>Rust 和 Cargo 有一些帮助人们找到和使用你发布的包的功能。我们将介绍这些功能,接着讲到如何发布一个包。</p>
|
||
<a class="header" href="#文档注释" name="文档注释"><h3>文档注释</h3></a>
|
||
<p>在第三章中,我们见到了以<code>//</code>开头的注释,Rust 还有第二种注释:<strong>文档注释</strong>(<em>documentation comment</em>)。注释固然对阅读代码的人有帮助,也可以生成 HTML 代码来显式公有 API 的文档注释,这有助于那些对如何<strong>使用</strong> crate 有兴趣而不关心如何<strong>实现</strong>的人。注意只有库 crate 能生成文档,因为二进制 crate 并没有人们需要知道如何使用的公有 API。</p>
|
||
<p>文档注释使用<code>///</code>而不是<code>//</code>并支持 Markdown 注解。他们就位于需要文档的项的之前。如下是一个<code>add_one</code>函数的文档注释:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">/// Adds one to the number given.
|
||
///
|
||
/// # Examples
|
||
///
|
||
/// ```
|
||
/// let five = 5;
|
||
///
|
||
/// assert_eq!(6, add_one(five));
|
||
/// ```
|
||
pub fn add_one(x: i32) -> i32 {
|
||
x + 1
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 14-1: A documentation comment for a
|
||
function</span></p>
|
||
<p><code>cargo doc</code>运行一个由 Rust 分发的工具,<code>rustdoc</code>,来为这些注释生成 HTML 文档。可以运行<code>cargo doc --open</code>在本地尝试一下,这会构建当前状态的文档(以及 crate 的依赖)并在浏览器中打开。导航到<code>add_one</code>函数将会发现文档注释是如何渲染的。</p>
|
||
<p>在文档注释中增加示例代码块是一个清楚的表明如何使用库的方法。这么做还有一个额外的好处:<code>cargo test</code>也会像测试那样运行文档中的示例代码!没有什么比有例子的文档更好的了!也没有什么比不能正常工作的例子更糟的了,因为代码在编写文档时已经改变。尝试<code>cargo test</code>运行列表 14-1 中<code>add_one</code>函数的文档;将会看到如下测试结果:</p>
|
||
<pre><code class="language-test"> Doc-tests add-one
|
||
|
||
running 1 test
|
||
test add_one_0 ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>尝试改变示例或函数并观察<code>cargo test</code>会捕获不再能运行的例子!</p>
|
||
<p>还有另一种风格的文档注释,<code>//!</code>,用于注释包含项的结构(例如:crate、模块或函数),而不是其之后的项。这通常用在 crate 的根(lib.rs)或模块的根(mod.rs)来分别编写 crate 或模块整体的文档。如下是包含整个标准库的<code>libstd</code>模块的文档:</p>
|
||
<pre><code>//! # The Rust Standard Library
|
||
//!
|
||
//! The Rust Standard Library provides the essential runtime
|
||
//! functionality for building portable Rust software.
|
||
</code></pre>
|
||
<a class="header" href="#使用pub-use来导出合适的公有-api" name="使用pub-use来导出合适的公有-api"><h3>使用<code>pub use</code>来导出合适的公有 API</h3></a>
|
||
<p>第七章介绍了如何使用<code>mod</code>关键字来将代码组织进模块中,如何使用<code>pub</code>关键字将项变为公有,和如何使用<code>use</code>关键字将项引入作用域。当发布 crate 给并不熟悉其使用的库的实现的人时,就值得花时间考虑 crate 的结构对于开发和对于依赖 crate 的人来说是否同样有用。如果结构对于供其他库使用来说并不方便,也无需重新安排内部组织:可以选择使用<code>pub use</code>来重新导出一个不同的公有结构。</p>
|
||
<p>例如列表 14-2 中,我们创建了一个库<code>art</code>,其包含一个<code>kinds</code>模块,模块中包含枚举<code>Color</code>和包含函数<code>mix</code>的模块<code>utils</code>:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">//! # Art
|
||
//!
|
||
//! A library for modeling artistic concepts.
|
||
|
||
pub mod kinds {
|
||
/// The primary colors according to the RYB color model.
|
||
pub enum PrimaryColor {
|
||
Red,
|
||
Yellow,
|
||
Blue,
|
||
}
|
||
|
||
/// The secondary colors according to the RYB color model.
|
||
pub enum SecondaryColor {
|
||
Orange,
|
||
Green,
|
||
Purple,
|
||
}
|
||
}
|
||
|
||
pub mod utils {
|
||
use kinds::*;
|
||
|
||
/// Combines two primary colors in equal amounts to create
|
||
/// a secondary color.
|
||
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
|
||
// ...snip...
|
||
# SecondaryColor::Green
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 14-2: An <code>art</code> library with items organized into
|
||
<code>kinds</code> and <code>utils</code> modules</span></p>
|
||
<p>为了使用这个库,列表 14-3 中另一个 crate 中使用了<code>use</code>语句:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">extern crate art;
|
||
|
||
use art::kinds::PrimaryColor;
|
||
use art::utils::mix;
|
||
|
||
fn main() {
|
||
let red = PrimaryColor::Red;
|
||
let yellow = PrimaryColor::Yellow;
|
||
mix(red, yellow);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 14-3: A program using the <code>art</code> crate's items
|
||
with its internal structure exported</span></p>
|
||
<p>库的用户并不需要知道<code>PrimaryColor</code>和<code>SecondaryColor</code>位于<code>kinds</code>模块中和<code>mix</code>位于<code>utils</code>模块中;这些结构对于内部组织是有帮助的,不过对于外部的观点来说没有什么意义。</p>
|
||
<p>为此,可以选择在列表 14-2 中增加如下<code>pub use</code>语句来将这些类型重新导出到顶级结构,如列表 14-4 所示:</p>
|
||
<p><span class="filename">Filename: src/lib.rs</span></p>
|
||
<pre><code class="language-rust,ignore">//! # Art
|
||
//!
|
||
//! A library for modeling artistic concepts.
|
||
|
||
pub use kinds::PrimaryColor;
|
||
pub use kinds::SecondaryColor;
|
||
pub use utils::mix;
|
||
|
||
pub mod kinds {
|
||
// ...snip...
|
||
</code></pre>
|
||
<p><span class="caption">Listing 14-4: Adding <code>pub use</code> statements to re-export
|
||
items</span></p>
|
||
<!-- Will add ghosting in libreoffice /Carol -->
|
||
<p>重导出的项将会被连接和排列在 crate API 文档的头版。<code>art</code> crate 的用户仍然可以像列表 14-3 那样使用内部结构,或者使用列表 14-4 中更方便的结构,如列表 14-5 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">extern crate art;
|
||
|
||
use art::PrimaryColor;
|
||
use art::mix;
|
||
|
||
fn main() {
|
||
// ...snip...
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 14-5: Using the re-exported items from the <code>art</code>
|
||
crate</span></p>
|
||
<!-- Will add ghosting in libreoffice /Carol -->
|
||
<p>创建一个有用的公有 API 结构更像一种艺术而不是科学。选择<code>pub use</code>提供了如何向用户暴露 crate 内部结构的灵活性。观察一些你所安装的 crate 的代码来看看其内部结构是否不同于公有 API。</p>
|
||
<a class="header" href="#在第一次发布之前" name="在第一次发布之前"><h3>在第一次发布之前</h3></a>
|
||
<p>在能够发布任何 crate 之前,你需要在<a href="https://crates.io">crates.io</a>上注册一个账号并获取一个 API token。为此,<a href="https://crates.io">访问其官网</a>并使用 GitHub 账号登陆。目前 GitHub 账号是必须的,不过将来网站可能会支持其他创建账号的方法。一旦登陆之后,查看<a href="https://crates.io/me">Account Settings</a>页面并使用其中指定的 API key 运行<code>cargo login</code>命令,这看起来像这样:</p>
|
||
<pre><code>$ cargo login abcdefghijklmnopqrstuvwxyz012345
|
||
</code></pre>
|
||
<p>这个命令会通知 Cargo 你的 API token 并将其储存在本地的 <em>~/.cargo/config</em> 文件中。注意这个 token 是一个<strong>秘密</strong>(<strong>secret</strong>)并不应该与其他人共享。如果因为任何原因与他人共享了这个信息,应该立即重新生成这个 token。</p>
|
||
<a class="header" href="#在发布新-crate-之前" name="在发布新-crate-之前"><h3>在发布新 crate 之前</h3></a>
|
||
<p>首先,crate 必须有一个位移的名称。虽然在本地开发 crate 时,可以使用任何你喜欢的名字,不过<a href="https://crates.io">crates.io</a>上的 crate 名称遵守先到先得的原则分配。一旦一个 crate 名被使用,就不能被另一个 crate 所使用,所以请确认你喜欢的名字在网站上是可用的。</p>
|
||
<p>如果尝试发布由<code>cargo new</code>生成的 crate,会出现一个警告接着是一个错误:</p>
|
||
<pre><code>$ cargo publish
|
||
Updating registry `https://github.com/rust-lang/crates.io-index`
|
||
warning: manifest has no description, license, license-file, documentation,
|
||
homepage or repository.
|
||
...snip...
|
||
error: api errors: missing or empty metadata fields: description, license.
|
||
Please see http://doc.crates.io/manifest.html#package-metadata for how to
|
||
upload metadata
|
||
</code></pre>
|
||
<p>我们可以在包的 <em>Cargo.toml</em> 文件中包含更多的信息。其中一些字段是可选的,不过描述和 license 是发布所必须的,因为这样人们才能知道 crate 是干什么的已经在什么样的条款下可以使用他们。</p>
|
||
<p>描述连同 crate 一起出现在搜索结果和 crate 页面中。描述通常是一两句话。<code>license</code>字段获取一个 license 标识符值,其可能的值由 Linux 基金会的<a href="http://spdx.org/licenses/">Software Package Data Exchange (SPDX)</a>指定。如果你想要使用一个不存在于SPDX的 license,则不使用<code>license</code>值,使用<code>license-file</code>来指定项目中包含你想要使用的 license 的文本的文件名。</p>
|
||
<p>关于项目所适用的 license 的指导超出了本书的范畴。很多 Rust 社区成员选择与 Rust 自身相同的 license,它是一个双许可的<code>MIT/Apache-2.0</code>,这表明可以通过斜杠来分隔指定多个 license。所以一个准备好发布的项目的 <em>Cargo.toml</em> 文件看起来像这样:</p>
|
||
<pre><code class="language-toml">[package]
|
||
name = "guessing_game"
|
||
version = "0.1.0"
|
||
authors = ["Your Name <you@example.com>"]
|
||
description = "A fun game where you guess what number the computer has chosen."
|
||
license = "MIT/Apache-2.0"
|
||
|
||
[dependencies]
|
||
</code></pre>
|
||
<p>请查看<a href="http://doc.crates.io/manifest.html#package-metadata">crates.io 的文档</a>中关于其他可以指定元数据的内容,他们可以帮助你的 crate 更容易被发现和使用!</p>
|
||
<a class="header" href="#发布到-cratesio" name="发布到-cratesio"><h3>发布到 Crates.io</h3></a>
|
||
<p>现在我们创建了一个账号,保存了 API token,为 crate 选择了一个名字,并指定了所需的元数据,我们已经准备好发布了!发布 crate 是一个特定版本的 crate 被上传并托管在 crates.io 的过程。</p>
|
||
<p>发布 crate 请多加小心,因为发布是<strong>永久性的</strong>。对应版本不能被覆盖,其代码也不可能被删除。然而,可以被发布的版本号却没有限制。</p>
|
||
<p>让我们运行<code>cargo publish</code>命令,这次它应该会成功因为已经指定了必须的元数据:</p>
|
||
<pre><code>$ cargo publish
|
||
Updating registry `https://github.com/rust-lang/crates.io-index`
|
||
Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
Compiling guessing_game v0.1.0
|
||
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.19 secs
|
||
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
</code></pre>
|
||
<p>恭喜!你现在向 Rust 社区分享了代码,而且任何人都可以轻松的将你的 crate 加入他们项目的依赖。</p>
|
||
<a class="header" href="#发布已有-crate-的新版本" name="发布已有-crate-的新版本"><h3>发布已有 crate 的新版本</h3></a>
|
||
<p>当你修改了 crate 并准备好发布新版本时,改变 <em>Cargo.toml</em> 中<code>version</code>所指定的值。请使用<a href="http://semver.org/">语义化版本规则</a>来根据修改的类型决定下一个版本呢号。接着运行<code>cargo publish</code>来上传新版本。</p>
|
||
<a class="header" href="#使用cargo-yank从-cratesio-删除版本" name="使用cargo-yank从-cratesio-删除版本"><h3>使用<code>cargo yank</code>从 Crates.io 删除版本</h3></a>
|
||
<p>发布版本时可能会出现意外,因为这样那样的原因导致功能被破坏,比如语法错误或忘记引入某些文件。对于这种情况,Cargo 支持 <em>yanking</em> 一个版本。</p>
|
||
<p>标记一个版本的 crate 为 yank 意味着没有项目能够再开始依赖这个版本,不过现存的已经依赖这个版本的项目仍然能够下载和依赖这个版本的内容。crates.io 的一个主要目的是作为一个代码的永久档案库,这样能够保证所有的项目都能继续构建,而允许删除一个版本违反了这个目标。本质上来说,yank 意味着所有带有 <em>Cargo.lock</em> 的项目并不会被破坏,同时任何未来生成的 <em>Cargo.lock</em> 将不能使用被撤回的版本。</p>
|
||
<p>yank 并<strong>不</strong>意味着删除了任何代码。例如 yank 功能不打算删除意外上传的 secret。如果这发生了,请立刻重置这些 secret。</p>
|
||
<p>为了 yank 一个版本的 crate,运行<code>cargo yank</code>并指定需要 yank 的版本:</p>
|
||
<pre><code>$ cargo yank --vers 1.0.1
|
||
</code></pre>
|
||
<p>也可以撤销 yank,并允许项目开始依赖这个版本,通过在命令中加上<code>--undo</code>:</p>
|
||
<pre><code>$ cargo yank --vers 1.0.1 --undo
|
||
</code></pre>
|
||
<a class="header" href="#cargo-工作空间" name="cargo-工作空间"><h2>Cargo 工作空间</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-03-cargo-workspaces.md">ch14-03-cargo-workspaces.md</a>
|
||
<br>
|
||
commit d945f6d4046f4fc3c09326213100492790aebb45</p>
|
||
</blockquote>
|
||
<p>第十二章中,我们构建一个包含二进制 crate 和库 crate 的包。不过如果库 crate 继续变得更大而我们想要进一步将包拆分为多个库 crate 呢?随着包增长,拆分出其主要组件将是非常有帮助的。对于这种情况,Cargo 提供了一个叫<strong>工作空间</strong>(<em>workspaces</em>)的功能,它可以帮助我们管理多个相关的并行开发的包。</p>
|
||
<p><strong>工作空间</strong>是一系列的包都共享同样的 <em>Cargo.lock</em> 和输出目录。让我们使用工作空间创建一个项目,这是我们熟悉的所以就可以关注工作空间的结构了。这里有一个二进制项目它使用了两个库:一个会提供<code>add_one</code>方法而第二个会提供<code>add_two</code>方法。让我们为这个二进制项目创建一个新 crate 作为开始:</p>
|
||
<pre><code>$ cargo new --bin adder
|
||
Created binary (application) `adder` project
|
||
$ cd adder
|
||
</code></pre>
|
||
<p>需要修改二进制包的 <em>Cargo.toml</em> 来告诉 Cargo 包<code>adder</code>是一个工作空间。再文件末尾增加如下:</p>
|
||
<pre><code class="language-toml">[workspace]
|
||
</code></pre>
|
||
<p>类似于很多 Cargo 的功能,工作空间支持配置惯例:只要遵循这些惯例就无需再增加任何配置了。这个惯例是任何作为子目录依赖的 crate 将是工作空间的一部分。让我们像这样在 <em>Cargo.toml</em> 中的<code>[dependencies]</code>增加一个<code>adder</code> crate 的路径依赖:</p>
|
||
<pre><code class="language-toml">[dependencies]
|
||
add-one = { path = "add-one" }
|
||
</code></pre>
|
||
<p>如果增加依赖但没有指定<code>path</code>,这将是一个不位于工作空间的正常的依赖。</p>
|
||
<p>接下来,在<code>adder</code>目录中生成<code>add-one</code> crate:</p>
|
||
<pre><code>$ cargo new add-one
|
||
Created library `add-one` project
|
||
</code></pre>
|
||
<p>现在<code>adder</code>目录应该有如下目录和文件:</p>
|
||
<pre><code>├── Cargo.toml
|
||
├── add-one
|
||
│ ├── Cargo.toml
|
||
│ └── src
|
||
│ └── lib.rs
|
||
└── src
|
||
└── main.rs
|
||
</code></pre>
|
||
<p>在 <em>add-one/src/lib.rs</em> 中增加<code>add_one</code>函数的实现:</p>
|
||
<p><span class="filename">Filename: add-one/src/lib.rs</span></p>
|
||
<pre><code class="language-rust">pub fn add_one(x: i32) -> i32 {
|
||
x + 1
|
||
}
|
||
</code></pre>
|
||
<p>打开<code>adder</code>的 <em>src/main.rs</em> 并增加一行<code>extern crate</code>将新的<code>add-one</code>库引入作用域,并修改<code>main</code>函数来使用<code>add_one</code>函数:</p>
|
||
<pre><code class="language-rust,ignore">extern crate add_one;
|
||
|
||
fn main() {
|
||
let num = 10;
|
||
println!("Hello, world! {} plus one is {}!", num, add_one::add_one(num));
|
||
}
|
||
</code></pre>
|
||
<p>让我们构建一下!</p>
|
||
<pre><code>$ cargo build
|
||
Compiling add-one v0.1.0 (file:///projects/adder/add-one)
|
||
Compiling adder v0.1.0 (file:///projects/adder)
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.68 secs
|
||
</code></pre>
|
||
<p>注意在 <em>adder</em> 目录运行<code>cargo build</code>会构建这个 crate 和 <em>adder/add-one</em> 中的<code>add-one</code> crate,不过只创建一个 <em>Cargo.lock</em> 和一个 <em>target</em> 目录,他们都位于 <em>adder</em> 目录。试试你能否用相同的方式增加<code>add-two</code> crate。</p>
|
||
<p>假如我们想要在<code>add-one</code> crate 中使用<code>rand</code> crate。一如既往在<code>Cargo.toml</code>的<code>[dependencies]</code>部分增加这个 crate:</p>
|
||
<p><span class="filename">Filename: add-one/Cargo.toml</span></p>
|
||
<pre><code class="language-toml">[dependencies]
|
||
|
||
rand = "0.3.14"
|
||
</code></pre>
|
||
<p>如果在 <em>add-one/src/lib.rs</em> 中加上<code>extern crate rand;</code>后再运行<code>cargo build</code>,则会编译成功:</p>
|
||
<pre><code>$ cargo build
|
||
Updating registry `https://github.com/rust-lang/crates.io-index`
|
||
Downloading rand v0.3.14
|
||
...snip...
|
||
Compiling rand v0.3.14
|
||
Compiling add-one v0.1.0 (file:///projects/adder/add-one)
|
||
Compiling adder v0.1.0 (file:///projects/adder)
|
||
Finished debug [unoptimized + debuginfo] target(s) in 10.18 secs
|
||
</code></pre>
|
||
<p>现在 <em>Cargo.lock</em> 的顶部反映了<code>add-one</code>依赖<code>rand</code>这一事实。然而即使在工作空间的某处使用了<code>rand</code>,也不能在工作空间的其他 crate 使用它,除非在对应的 <em>Cargo.toml</em> 也增加<code>rand</code>的依赖。例如,如果在顶层的<code>adder</code> crate 的 <em>src/main.rs</em> 中增加<code>extern crate rand;</code>,将会出现一个错误:</p>
|
||
<pre><code>$ cargo build
|
||
Compiling adder v0.1.0 (file:///projects/adder)
|
||
error[E0463]: can't find crate for `rand`
|
||
--> src/main.rs:1:1
|
||
|
|
||
1 | extern crate rand;
|
||
| ^^^^^^^^^^^^^^^^^^^ can't find crate
|
||
</code></pre>
|
||
<p>为了修复这个错误,修改顶层的 <em>Cargo.toml</em> 并表明<code>rand</code>是<code>adder</code> crate 的一个依赖。</p>
|
||
<p>作为另一个提高,为 crate 中的<code>add_one::add_one</code>函数增加一个测试:</p>
|
||
<p><span class="filename">Filename: add-one/src/lib.rs</span></p>
|
||
<pre><code class="language-rust">pub fn add_one(x: i32) -> i32 {
|
||
x + 1
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn it_works() {
|
||
assert_eq!(3, add_one(2));
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>现在在顶层的 <em>adder</em> 目录运行<code>cargo test</code>:</p>
|
||
<pre><code>$ cargo test
|
||
Compiling adder v0.1.0 (file:///projects/adder)
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.27 secs
|
||
Running target/debug/adder-f0253159197f7841
|
||
|
||
running 0 tests
|
||
|
||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>等等,零个测试?我们不是刚增加了一个吗?如果我们观察输出,就不难发现在工作空间中的<code>cargo test</code>只运行顶层 crate 的测试。为了运行其他 crate 的测试,需要使用<code>-p</code>参数来表明我们希望运行指定包的测试:</p>
|
||
<pre><code>$ cargo test -p add-one
|
||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||
Running target/debug/deps/add_one-abcabcabc
|
||
|
||
running 1 test
|
||
test tests::it_works ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
|
||
|
||
Doc-tests add-one
|
||
|
||
running 0 tests
|
||
|
||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
|
||
</code></pre>
|
||
<p>类似的,如果选择将工作空间发布到 crates.io,其中的每一个包都需要单独发布。</p>
|
||
<p>随着项目增长,考虑使用工作空间:每一个更小的组件比一大块代码要容易理解。将 crate 保持在工作空间中易于协调他们的改变,如果他们一起运行并经常需要同时被修改的话。</p>
|
||
<a class="header" href="#使用cargo-install从-cratesio-安装文件" name="使用cargo-install从-cratesio-安装文件"><h2>使用<code>cargo install</code>从 Crates.io 安装文件</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-04-installing-binaries.md">ch14-04-installing-binaries.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p><code>cargo install</code>命令用于在本地安装和使用二进制 crate。它并不打算替换系统中的包;它意在作为一个方便 Rust 开发者们安装其他人已经在 crates.io 上共享的工具的手段。只有有二进制目标文件的包能够安装,而且所有二进制文件都被安装到 Rust 安装根目录的 <em>bin</em> 文件夹中。如果你使用 <em>rustup.rs</em> 安装的 Rust 且没有自定义任何配置,这将是<code>$HOME/.cargo/bin</code>。将这个目录添加到<code>$PATH</code>环境变量中就能够运行通过<code>cargo install</code>安装的程序了。</p>
|
||
<p>例如,第十二章提到的叫做<code>ripgrep</code>的用于搜索文件的<code>grep</code>的 Rust 实现。如果想要安装<code>ripgrep</code>,可以运行如下:</p>
|
||
<pre><code>$ cargo install ripgrep
|
||
Updating registry `https://github.com/rust-lang/crates.io-index`
|
||
Downloading ripgrep v0.3.2
|
||
...snip...
|
||
Compiling ripgrep v0.3.2
|
||
Finished release [optimized + debuginfo] target(s) in 97.91 secs
|
||
Installing ~/.cargo/bin/rg
|
||
</code></pre>
|
||
<p>最后一行输出展示了安装的二进制文件的位置和名称,在这里<code>ripgrep</code>被命名为<code>rg</code>。只要你像上面提到的那样将安装目录假如<code>$PATH</code>,就可以运行<code>rg --help</code>并开始使用一个更快更 Rust 的工具来搜索文件了!</p>
|
||
<a class="header" href="#cargo-自定义扩展命令" name="cargo-自定义扩展命令"><h2>Cargo 自定义扩展命令</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-05-extending-cargo.md">ch14-05-extending-cargo.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p>Cargo 被设计为可扩展的,通过新的子命令而无须修改 Cargo 自身。如果<code>$PATH</code>中有类似<code>cargo-something</code>的二进制文件,就可以通过<code>cargo something</code>来像 Cargo 子命令一样运行它。像这样的自定义命令也可以运行<code>cargo --list</code>来展示出来,通过<code>cargo install</code>向 Cargo 安装扩展并可以如内建 Cargo 工具那样运行他们是很方便的!</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>通过 Cargo 和 crates.io 来分享代码是使得 Rust 生态环境可以用于许多不同的任务的重要组成部分。Rust 的标准库是小而稳定的,不过 crate 易于分享和使用,并采用一个不同语言自身的时间线来提供改进。不要羞于在 crates.io 上共享对你有用的代码;因为它很有可能对别人也很有用!</p>
|
||
<a class="header" href="#智能指针" name="智能指针"><h1>智能指针</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-00-smart-pointers.md">ch15-00-smart-pointers.md</a>
|
||
<br>
|
||
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
|
||
</blockquote>
|
||
<p><strong>指针</strong>是一个常见的编程概念,它代表一个指向储存其他数据的位置。第四章学习了 Rust 的引用;他们是一类很平常的指针,以<code>&</code>符号为标志并借用了他们所指向的值。<strong>智能指针</strong>(<em>Smart pointers</em>)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和能力,比如说引用计数。智能指针模式起源于 C++。在 Rust 中,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反大部分情况,智能指针<strong>拥有</strong>他们指向的数据。</p>
|
||
<p>本书中已经出现过一些智能指针,虽然当时我们并不这么称呼他们。例如在某种意义上说,第八章的<code>String</code>和<code>Vec<T></code>都是智能指针。他们拥有一些数据并允许你修改他们,并带有元数据(比如他们的容量)和额外的功能或保证(<code>String</code>的数据总是有效的 UTF-8 编码)。智能指针区别于常规结构体的特性在于他们实现了<code>Deref</code>和<code>Drop</code> trait,而本章会讨论这些 trait 以及为什么对于智能指针来说他们很重要。</p>
|
||
<p>考虑到智能指针是一个在 Rust 经常被使用的通用设计模式,本章并不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的。这里将会讲到的是来自标准库中最常用的一些:</p>
|
||
<ul>
|
||
<li><code>Box<T></code>,用于在堆上分配值</li>
|
||
<li><code>Rc<T></code>,一个引用计数类型,其数据可以有多个所有者</li>
|
||
<li><code>RefCell<T></code>,其本身并不是智能指针,不过它管理智能指针<code>Ref</code>和<code>RefMut</code>的访问,在运行时而不是在编译时执行借用规则。</li>
|
||
</ul>
|
||
<p>同时我们还将涉及:</p>
|
||
<ul>
|
||
<li><strong>内部可变性</strong>(<em>interior mutability</em>)模式,当一个不可变类型暴露出改变其内部值的 API,这时借用规则适用于运行时而不是编译时。</li>
|
||
<li>引用循环,它如何会泄露内存,以及如何避免他们</li>
|
||
</ul>
|
||
<p>让我们开始吧!</p>
|
||
<a class="header" href="#boxt用于已知大小的堆上数据" name="boxt用于已知大小的堆上数据"><h2><code>Box<T></code>用于已知大小的堆上数据</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-01-box.md">ch15-01-box.md</a>
|
||
<br>
|
||
commit 85b2c9ac704c9dc4bbedb97209d336afb9809dc1</p>
|
||
</blockquote>
|
||
<p>最简单直接的智能指针是 <em>box</em>,它的类型是<code>Box<T></code>。 box 允许你将一个值放在堆上(第四章介绍过栈与堆)。列表 15-1 展示了如何使用 box 在堆上储存一个<code>i32</code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let b = Box::new(5);
|
||
println!("b = {}", b);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-1: Storing an <code>i32</code> value on the heap using a
|
||
box</span></p>
|
||
<p>这会打印出<code>b = 5</code>。在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像<code>b</code>这样的 box 在<code>main</code>的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。</p>
|
||
<p>将一个单独的值存放在堆上并不是很有意义,所以像列表 15-1 这样单独使用 box 并不常见。一个 box 的实用场景是当你希望确保类型有一个已知大小的时候。例如,考虑一下列表 15-2,它是一个用于 <em>cons list</em> 的枚举定义,这是一个来源于函数式编程的数据结构类型。注意它还不能编译:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">enum List {
|
||
Cons(i32, List),
|
||
Nil,
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-2: The first attempt of defining an enum to
|
||
represent a cons list data structure of <code>i32</code> values</span></p>
|
||
<p>我们实现了一个只存放<code>i32</code>值的 cons list。也可以选择使用第十章介绍的泛型来实现一个类型无关的 cons list。</p>
|
||
<blockquote>
|
||
<a class="header" href="#cons-list-的更多内容" name="cons-list-的更多内容"><h4>cons list 的更多内容</h4></a>
|
||
<p><em>cons list</em> 是一个来源于 Lisp 编程语言及其方言的数据结构。在 Lisp 中,<code>cons</code>函数("construct function"的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。</p>
|
||
<p>cons 函数的概念涉及到更通用的函数式编程术语;“将 x 与 y 连接”通常意味着构建一个新的容器而将 x 的元素放在新容器的开头,其后则是容器 y 的元素。</p>
|
||
<p>cons list 通过递归调用<code>cons</code>函数产生。代表递归的 base case 的规范名称是<code>Nil</code>,它宣布列表的终止。注意这不同于第六章中的"null"或"nil"的概念,他们代表无效或缺失的值。</p>
|
||
</blockquote>
|
||
<p>cons list 是一个每个元素和之后的其余部分都只包含一个值的列表。列表的其余部分由嵌套的 cons list 定义。其结尾由值<code>Nil</code>表示。cons list 在 Rust 中并不常见;通常<code>Vec<T></code>是一个更好的选择。实现这个数据结构是<code>Box<T></code>实用性的一个好的例子。让我们看看为什么!</p>
|
||
<p>使用 cons list 来储存列表<code>1, 2, 3</code>将看起来像这样:</p>
|
||
<pre><code class="language-rust,ignore">use List::{Cons, Nil};
|
||
|
||
fn main() {
|
||
let list = Cons(1, Cons(2, Cons(3, Nil)));
|
||
}
|
||
</code></pre>
|
||
<p>第一个<code>Cons</code>储存了<code>1</code>和另一个<code>List</code>值。这个<code>List</code>是另一个包含<code>2</code>的<code>Cons</code>值和下一个<code>List</code>值。这又是另一个存放了<code>3</code>的<code>Cons</code>值和最后一个值为<code>Nil</code>的<code>List</code>,非递归成员代表了列表的结尾。</p>
|
||
<p>如果尝试编译上面的代码,会得到如列表 15-3 所示的错误:</p>
|
||
<pre><code>error[E0072]: recursive type `List` has infinite size
|
||
-->
|
||
|
|
||
1 | enum List {
|
||
| _^ starting here...
|
||
2 | | Cons(i32, List),
|
||
3 | | Nil,
|
||
4 | | }
|
||
| |_^ ...ending here: recursive type has infinite size
|
||
|
|
||
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to
|
||
make `List` representable
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-3: The error we get when attempting to define
|
||
a recursive enum</span></p>
|
||
<p>错误表明这个类型“有无限的大小”。为什么呢?因为<code>List</code>的一个成员被定义为递归的:它存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放<code>List</code>值到底需要多少空间。让我们一点一点的看:首先了解一下 Rust 如何决定需要多少空间来存放一个非递归类型。回忆一下第六章讨论枚举定义时的列表 6-2 中定义的<code>Message</code>枚举:</p>
|
||
<pre><code class="language-rust">enum Message {
|
||
Quit,
|
||
Move { x: i32, y: i32 },
|
||
Write(String),
|
||
ChangeColor(i32, i32, i32),
|
||
}
|
||
</code></pre>
|
||
<p>当 Rust 需要知道需要为<code>Message</code>值分配多少空间时,它可以检查每一个成员并发现<code>Message::Quit</code>并不需要任何空间,<code>Message::Move</code>需要足够储存两个<code>i32</code>值的空间,依此类推。因此,<code>Message</code>值所需的最大空间等于储存其最大成员的空间大小。</p>
|
||
<p>与此相对当 Rust 编译器检查像列表 15-2 中的<code>List</code>这样的递归类型时会发生什么呢。编译器尝试计算出储存一个<code>List</code>枚举需要多少内存,并开始检查<code>Cons</code>成员,那么<code>Cons</code>需要的空间等于<code>i32</code>的大小加上<code>List</code>的大小。为了计算<code>List</code>需要多少内存,它检查其成员,从<code>Cons</code>成员开始。<code>Cons</code>成员储存了一个<code>i32</code>值和一个<code>List</code>值,这样的计算将无限进行下去,如图 15-4 所示:</p>
|
||
<p><img alt="An infinite Cons list" src="img/trpl15-01.svg" class="center" style="width: 50%;" /></p>
|
||
<p><span class="caption">Figure 15-4: An infinite <code>List</code> consisting of infinite
|
||
<code>Cons</code> variants</span></p>
|
||
<p>Rust 无法计算出要为定义为递归的类型分配多少空间,所以编译器给出了列表 15-3 中的错误。这个错误也包括了有用的建议:</p>
|
||
<pre><code class="language-text">= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to
|
||
make `List` representable
|
||
</code></pre>
|
||
<p>因为<code>Box<T></code>是一个指针,我们总是知道它需要多少空间:指针需要一个<code>usize</code>大小的空间。这个<code>usize</code>的值将是堆数据的地址。而堆数据可以是任意大小,不过开始这个堆数据的地址总是能放进一个<code>usize</code>中。所以如果将列表 15-2 的定义修改为像这里列表 15-5 中的定义,并修改<code>main</code>函数为<code>Cons</code>成员中的值使用<code>Box::new</code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">enum List {
|
||
Cons(i32, Box<List>),
|
||
Nil,
|
||
}
|
||
|
||
use List::{Cons, Nil};
|
||
|
||
fn main() {
|
||
let list = Cons(1,
|
||
Box::new(Cons(2,
|
||
Box::new(Cons(3,
|
||
Box::new(Nil))))));
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-5: Definition of <code>List</code> that uses <code>Box<T></code> in
|
||
order to have a known size</span></p>
|
||
<p>这样编译器就能够计算出储存一个<code>List</code>值需要的大小了。Rust 将会检查<code>List</code>,同样的从<code>Cons</code>成员开始检查。<code>Cons</code>成员需要<code>i32</code>的大小加上一个<code>usize</code>的大小,因为 box 总是<code>usize</code>大小的,不管它指向的是什么。接着 Rust 检查<code>Nil</code>成员,它并不储存一个值,所以<code>Nil</code>并不需要任何空间。我们通过 box 打破了这无限递归的连锁。图 15-6 展示了现在<code>Cons</code>成员看起来像什么:</p>
|
||
<p><img alt="A finite Cons list" src="img/trpl15-02.svg" class="center" /></p>
|
||
<p><span class="caption">Figure 15-6: A <code>List</code> that is not infinitely sized since
|
||
<code>Cons</code> holds a <code>Box</code></span></p>
|
||
<p>这就是 box 主要应用场景:打破无限循环的数据结构以便编译器可以知道其大小。第十七章讨论 trait 对象时我们将了解另一个 Rust 中会出现未知大小数据的情况。</p>
|
||
<p>虽然我们并不经常使用 box,他们也是一个了解智能指针模式的好的方式。<code>Box<T></code>作为智能指针经常被使用的两个方面是他们<code>Deref</code>和<code>Drop</code> trait 的实现。让我们研究这些 trait 如何工作以及智能指针如何利用他们。</p>
|
||
<a class="header" href="#deref-trait-允许通过引用访问数据" name="deref-trait-允许通过引用访问数据"><h2><code>Deref</code> Trait 允许通过引用访问数据</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-02-deref.md">ch15-02-deref.md</a>
|
||
<br>
|
||
commit ecc3adfe0cfa0a4a15a178dc002702fd0ea74b3f</p>
|
||
</blockquote>
|
||
<p>第一个智能指针相关的重要 trait 是<code>Deref</code>,它允许我们重载<code>*</code>,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的<code>*</code>能使访问其持有的数据更为方便,在本章结束前谈到解引用强制多态时我们会说明方便的意义。</p>
|
||
<p>第八章的哈希 map 的“根据旧值更新一个值”部分简要的提到了解引用运算符。当时有一个可变引用,而我们希望改变这个引用所指向的值。为此,首先我们必须解引用。这是另一个使用<code>i32</code>值引用的例子:</p>
|
||
<pre><code class="language-rust">let mut x = 5;
|
||
{
|
||
let y = &mut x;
|
||
|
||
*y += 1
|
||
}
|
||
|
||
assert_eq!(6, x);
|
||
</code></pre>
|
||
<p>我们使用<code>*y</code>来访问可变引用<code>y</code>所指向的数据,而不是可变引用本身。接着可以修改它的数据,在这里对其加一。</p>
|
||
<p>引用并不是智能指针,他们只是引用指向的一个值,所以这个解引用操作是很直接的。智能指针还会储存指针或数据的元数据。当解引用一个智能指针时,我们只想要数据,而不需要元数据。我们希望能在使用常规引用的地方也能使用智能指针。为此,可以通过实现<code>Deref</code> trait 来重载<code>*</code>运算符的行为。</p>
|
||
<p>列表 15-7 展示了一个定义为储存 mp3 数据和元数据的结构体通过<code>Deref</code> trait 来重载<code>*</code>的例子。<code>Mp3</code>,在某种意义上是一个智能指针:它拥有包含音频的<code>Vec<u8></code>数据。另外,它储存了一些可选的元数据,在这个例子中是音频数据中艺术家和歌曲的名称。我们希望能够方便的访问音频数据而不是元数据,所以需要实现<code>Deref</code> trait 来返回音频数据。实现<code>Deref</code> trait 需要一个叫做<code>deref</code>的方法,它借用<code>self</code>并返回其内部数据:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::ops::Deref;
|
||
|
||
struct Mp3 {
|
||
audio: Vec<u8>,
|
||
artist: Option<String>,
|
||
title: Option<String>,
|
||
}
|
||
|
||
impl Deref for Mp3 {
|
||
type Target = Vec<u8>;
|
||
|
||
fn deref(&self) -> &Vec<u8> {
|
||
&self.audio
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
let my_favorite_song = Mp3 {
|
||
// we would read the actual audio data from an mp3 file
|
||
audio: vec![1, 2, 3],
|
||
artist: Some(String::from("Nirvana")),
|
||
title: Some(String::from("Smells Like Teen Spirit")),
|
||
};
|
||
|
||
assert_eq!(vec![1, 2, 3], *my_favorite_song);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-7: An implementation of the <code>Deref</code> trait on a
|
||
struct that holds mp3 file data and metadata</span></p>
|
||
<p>大部分代码看起来都比较熟悉:一个结构体、一个 trait 实现、和一个创建了结构体示例的 main 函数。其中有一部分我们还未全面的讲解:类似于第十三章学习迭代器 trait 时出现的<code>type Item</code>,<code>type Target = T;</code>语法用于定义关联类型,第十九章会更详细的介绍。不必过分担心例子中的这一部分;它只是一个稍显不同的定义泛型参数的方式。</p>
|
||
<p>在<code>assert_eq!</code>中,我们验证<code>vec![1, 2, 3]</code>是否为<code>Mp3</code>实例<code>*my_favorite_song</code>解引用的值,结果正是如此因为我们实现了<code>deref</code>方法来返回音频数据。如果没有为<code>Mp3</code>实现<code>Deref</code> trait,Rust 将不会编译<code>*my_favorite_song</code>:会出现错误说<code>Mp3</code>类型不能被解引用。</p>
|
||
<p>没有<code>Deref</code> trait 的话,编译器只能解引用<code>&</code>引用,而<code>my_favorite_song</code>并不是(它是一个<code>Mp3</code>结构体)。通过<code>Deref</code> trait,编译器知道实现了<code>Deref</code> trait 的类型有一个返回引用的<code>deref</code>方法(在这个例子中,是<code>&self.audio</code>因为列表 15-7 中的<code>deref</code>的定义)。所以为了得到一个<code>*</code>可以解引用的<code>&</code>引用,编译器将<code>*my_favorite_song</code>展开为如下:</p>
|
||
<pre><code class="language-rust,ignore">*(my_favorite_song.deref())
|
||
</code></pre>
|
||
<p>这个就是<code>self.audio</code>中的结果值。<code>deref</code>返回一个引用并接下来必需解引用而不是直接返回值的原因是所有权:如果<code>deref</code>方法直接返回值而不是引用,其值将被移动出<code>self</code>。和大部分使用解引用运算符的地方相同,这里并不想获取<code>my_favorite_song.audio</code>的所有权。</p>
|
||
<p>注意将<code>*</code>替换为<code>deref</code>调用和<code>*</code>调用的过程在每次使用<code>*</code>的时候都会发生一次。<code>*</code>的替换并不会无限递归进行。最终的数据类型是<code>Vec<u8></code>,它与列表 15-7 中<code>assert_eq!</code>的<code>vec![1, 2, 3]</code>相匹配。</p>
|
||
<a class="header" href="#函数和方法的隐式解引用强制多态" name="函数和方法的隐式解引用强制多态"><h3>函数和方法的隐式解引用强制多态</h3></a>
|
||
<p>Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的<strong>解引用强制多态</strong>(<em>deref coercions</em>)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于当传递给函数的参数类型不同于函数签名中定义参数类型的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用<code>&</code>和<code>*</code>的引用和解引用。</p>
|
||
<p>使用列表 15-7 中的<code>Mp3</code>结构体,如下是一个获取<code>u8</code> slice 并压缩 mp3 音频数据的函数签名:</p>
|
||
<pre><code class="language-rust,ignore">fn compress_mp3(audio: &[u8]) -> Vec<u8> {
|
||
// the actual implementation would go here
|
||
}
|
||
</code></pre>
|
||
<p>如果 Rust 没有解引用强制多态,为了使用<code>my_favorite_song</code>中的音频数据调用此函数,必须写成:</p>
|
||
<pre><code class="language-rust,ignore">compress_mp3(my_favorite_song.audio.as_slice())
|
||
</code></pre>
|
||
<p>也就是说,必须明确表用需要<code>my_favorite_song</code>中的<code>audio</code>字段而且我们希望有一个 slice 来引用这整个<code>Vec<u8></code>。如果有很多地方需要用相同的方式处理<code>audio</code>数据,那么<code>.audio.as_slice()</code>就显得冗长重复了。</p>
|
||
<p>然而,因为解引用强制多态和<code>Mp3</code>的<code>Deref</code> trait 实现,我们可以使用如下代码使用<code>my_favorite_song</code>中的数据调用这个函数:</p>
|
||
<pre><code class="language-rust,ignore">let result = compress_mp3(&my_favorite_song);
|
||
</code></pre>
|
||
<p>只有<code>&</code>和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了<code>Deref</code>实现的优势:Rust 知道<code>Mp3</code>实现了<code>Deref</code> trait 并从<code>deref</code>方法返回<code>&Vec<u8></code>。它也知道标准库实现了<code>Vec<T></code>的<code>Deref</code> trait,其<code>deref</code>方法返回<code>&[T]</code>(我们也可以通过查阅<code>Vec<T></code>的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次<code>Deref::deref</code>来将<code>&Mp3</code>变成<code>&Vec<u8></code>再变成<code>&[T]</code>来满足<code>compress_mp3</code>的签名。这意味着我们可以少写一些代码!Rust 会多次分析<code>Deref::deref</code>的返回值类型直到它满足参数的类型,只要相关类型实现了<code>Deref</code> trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚!</p>
|
||
<p>类似于如何使用<code>Deref</code> trait 重载<code>&T</code>的<code>*</code>运算符,<code>DerefMut</code> trait用于重载<code>&mut T</code>的<code>*</code>运算符。</p>
|
||
<p>Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态:</p>
|
||
<ul>
|
||
<li>从<code>&T</code>到<code>&U</code>当<code>T: Deref<Target=U></code>。</li>
|
||
<li>从<code>&mut T</code>到<code>&mut U</code>当<code>T: DerefMut<Target=U></code>。</li>
|
||
<li>从<code>&mut T</code>到<code>&U</code>当<code>T: Deref<Target=U></code>。</li>
|
||
</ul>
|
||
<p>头两个情况除了可变性之外是相同的:如果有一个<code>&T</code>,而<code>T</code>实现了返回<code>U</code>类型的<code>Deref</code>,可以直接得到<code>&U</code>。对于可变引用也是一样。最后一个有些微妙:如果有一个可变引用,它也可以强转为一个不可变引用。反之则是_不可能_的:不可变引用永远也不能强转为可变引用。</p>
|
||
<p><code>Deref</code> trait 对于智能指针模式十分重要的原因在于智能指针可以被看作普通引用并被用于期望使用普通引用的地方。例如,无需重新编写方法和函数来直接获取智能指针。</p>
|
||
<a class="header" href="#drop-trait-运行清理代码" name="drop-trait-运行清理代码"><h2><code>Drop</code> Trait 运行清理代码</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-03-drop.md">ch15-03-drop.md</a>
|
||
<br>
|
||
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56</p>
|
||
</blockquote>
|
||
<p>对于智能指针模式来说另一个重要的 trait 是<code>Drop</code>。<code>Drop</code>运行我们在值要离开作用域时执行一些代码。智能指针在被丢弃时会执行一些重要的清理工作,比如释放内存或减少引用计数。更一般的来讲,数据类型可以管理多于内存的资源,比如文件或网络连接,而使用<code>Drop</code>在代码处理完他们之后释放这些资源。我们在智能指针上下文中讨论<code>Drop</code>是因为其功能几乎总是用于实现智能指针。</p>
|
||
<p>在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。这意味着无需记住在所有处理完这些类型实例后调用清理代码,而仍然不会泄露资源!</p>
|
||
<p>指定在值离开作用域时应该执行的代码的方式是实现<code>Drop</code> trait。<code>Drop</code> trait 要求我们实现一个叫做<code>drop</code>的方法,它获取一个<code>self</code>的可变引用。</p>
|
||
<p>列表 15-8 展示了并没有实际功能的结构体<code>CustomSmartPointer</code>,不过我们会在创建实例之后打印出<code>CustomSmartPointer created.</code>,而在实例离开作用域时打印出<code>Dropping CustomSmartPointer!</code>,这样就能看出每一段代码是何时被执行的。实际的项目中,我们应该在<code>drop</code>中清理任何智能指针运行所需要的资源,而不是这个例子中的<code>println!</code>语句:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">struct CustomSmartPointer {
|
||
data: String,
|
||
}
|
||
|
||
impl Drop for CustomSmartPointer {
|
||
fn drop(&mut self) {
|
||
println!("Dropping CustomSmartPointer!");
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
let c = CustomSmartPointer { data: String::from("some data") };
|
||
println!("CustomSmartPointer created.");
|
||
println!("Wait for it...");
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-8: A <code>CustomSmartPointer</code> struct that
|
||
implements the <code>Drop</code> trait, where we could put code that would clean up after
|
||
the <code>CustomSmartPointer</code>.</span></p>
|
||
<p><code>Drop</code> trait 位于 prelude 中,所以无需导入它。<code>drop</code>方法的实现调用了<code>println!</code>;这里是你需要放入实际关闭套接字代码的地方。在<code>main</code>函数中,我们创建一个<code>CustomSmartPointer</code>的新实例并打印出<code>CustomSmartPointer created.</code>以便在运行时知道代码运行到此处。在<code>main</code>的结尾,<code>CustomSmartPointer</code>的实例会离开作用域。注意我们没有显式调用<code>drop</code>方法:</p>
|
||
<p>当运行这个程序,我们会看到:</p>
|
||
<pre><code>CustomSmartPointer created.
|
||
Wait for it...
|
||
Dropping CustomSmartPointer!
|
||
</code></pre>
|
||
<p>被打印到屏幕上,它展示了 Rust 在实例离开作用域时自动调用了<code>drop</code>。</p>
|
||
<p>可以使用<code>std::mem::drop</code>函数来在值离开作用域之前丢弃它。这通常是不必要的;整个<code>Drop</code> trait 的要点在于它自动的帮我们处理清理工作。在第十六章讲到并发时我们会看到一个需要在离开作用域之前丢弃值的例子。现在知道这是可能的即可,<code>std::mem::drop</code>位于 prelude 中所以可以如列表 15-9 所示直接调用<code>drop</code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let c = CustomSmartPointer { data: String::from("some data") };
|
||
println!("CustomSmartPointer created.");
|
||
drop(c);
|
||
println!("Wait for it...");
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-9: Calling <code>std::mem::drop</code> to explicitly drop
|
||
a value before it goes out of scope</span></p>
|
||
<p>运行这段代码会打印出如下内容,因为<code>Dropping CustomSmartPointer!</code>在<code>CustomSmartPointer created.</code>和<code>Wait for it...</code>之间被打印出来,表明析构代码被执行了:</p>
|
||
<pre><code>CustomSmartPointer created.
|
||
Dropping CustomSmartPointer!
|
||
Wait for it...
|
||
</code></pre>
|
||
<p>注意不允许直接调用我们定义的<code>drop</code>方法:如果将列表 15-9 中的<code>drop(c)</code>替换为<code>c.drop()</code>,会得到一个编译错误表明<code>explicit destructor calls not allowed</code>。不允许直接调用<code>Drop::drop</code>的原因是 Rust 在值离开作用域时会自动插入<code>Drop::drop</code>,这样就会丢弃值两次。丢弃一个值两次可能会造成错误或破坏内存,所以 Rust 就不允许这么做。相应的可以调用<code>std::mem::drop</code>,它的定义是:</p>
|
||
<pre><code class="language-rust">pub mod std {
|
||
pub mod mem {
|
||
pub fn drop<T>(x: T) { }
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>这个函数对于<code>T</code>是泛型的,所以可以传递任何值。这个函数的函数体并没有任何实际内容,所以它也不会利用其参数。这个空函数的作用在于<code>drop</code>获取其参数的所有权,它意味着在这个函数结尾<code>x</code>离开作用域时<code>x</code>会被丢弃。</p>
|
||
<p>使用<code>Drop</code> trait 实现指定的代码在很多方面都使得清理值变得方便和安全:比如可以使用它来创建我们自己的内存分配器!通过<code>Drop</code> trait 和 Rust 所有权系统,就无需担心之后清理代码,因为 Rust 会自动考虑这些问题。如果代码在值仍被使用时就清理它会出现编译错误,因为所有权系统确保了引用总是有效的,这也就保证了<code>drop</code>只会在值不再被使用时被调用一次。</p>
|
||
<p>现在我们学习了<code>Box<T></code>和一些智能指针的特性,让我们聊聊一些其他标准库中定义的拥有各种实用功能的智能指针。</p>
|
||
<a class="header" href="#rct-引用计数智能指针" name="rct-引用计数智能指针"><h2><code>Rc<T></code> 引用计数智能指针</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-04-rc.md">ch15-04-rc.md</a>
|
||
<br>
|
||
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56</p>
|
||
</blockquote>
|
||
<p>大部分情况下所有权是非常明确的:可以准确的知道哪个变量拥有某个值。然而并不总是如此;有时确实可能需要多个所有者。为此,Rust 有一个叫做<code>Rc<T></code>的类型。它的名字是<strong>引用计数</strong>(<em>reference counting</em>)的缩写。引用计数意味着它记录一个值引用的数量来知晓这个值是否仍在被使用。如果这个值有零个引用,就知道可以在没有有效引用的前提下清理这个值。</p>
|
||
<p>根据现实生活场景来想象的话,它就像一个客厅的电视。当一个人进来看电视时,他打开电视。其他人也会进来看电视。当最后一个人离开房间时,他关掉电视因为它不再被使用了。如果某人在其他人还在看的时候关掉了电视,正在看电视人肯定会抓狂的!</p>
|
||
<p><code>Rc<T></code>用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的那一部分会最后结束使用它。如果我们知道的话那么常规的所有权规则会在编译时强制起作用。</p>
|
||
<p>注意<code>Rc<T></code>只能用于单线程场景;下一章并发会涉及到如何在多线程程序中进行引用计数。如果尝试在多线程中使用<code>Rc<T></code>则会得到一个编译错误。</p>
|
||
<a class="header" href="#使用rct分享数据" name="使用rct分享数据"><h3>使用<code>Rc<T></code>分享数据</h3></a>
|
||
<p>让我们回到列表 15-5 中的 cons list 例子。在列表 15-11 中尝试使用<code>Box<T></code>定义的<code>List</code>。首先创建了一个包含 5 接着是 10 的列表实例。之后我们想要创建另外两个列表:一个以 3 开始并后接第一个包含 5 和 10 的列表,另一个以 4 开始其后<strong>也</strong>是第一个列表。换句话说,我们希望这两个列表共享第三个列表的所有权,概念上类似于图 15-10:</p>
|
||
<p><img alt="Two lists that share ownership of a third list" src="img/trpl15-03.svg" class="center" /></p>
|
||
<p><span class="caption">Figure 15-10: Two lists, <code>b</code> and <code>c</code>, sharing ownership
|
||
of a third list, <code>a</code></span></p>
|
||
<p>尝试使用<code>Box<T></code>定义的<code>List</code>并不能工作,如列表 15-11 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">enum List {
|
||
Cons(i32, Box<List>),
|
||
Nil,
|
||
}
|
||
|
||
use List::{Cons, Nil};
|
||
|
||
fn main() {
|
||
let a = Cons(5,
|
||
Box::new(Cons(10,
|
||
Box::new(Nil))));
|
||
let b = Cons(3, Box::new(a));
|
||
let c = Cons(4, Box::new(a));
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-11: Having two lists using <code>Box<T></code> that try
|
||
to share ownership of a third list won't work</span></p>
|
||
<p>编译会得出如下错误:</p>
|
||
<pre><code>error[E0382]: use of moved value: `a`
|
||
--> src/main.rs:13:30
|
||
|
|
||
12 | let b = Cons(3, Box::new(a));
|
||
| - value moved here
|
||
13 | let c = Cons(4, Box::new(a));
|
||
| ^ value used here after move
|
||
|
|
||
= note: move occurs because `a` has type `List`, which does not
|
||
implement the `Copy` trait
|
||
</code></pre>
|
||
<p><code>Cons</code>成员拥有其储存的数据,所以当创建<code>b</code>列表时将<code>a</code>的所有权移动到了<code>b</code>。接着当再次尝使用<code>a</code>创建<code>c</code>时,这不被允许因为<code>a</code>的所有权已经被移动。</p>
|
||
<p>相反可以改变<code>Cons</code>的定义来存放一个引用,不过接着必须指定生命周期参数,而且在构造列表时,也必须使列表中的每一个元素都至少与列表本身存在的一样久。否则借用检查器甚至都不会允许我们编译代码。</p>
|
||
<p>如列表 15-12 所示,可以将<code>List</code>的定义从<code>Box<T></code>改为<code>Rc<T></code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">enum List {
|
||
Cons(i32, Rc<List>),
|
||
Nil,
|
||
}
|
||
|
||
use List::{Cons, Nil};
|
||
use std::rc::Rc;
|
||
|
||
fn main() {
|
||
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
|
||
let b = Cons(3, a.clone());
|
||
let c = Cons(4, a.clone());
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-12: A definition of <code>List</code> that uses
|
||
<code>Rc<T></code></span></p>
|
||
<p>注意必须为<code>Rc</code>增加<code>use</code>语句因为它不在 prelude 中。在<code>main</code>中创建了存放 5 和 10 的列表并将其存放在一个叫做<code>a</code>的新的<code>Rc</code>中。接着当创建<code>b</code>和<code>c</code>时,我们对<code>a</code>调用了<code>clone</code>方法。</p>
|
||
<a class="header" href="#克隆rct会增加引用计数" name="克隆rct会增加引用计数"><h3>克隆<code>Rc<T></code>会增加引用计数</h3></a>
|
||
<p>之前我们见过<code>clone</code>方法,当时使用它来创建某些数据的完整拷贝。但是对于<code>Rc<T></code>来说,它并不创建一个完整的拷贝。<code>Rc<T></code>存放了<strong>引用计数</strong>,也就是说,一个存在多少个克隆的计数器。让我们像列表 15-13 那样在创建<code>c</code>时增加一个内部作用域,并在不同的位置打印出关联函数<code>Rc::strong_count</code>的结果。<code>Rc::strong_count</code>返回传递给它的<code>Rc</code>值的引用计数,而在本章的稍后部分介绍避免引用循环时讲到它为什么叫做<code>strong_count</code>。</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust"># enum List {
|
||
# Cons(i32, Rc<List>),
|
||
# Nil,
|
||
# }
|
||
#
|
||
# use List::{Cons, Nil};
|
||
# use std::rc::Rc;
|
||
#
|
||
fn main() {
|
||
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
|
||
println!("rc = {}", Rc::strong_count(&a));
|
||
let b = Cons(3, a.clone());
|
||
println!("rc after creating b = {}", Rc::strong_count(&a));
|
||
{
|
||
let c = Cons(4, a.clone());
|
||
println!("rc after creating c = {}", Rc::strong_count(&a));
|
||
}
|
||
println!("rc after c goes out of scope = {}", Rc::strong_count(&a));
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-13: Printing out the reference count</span></p>
|
||
<p>这会打印出:</p>
|
||
<pre><code>rc = 1
|
||
rc after creating b = 2
|
||
rc after creating c = 3
|
||
rc after c goes out of scope = 2
|
||
</code></pre>
|
||
<p>不难看出<code>a</code>的初始引用计数是一。接着每次调用<code>clone</code>,计数会加一。当<code>c</code>离开作用域时,计数减一,这发生在<code>Rc<T></code>的<code>Drop</code> trait 实现中。这个例子中不能看到的是当<code>b</code>接着是<code>a</code>在<code>main</code>函数的结尾离开作用域时,包含 5 和 10 的列表的引用计数会是 0,这时列表将被丢弃。这个策略允许拥有多个所有者,而引用计数会确保任何所有者存在时这个值保持有效。</p>
|
||
<p>在本部分的开始,我们说<code>Rc<T></code>只允许程序的多个部分读取<code>Rc<T></code>中<code>T</code>的不可变引用。如果<code>Rc<T></code>允许一个可变引用,我们将遇到第四章讨论的借用规则所不允许的问题:两个指向同一位置的可变借用会导致数据竞争和不一致。不过可变数据是非常有用的!在下一部分,我们将讨论内部可变性模式和<code>RefCell<T></code>类型,它可以与<code>Rc<T></code>结合使用来处理不可变性的限制。</p>
|
||
<a class="header" href="#refcellt和内部可变性模式" name="refcellt和内部可变性模式"><h2><code>RefCell<T></code>和内部可变性模式</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-05-interior-mutability.md">ch15-05-interior-mutability.md</a>
|
||
<br>
|
||
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56</p>
|
||
</blockquote>
|
||
<p><strong>内部可变性</strong>(<em>Interior mutability</em>)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时改变数据,这通常是借用规则所不允许。内部可变性模式涉及到在数据结构中使用<code>unsafe</code>代码来模糊 Rust 通常的可变性和借用规则。我们还未讲到不安全代码;第十九章会学习他们。内部可变性模式用于当你可以确保代码在运行时也会遵守借用规则,哪怕编译器也不能保证的情况。引入的<code>unsafe</code>代码将被封装进安全的 API 中,而外部类型仍然是不可变的。</p>
|
||
<p>让我们通过遵循内部可变性模式的<code>RefCell<T></code>类型来开始探索。</p>
|
||
<a class="header" href="#refcellt拥有内部可变性" name="refcellt拥有内部可变性"><h3><code>RefCell<T></code>拥有内部可变性</h3></a>
|
||
<p>不同于<code>Rc<T></code>,<code>RefCell<T></code>代表其数据的唯一的所有权。那么是什么让<code>RefCell<T></code>不同于像<code>Box<T></code>这样的类型呢?回忆一下第四章所学的借用规则:</p>
|
||
<ol>
|
||
<li>在任意给定时间,<strong>只能</strong>拥有如下中的一个:</li>
|
||
</ol>
|
||
<ul>
|
||
<li>一个可变引用。</li>
|
||
<li>任意属性的不可变引用。</li>
|
||
</ul>
|
||
<ol start="2">
|
||
<li>引用必须总是有效的。</li>
|
||
</ol>
|
||
<p>对于引用和<code>Box<T></code>,借用规则的不可变性作用于编译时。对于<code>RefCell<T></code>,这些不可变性作用于<strong>运行时</strong>。对于引用,如果违反这些规则,会得到一个编译错误。而对于<code>RefCell<T></code>,违反这些规则会<code>panic!</code>。</p>
|
||
<p>Rust 编译器执行的静态分析天生是保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是停机问题(停机问题),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。</p>
|
||
<p>因为一些分析是不可能的,Rust 编译器在其不确定的时候甚至都不尝试猜测,所以说它是保守的而且有时会拒绝事实上不会违反 Rust 保证的正确的程序。换句话说,如果 Rust 接受不正确的程序,那么人们也就不会相信 Rust 所做的保证了。如果 Rust 拒绝正确的程序,会给程序员带来不便,但不会带来灾难。<code>RefCell<T></code>正是用于当你知道代码遵守借用规则,而编译器不能理解的时候。</p>
|
||
<p>类似于<code>Rc<T></code>,<code>RefCell<T></code>只能用于单线程场景。在并发章节会介绍如何在多线程程序中使用<code>RefCell<T></code>的功能。现在所有你需要知道的就是如果尝试在多线程上下文中使用<code>RefCell<T></code>,会得到一个编译错误。</p>
|
||
<p>对于引用,可以使用<code>&</code>和<code>&mut</code>语法来分别创建不可变和可变的引用。不过对于<code>RefCell<T></code>,我们使用<code>borrow</code>和<code>borrow_mut</code>方法,它是<code>RefCell<T></code>拥有的安全 API 的一部分。<code>borrow</code>返回<code>Ref</code>类型的智能指针,而<code>borrow_mut</code>返回<code>RefMut</code>类型的智能指针。这两个类型实现了<code>Deref</code>所以可以被当作常规引用处理。<code>Ref</code>和<code>RefMut</code>动态的借用所有权,而他们的<code>Drop</code>实现也动态的释放借用。</p>
|
||
<p>列表 15-14 展示了如何使用<code>RefCell<T></code>来使函数不可变的和可变的借用它的参数。注意<code>data</code>变量使用<code>let data</code>而不是<code>let mut data</code>来声明为不可变的,而<code>a_fn_that_mutably_borrows</code>则允许可变的借用数据并修改它!</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::cell::RefCell;
|
||
|
||
fn a_fn_that_immutably_borrows(a: &i32) {
|
||
println!("a is {}", a);
|
||
}
|
||
|
||
fn a_fn_that_mutably_borrows(b: &mut i32) {
|
||
*b += 1;
|
||
}
|
||
|
||
fn demo(r: &RefCell<i32>) {
|
||
a_fn_that_immutably_borrows(&r.borrow());
|
||
a_fn_that_mutably_borrows(&mut r.borrow_mut());
|
||
a_fn_that_immutably_borrows(&r.borrow());
|
||
}
|
||
|
||
fn main() {
|
||
let data = RefCell::new(5);
|
||
demo(&data);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-14: Using <code>RefCell<T></code>, <code>borrow</code>, and
|
||
<code>borrow_mut</code></span></p>
|
||
<p>这个例子打印出:</p>
|
||
<pre><code>a is 5
|
||
a is 6
|
||
</code></pre>
|
||
<p>在<code>main</code>函数中,我们新声明了一个包含值 5 的<code>RefCell<T></code>,并储存在变量<code>data</code>中,声明时并没有使用<code>mut</code>关键字。接着使用<code>data</code>的一个不可变引用来调用<code>demo</code>函数:对于<code>main</code>函数而言<code>data</code>是不可变的!</p>
|
||
<p>在<code>demo</code>函数中,通过调用<code>borrow</code>方法来获取到<code>RefCell<T></code>中值的不可变引用,并使用这个不可变引用调用了<code>a_fn_that_immutably_borrows</code>函数。更为有趣的是,可以通过<code>borrow_mut</code>方法来获取<code>RefCell<T></code>中值的<strong>可变</strong>引用,而<code>a_fn_that_mutably_borrows</code>函数就允许修改这个值。可以看到下一次调用<code>a_fn_that_immutably_borrows</code>时打印出的值是 6 而不是 5。</p>
|
||
<a class="header" href="#refcellt在运行时检查借用规则" name="refcellt在运行时检查借用规则"><h3><code>RefCell<T></code>在运行时检查借用规则</h3></a>
|
||
<p>回忆一下第四章因为借用规则,尝试使用常规引用在同一作用域中创建两个可变引用的代码无法编译:</p>
|
||
<pre><code class="language-rust,ignore">let mut s = String::from("hello");
|
||
|
||
let r1 = &mut s;
|
||
let r2 = &mut s;
|
||
</code></pre>
|
||
<p>这会得到一个编译错误:</p>
|
||
<pre><code>error[E0499]: cannot borrow `s` as mutable more than once at a time
|
||
-->
|
||
|
|
||
5 | let r1 = &mut s;
|
||
| - first mutable borrow occurs here
|
||
6 | let r2 = &mut s;
|
||
| ^ second mutable borrow occurs here
|
||
7 | }
|
||
| - first borrow ends here
|
||
</code></pre>
|
||
<p>与此相反,使用<code>RefCell<T></code>并在同一作用域调用两次<code>borrow_mut</code>的代码是<strong>可以</strong>编译的,不过它会在运行时 panic。如下代码:</p>
|
||
<pre><code class="language-rust,should_panic">use std::cell::RefCell;
|
||
|
||
fn main() {
|
||
let s = RefCell::new(String::from("hello"));
|
||
|
||
let r1 = s.borrow_mut();
|
||
let r2 = s.borrow_mut();
|
||
}
|
||
</code></pre>
|
||
<p>能够编译不过在<code>cargo run</code>运行时会出现如下错误:</p>
|
||
<pre><code> Finished dev [unoptimized + debuginfo] target(s) in 0.83 secs
|
||
Running `target/debug/refcell`
|
||
thread 'main' panicked at 'already borrowed: BorrowMutError',
|
||
/stable-dist-rustc/build/src/libcore/result.rs:868
|
||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||
</code></pre>
|
||
<p>这个运行时<code>BorrowMutError</code>类似于编译错误:它表明我们已经可变得借用过一次<code>s</code>了,所以不允许再次借用它。我们并没有绕过借用规则,只是选择让 Rust 在运行时而不是编译时执行他们。你可以选择在任何时候任何地方使用<code>RefCell<T></code>,不过除了不得不编写很多<code>RefCell</code>之外,最终还是可能会发现其中的问题(可能是在生产环境而不是开发环境)。另外,在运行时检查借用规则有性能惩罚。</p>
|
||
<a class="header" href="#结合rct和refcellt来拥有多个可变数据所有者" name="结合rct和refcellt来拥有多个可变数据所有者"><h3>结合<code>Rc<T></code>和<code>RefCell<T></code>来拥有多个可变数据所有者</h3></a>
|
||
<p>那么为什么要权衡考虑选择引入<code>RefCell<T></code>呢?好吧,还记得我们说过<code>Rc<T></code>只能拥有一个<code>T</code>的不可变引用吗?考虑到<code>RefCell<T></code>是不可变的,但是拥有内部可变性,可以将<code>Rc<T></code>与<code>RefCell<T></code>结合来创造一个既有引用计数又可变的类型。列表 15-15 展示了一个这么做的例子,再次回到列表 15-5 中的 cons list。在这个例子中,不同于在 cons list 中储存<code>i32</code>值,我们储存一个<code>Rc<RefCell<i32>></code>值。希望储存这个类型是因为其可以拥有不属于列表一部分的这个值的所有者(<code>Rc<T></code>提供的多个所有者功能),而且还可以改变内部的<code>i32</code>值(<code>RefCell<T></code>提供的内部可变性功能):</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">#[derive(Debug)]
|
||
enum List {
|
||
Cons(Rc<RefCell<i32>>, Rc<List>),
|
||
Nil,
|
||
}
|
||
|
||
use List::{Cons, Nil};
|
||
use std::rc::Rc;
|
||
use std::cell::RefCell;
|
||
|
||
fn main() {
|
||
let value = Rc::new(RefCell::new(5));
|
||
|
||
let a = Cons(value.clone(), Rc::new(Nil));
|
||
let shared_list = Rc::new(a);
|
||
|
||
let b = Cons(Rc::new(RefCell::new(6)), shared_list.clone());
|
||
let c = Cons(Rc::new(RefCell::new(10)), shared_list.clone());
|
||
|
||
*value.borrow_mut() += 10;
|
||
|
||
println!("shared_list after = {:?}", shared_list);
|
||
println!("b after = {:?}", b);
|
||
println!("c after = {:?}", c);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-15: Using <code>Rc<RefCell<i32>></code> to create a
|
||
<code>List</code> that we can mutate</span></p>
|
||
<p>我们创建了一个值,它是<code>Rc<RefCell<i32>></code>的实例。将其储存在变量<code>value</code>中因为我们希望之后能直接访问它。接着在<code>a</code>中创建了一个拥有存放了<code>value</code>值的<code>Cons</code>成员的<code>List</code>,而且<code>value</code>需要被克隆因为我们希望除了<code>a</code>之外还拥有<code>value</code>的所有权。接着将<code>a</code>封装进<code>Rc<T></code>中这样就可以创建都引用<code>a</code>的有着不同开头的列表<code>b</code>和<code>c</code>,类似列表 15-12 中所做的那样。</p>
|
||
<p>一旦创建了<code>shared_list</code>、<code>b</code>和<code>c</code>,接下来就可以通过解引用<code>Rc<T></code>和对<code>RefCell</code>调用<code>borrow_mut</code>来将 10 与 5 相加了。</p>
|
||
<p>当打印出<code>shared_list</code>、<code>b</code>和<code>c</code>时,可以看到他们都拥有被修改的值 15:</p>
|
||
<pre><code>shared_list after = Cons(RefCell { value: 15 }, Nil)
|
||
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
|
||
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
|
||
</code></pre>
|
||
<p>这是非常巧妙的!通过使用<code>RefCell<T></code>,我们可以拥有一个表面上不可变的<code>List</code>,不过可以使用<code>RefCell<T></code>中提供内部可变性的方法来在需要时修改数据。<code>RefCell<T></code>的运行时借用规则检查也确实保护我们免于出现数据竞争,而且我们也决定牺牲一些速度来换取数据结构的灵活性。</p>
|
||
<p><code>RefCell<T></code>并不是标准库中唯一提供内部可变性的类型。<code>Cell<T></code>有点类似,不过不同于<code>RefCell<T></code>那样提供内部值的引用,其值被拷贝进和拷贝出<code>Cell<T></code>。<code>Mutex<T></code>提供线程间安全的内部可变性,下一章并发会讨论它的应用。请查看标准库来获取更多细节和不同类型的区别。</p>
|
||
<a class="header" href="#引用循环和内存泄漏是安全的" name="引用循环和内存泄漏是安全的"><h2>引用循环和内存泄漏是安全的</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-06-reference-cycles.md">ch15-06-reference-cycles.md</a>
|
||
<br>
|
||
commit 9430a3d28a2121a938d704ce48b15d21062f880e</p>
|
||
</blockquote>
|
||
<p>我们讨论过 Rust 做出的一些保证,例如永远也不会遇到一个空值,而且数据竞争也会在编译时被阻止。Rust 的内存安全保证也使其更难以制造从不被清理的内存,这被称为<strong>内存泄露</strong>。然而 Rust 并不是<strong>不可能</strong>出现内存泄漏,避免内存泄露<strong>并</strong>不是 Rust 的保证之一。换句话说,内存泄露是安全的。</p>
|
||
<p>在使用<code>Rc<T></code>和<code>RefCell<T></code>时,有可能创建循环引用,这时各个项相互引用并形成环。这是不好的因为每一项的引用计数将永远也到不了 0,其值也永远也不会被丢弃。让我们看看这是如何发生的以及如何避免它。</p>
|
||
<p>在列表 15-16 中,我们将使用列表 15-5 中<code>List</code>定义的另一个变体。我们将回到储存<code>i32</code>值作为<code>Cons</code>成员的第一个元素。现在<code>Cons</code>成员的第二个元素是<code>RefCell<Rc<List>></code>:这时就不能修改<code>i32</code>值了,但是能够修改<code>Cons</code>成员指向的那个<code>List</code>。还需要增加一个<code>tail</code>方法来方便我们在拥有一个<code>Cons</code>成员时访问第二个项:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">#[derive(Debug)]
|
||
enum List {
|
||
Cons(i32, RefCell<Rc<List>>),
|
||
Nil,
|
||
}
|
||
|
||
impl List {
|
||
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
|
||
match *self {
|
||
Cons(_, ref item) => Some(item),
|
||
Nil => None,
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-16: A cons list definition that holds a
|
||
<code>RefCell</code> so that we can modify what a <code>Cons</code> variant is referring to</span></p>
|
||
<p>接下来,在列表 15-17 中,我们将在变量<code>a</code>中创建一个<code>List</code>值,其内部是一个<code>5, Nil</code>的列表。接着在变量<code>b</code>创建一个值 10 和指向<code>a</code>中列表的<code>List</code>值。最后修改<code>a</code>指向<code>b</code>而不是<code>Nil</code>,这会创建一个循环:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust"># #[derive(Debug)]
|
||
# enum List {
|
||
# Cons(i32, RefCell<Rc<List>>),
|
||
# Nil,
|
||
# }
|
||
#
|
||
# impl List {
|
||
# fn tail(&self) -> Option<&RefCell<Rc<List>>> {
|
||
# match *self {
|
||
# Cons(_, ref item) => Some(item),
|
||
# Nil => None,
|
||
# }
|
||
# }
|
||
# }
|
||
#
|
||
use List::{Cons, Nil};
|
||
use std::rc::Rc;
|
||
use std::cell::RefCell;
|
||
|
||
fn main() {
|
||
|
||
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
|
||
|
||
println!("a initial rc count = {}", Rc::strong_count(&a));
|
||
println!("a next item = {:?}", a.tail());
|
||
|
||
let b = Rc::new(Cons(10, RefCell::new(a.clone())));
|
||
|
||
println!("a rc count after b creation = {}", Rc::strong_count(&a));
|
||
println!("b initial rc count = {}", Rc::strong_count(&b));
|
||
println!("b next item = {:?}", b.tail());
|
||
|
||
if let Some(ref link) = a.tail() {
|
||
*link.borrow_mut() = b.clone();
|
||
}
|
||
|
||
println!("b rc count after changing a = {}", Rc::strong_count(&b));
|
||
println!("a rc count after changing a = {}", Rc::strong_count(&a));
|
||
|
||
// Uncomment the next line to see that we have a cycle; it will
|
||
// overflow the stack
|
||
// println!("a next item = {:?}", a.tail());
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-17: Creating a reference cycle of two <code>List</code>
|
||
values pointing to each other</span></p>
|
||
<p>使用<code>tail</code>方法来获取<code>a</code>中<code>RefCell</code>的引用,并将其放入变量<code>link</code>中。接着对<code>RefCell</code>使用<code>borrow_mut</code>方法将其中的值从存放<code>Nil</code>值的<code>Rc</code>改为<code>b</code>中的<code>Rc</code>。这创建了一个看起来像图 15-18 所示的引用循环:</p>
|
||
<p><img alt="Reference cycle of lists" src="img/trpl15-04.svg" class="center" style="width: 50%;" /></p>
|
||
<p><span class="caption">Figure 15-18: A reference cycle of lists <code>a</code> and <code>b</code>
|
||
pointing to each other</span></p>
|
||
<p>如果你注释掉最后的<code>println!</code>,Rust 会尝试打印出<code>a</code>指向<code>b</code>指向<code>a</code>这样的循环直到栈溢出。</p>
|
||
<p>观察最后一个<code>println!</code>之前的打印结果,就会发现在将<code>a</code>改变为指向<code>b</code>之后<code>a</code>和<code>b</code>的引用计数都是 2。在<code>main</code>的结尾,Rust 首先会尝试丢弃<code>b</code>,这会使<code>Rc</code>的引用计数减一,但是这个计数是 1 而不是 0,所以<code>Rc</code>在堆上的内存不会被丢弃。它只是会永远的停留在 1 上。这个特定例子中,程序立马就结束了,所以并不是一个问题,不过如果是一个更加复杂的程序,它在这个循环中分配了很多内存并占有很长时间,这就是个问题了。这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。</p>
|
||
<p>现在,如你所见,在 Rust 中创建引用循环是困难和繁琐的。但并不是不可能:避免引用循环这种形式的内存泄漏并不是 Rust 的保证之一。如果你有包含<code>Rc<T></code>的<code>RefCell<T></code>值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环。在列表 15-14 的例子中,可能解决方式就是不要编写像这样可能造成引用循环的代码,因为我们希望<code>Cons</code>成员拥有他们指向的列表。</p>
|
||
<p>举例来说,对于像图这样的数据结构,为了创建父节点指向子节点的边和以相反方向从子节点指向父节点的边,有时需要创建这样的引用循环。如果一个方向拥有所有权而另一个方向没有,对于模拟这种数据关系的一种不会创建引用循环和内存泄露的方式是使用<code>Weak<T></code>。接下来让我们探索一下!</p>
|
||
<a class="header" href="#避免引用循环将rct变为weakt" name="避免引用循环将rct变为weakt"><h3>避免引用循环:将<code>Rc<T></code>变为<code>Weak<T></code></h3></a>
|
||
<p>Rust 标准库中提供了<code>Weak<T></code>,一个用于存在引用循环但只有一个方向有所有权的智能指针。我们已经展示过如何克隆<code>Rc<T></code>来增加引用的<code>strong_count</code>;<code>Weak<T></code>是一种引用<code>Rc<T></code>但不增加<code>strong_count</code>的方式:相反它增加<code>Rc</code>引用的<code>weak_count</code>。当<code>Rc</code>离开作用域,其内部值会在<code>strong_count</code>为 0 的时候被丢弃,即便<code>weak_count</code>不为 0 。为了能够从<code>Weak<T></code>中获取值,首先需要使用<code>upgrade</code>方法将其升级为<code>Option<Rc<T>></code>。升级<code>Weak<T></code>的结果在<code>Rc</code>还未被丢弃时是<code>Some</code>,而在<code>Rc</code>被丢弃时是<code>None</code>。因为<code>upgrade</code>返回一个<code>Option</code>,我们知道 Rust 会确保<code>Some</code>和<code>None</code>的情况都被处理并不会尝试使用一个无效的指针。</p>
|
||
<p>不同于列表 15-17 中每个项只知道它的下一项,假如我们需要一个树,它的项知道它的子项<strong>和</strong>父项。</p>
|
||
<p>让我们从一个叫做<code>Node</code>的存放拥有所有权的<code>i32</code>值和其子<code>Node</code>值的引用的结构体开始:</p>
|
||
<pre><code class="language-rust">use std::rc::Rc;
|
||
use std::cell::RefCell;
|
||
|
||
#[derive(Debug)]
|
||
struct Node {
|
||
value: i32,
|
||
children: RefCell<Vec<Rc<Node>>>,
|
||
}
|
||
</code></pre>
|
||
<p>我们希望能够<code>Node</code>拥有其子节点,同时也希望变量可以拥有每个节点以便可以直接访问他们。这就是为什么<code>Vec</code>中的项是<code>Rc<Node></code>值。我们也希望能够修改其他节点的子节点,这就是为什么<code>children</code>中<code>Vec</code>被放进了<code>RefCell</code>的原因。在列表 15-19 中创建了一个叫做<code>leaf</code>的带有值 3 并没有子节点的<code>Node</code>实例,和另一个带有值 5 和以<code>leaf</code>作为子节点的实例<code>branch</code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let leaf = Rc::new(Node {
|
||
value: 3,
|
||
children: RefCell::new(vec![]),
|
||
});
|
||
|
||
let branch = Rc::new(Node {
|
||
value: 5,
|
||
children: RefCell::new(vec![leaf.clone()]),
|
||
});
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-19: Creating a <code>leaf</code> node and a <code>branch</code> node
|
||
where <code>branch</code> has <code>leaf</code> as one of its children but <code>leaf</code> has no reference to
|
||
<code>branch</code></span></p>
|
||
<p><code>leaf</code>中的<code>Node</code>现在有两个所有者:<code>leaf</code>和<code>branch</code>,因为我们克隆了<code>leaf</code>中的<code>Rc</code>并储存在了<code>branch</code>中。<code>branch</code>中的<code>Node</code>知道它与<code>leaf</code>相关联因为<code>branch</code>在<code>branch.children</code>中有<code>leaf</code>的引用。然而,<code>leaf</code>并不知道它与<code>branch</code>相关联,而我们希望<code>leaf</code>知道<code>branch</code>是其父节点。</p>
|
||
<p>为了做到这一点,需要在<code>Node</code>结构体定义中增加一个<code>parent</code>字段,不过<code>parent</code>的类型应该是什么呢?我们知道它不能包含<code>Rc<T></code>,因为这样<code>leaf.parent</code>将会指向<code>branch</code>而<code>branch.children</code>会包含<code>leaf</code>的指针,这会形成引用循环。<code>leaf</code>和<code>branch</code>不会被丢弃因为他们总是引用对方且引用计数永远也不会是零。</p>
|
||
<p>所以在<code>parent</code>的类型中是使用<code>Weak<T></code>而不是<code>Rc</code>,具体来说是<code>RefCell<Weak<Node>></code>:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::rc::{Rc, Weak};
|
||
use std::cell::RefCell;
|
||
|
||
#[derive(Debug)]
|
||
struct Node {
|
||
value: i32,
|
||
parent: RefCell<Weak<Node>>,
|
||
children: RefCell<Vec<Rc<Node>>>,
|
||
}
|
||
</code></pre>
|
||
<p>这样,一个节点就能够在拥有父节点时指向它,而并不拥有其父节点。一个父节点哪怕在拥有指向它的子节点也会被丢弃,只要是其自身也没有一个父节点就行。现在将<code>main</code>函数更新为如列表 15-20 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let leaf = Rc::new(Node {
|
||
value: 3,
|
||
parent: RefCell::new(Weak::new()),
|
||
children: RefCell::new(vec![]),
|
||
});
|
||
|
||
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
|
||
|
||
let branch = Rc::new(Node {
|
||
value: 5,
|
||
parent: RefCell::new(Weak::new()),
|
||
children: RefCell::new(vec![leaf.clone()]),
|
||
});
|
||
|
||
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
|
||
|
||
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-20: A <code>leaf</code> node and a <code>branch</code> node where
|
||
<code>leaf</code> has a <code>Weak</code> reference to its parent, <code>branch</code></span></p>
|
||
<p>创建<code>leaf</code>节点是类似的;因为它作为开始并没有父节点,这里创建了一个新的<code>Weak</code>引用实例。当尝试通过<code>upgrade</code>方法获取<code>leaf</code>父节点的引用时,会得到一个<code>None</code>值,如第一个<code>println!</code>输出所示:</p>
|
||
<pre><code class="language-=">leaf parent = None
|
||
</code></pre>
|
||
<p>类似的,<code>branch</code>也有一个新的<code>Weak</code>引用,因为也没有父节点。<code>leaf</code>仍然作为<code>branch</code>的一个子节点。一旦在<code>branch</code>中有了一个新的<code>Node</code>实例,就可以修改<code>leaf</code>将一个<code>branch</code>的<code>Weak</code>引用作为其父节点。这里使用了<code>leaf</code>中<code>parent</code>字段里的<code>RefCell</code>的<code>borrow_mut</code>方法,接着使用了<code>Rc::downgrade</code>函数来从<code>branch</code>中的<code>Rc</code>值创建了一个指向<code>branch</code>的<code>Weak</code>引用。</p>
|
||
<p>当再次打印出<code>leaf</code>的父节点时,这一次将会得到存放了<code>branch</code>的<code>Some</code>值。另外需要注意到这里并没有打印出类似列表 15-14 中那样最终导致栈溢出的循环:<code>Weak</code>引用仅仅打印出<code>(Weak)</code>:</p>
|
||
<pre><code>leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
|
||
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
|
||
children: RefCell { value: [] } }] } })
|
||
</code></pre>
|
||
<p>没有无限的输出(或直到栈溢出)的事实表明这里并没有引用循环。另一种证明的方式时观察调用<code>Rc::strong_count</code>和<code>Rc::weak_count</code>的值。在列表 15-21 中,创建了一个新的内部作用域并将<code>branch</code>的创建放入其中,这样可以观察<code>branch</code>被创建时和离开作用域被丢弃时发生了什么:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">fn main() {
|
||
let leaf = Rc::new(Node {
|
||
value: 3,
|
||
parent: RefCell::new(Weak::new()),
|
||
children: RefCell::new(vec![]),
|
||
});
|
||
|
||
println!(
|
||
"leaf strong = {}, weak = {}",
|
||
Rc::strong_count(&leaf),
|
||
Rc::weak_count(&leaf),
|
||
);
|
||
|
||
{
|
||
let branch = Rc::new(Node {
|
||
value: 5,
|
||
parent: RefCell::new(Weak::new()),
|
||
children: RefCell::new(vec![leaf.clone()]),
|
||
});
|
||
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
|
||
|
||
println!(
|
||
"branch strong = {}, weak = {}",
|
||
Rc::strong_count(&branch),
|
||
Rc::weak_count(&branch),
|
||
);
|
||
|
||
println!(
|
||
"leaf strong = {}, weak = {}",
|
||
Rc::strong_count(&leaf),
|
||
Rc::weak_count(&leaf),
|
||
);
|
||
}
|
||
|
||
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
|
||
println!(
|
||
"leaf strong = {}, weak = {}",
|
||
Rc::strong_count(&leaf),
|
||
Rc::weak_count(&leaf),
|
||
);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 15-21: Creating <code>branch</code> in an inner scope and
|
||
examining strong and weak reference counts of <code>leaf</code> and <code>branch</code></span></p>
|
||
<p>创建<code>leaf</code>之后,强引用计数是 1 (用于<code>leaf</code>自身)而弱引用计数是 0。在内部作用域中,在创建<code>branch</code>和关联<code>leaf</code>和<code>branch</code>之后,<code>branch</code>的强引用计数为 1(用于<code>branch</code>自身)而弱引用计数为 1(因为<code>leaf.parent</code>通过一个<code>Weak<T></code>指向<code>branch</code>)。<code>leaf</code>的强引用计数为 2,因为<code>branch</code>现在有一个<code>leaf</code>克隆的<code>Rc</code>储存在<code>branch.children</code>中。<code>leaf</code>的弱引用计数仍然为 0。</p>
|
||
<p>当内部作用域结束,<code>branch</code>离开作用域,其强引用计数减少为 0,所以其<code>Node</code>被丢弃。来自<code>leaf.parent</code>的弱引用计数 1 与<code>Node</code>是否被丢弃无关,所以并没有产生内存泄露!</p>
|
||
<p>如果在内部作用域结束后尝试访问<code>leaf</code>的父节点,会像<code>leaf</code>拥有父节点之前一样得到<code>None</code>值。在程序的末尾,<code>leaf</code>的强引用计数为 1 而弱引用计数为 0,因为现在<code>leaf</code>又是唯一指向其自己的值了。</p>
|
||
<p>所有这些管理计数和值是否应该被丢弃的逻辑都通过<code>Rc</code>和<code>Weak</code>和他们的<code>Drop</code> trait 实现来控制。通过在定义中指定从子节点到父节点的关系为一个<code>Weak<T></code>引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄露。</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>现在我们学习了如何选择不同类型的智能指针来选择不同的保证并与 Rust 的常规引用向取舍。<code>Box<T></code>有一个已知的大小并指向分配在堆上的数据。<code>Rc<T></code>记录了堆上数据的引用数量这样就可以拥有多个所有者。<code>RefCell<T></code>和其内部可变性使其可以用于需要不可变类型,但希望在运行时而不是编译时检查借用规则的场景。</p>
|
||
<p>我们还介绍了提供了很多智能指针功能的 trait <code>Deref</code>和<code>Drop</code>。同时探索了形成引用循环和造成内存泄漏的可能性,以及如何使用<code>Weak<T></code>避免引用循环。</p>
|
||
<p>如果本章内容引起了你的兴趣并希望现在就实现你自己的智能指针的话,请阅读 <a href="https://doc.rust-lang.org/stable/nomicon/">The Nomicon</a> 来获取更多有用的信息。</p>
|
||
<p>接下来,让我们谈谈 Rust 的并发。我们还会学习到一些新的对并发有帮助的智能指针。</p>
|
||
<a class="header" href="#无畏并发" name="无畏并发"><h1>无畏并发</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-00-concurrency.md">ch16-00-concurrency.md</a>
|
||
<br>
|
||
commit da15de39eaabd50100d6fa662c653169254d9175</p>
|
||
</blockquote>
|
||
<p>确保内存安全并不是 Rust 的唯一目标:更好的处理并发和并行编程一直是 Rust 的另一个主要目标。
|
||
<strong>并发编程</strong>(concurrent programming)代表程序的不同部分相互独立的执行,而<strong>并行编程</strong>代表程序不同部分同时执行,这两个概念在计算机拥有更多处理器可供程序利用时变得更加重要。由于历史的原因,在此类上下文中编程一直是困难且容易出错的:Rust 希望能改变这一点。</p>
|
||
<p>最开始,我们认为内存安全和防止并发问题是需要通过两个不同的方法解决的两个相互独立的挑战。然而,随着时间的推移,我们发现所有权和类型系统是一系列解决内存安全<strong>和</strong>并发问题的强用力的工具!通过改进所有权和类型检查,很多并发错误在 Rust 中都是<strong>编译时</strong>错误,而不是运行时错误。我们给 Rust 的这一部分起了一个绰号<strong>无畏并发</strong>(<em>fearless concurrency</em>)。无畏并发意味着 Rust 不光允许你自信代码不会出现诡异的错误,也让你可以轻易重构这种代码而无需担心会引入新的 bug。</p>
|
||
<blockquote>
|
||
<p>注意:对于 Rust 的口号<strong>无畏并发</strong>,这里用<strong>并发</strong>指代很多问题而不是更精确的区分<strong>并发和(或)并行</strong>,是出于简化问题的原因。如果这是一本专注于并发和/或并行的书,我们肯定会更精确的。对于本章,当我们谈到<strong>并发</strong>时,请自行替换为<strong>并发和(或)并行</strong>。</p>
|
||
</blockquote>
|
||
<p>很多语言所提供的处理并发问题的解决方法都非常有特色,尤其是对于更高级的语言,这是一个非常合理的策略。然而对于底层语言则没有奢侈的选择。在任何给定的情况下,我们都期望底层语言可以提供最高的性能,并且对硬件有更薄的抽象。因此,Rust 给了我们多种工具,并以适合实际情况和需求的方式来为问题建模。</p>
|
||
<p>如下是本章将要涉及到的内容:</p>
|
||
<ul>
|
||
<li>如何创建线程来同时运行多段代码。</li>
|
||
<li>并发<strong>消息传递</strong>(<em>Message passing</em>),其中通道(channel)被用来在线程间传递消息。</li>
|
||
<li>并发<strong>共享状态</strong>(<em>Shared state</em>),其中多个线程可以访问同一片数据。</li>
|
||
<li><code>Sync</code>和<code>Send</code> trait,他们允许 Rust 的并发保证能被扩展到用户定义的和标准库中提供的类型中。</li>
|
||
</ul>
|
||
<a class="header" href="#使用线程同时运行代码" name="使用线程同时运行代码"><h2>使用线程同时运行代码</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-01-threads.md">ch16-01-threads.md</a>
|
||
<br>
|
||
commit 55b294f20fc846a13a9be623bf322d8b364cee77</p>
|
||
</blockquote>
|
||
<p>在今天使用的大部分操作系统中,当程序执行时,操作系统运行代码的上下文称为<strong>进程</strong>(<em>process</em>)。操作系统可以运行很多进程,而操作系统也管理这些进程使得多个程序可以在电脑上同时运行。</p>
|
||
<p>我们可以将每个进程运行一个程序的概念再往下抽象一层:程序也可以在其上下文中同时运行独立的部分。这个功能叫做<strong>线程</strong>(<em>thread</em>)。</p>
|
||
<p>将程序需要执行的计算拆分到多个线程中可以提高性能,因为程序可以在同时进行很多工作。不过使用线程会增加程序复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这可能会由于线程以不一致的顺序访问数据或资源而导致竞争状态,或由于两个线程相互阻止对方继续运行而造成死锁,以及仅仅出现于特定场景并难以稳定重现的 bug。Rust 减少了这些或那些使用线程的负面影响,不过在多线程上下文中编程,相比只期望在单个线程中运行的程序,仍然要采用不同的思考方式和代码结构。</p>
|
||
<p>编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的 API。另外,很多编程语言提供了自己的特殊的线程实现。编程语言提供的线程有时被称作<strong>轻量级</strong>(<em>lightweight</em>)或<strong>绿色</strong>(<em>green</em>)线程。这些语言将一系列绿色线程放入不同数量的操作系统线程中执行。因为这个原因,语言调用操作系统 API 创建线程的模型有时被称为 <em>1:1</em>,一个 OS 线程对应一个语言线程。绿色线程模型被称为 <em>M:N</em> 模型,<code>M</code>个绿色线程对应<code>N</code>个 OS 线程,这里<code>M</code>和<code>N</code>不必相同。</p>
|
||
<p>每一个模型都有其自己的优势和取舍。对于 Rust 来说最重要的取舍是运行时支持。<strong>运行时</strong>是一个令人迷惑的概念;在不同上下文中它可能有不同的含义。这里其代表二进制文件中包含的语言自身的代码。对于一些语言,这些代码是庞大的,另一些则很小。通俗的说,“没有运行时”通常被人们用来指代“小运行时”,因为任何非汇编语言都存在一定数量的运行时。更小的运行时拥有更少的功能不过其优势在于更小的二进制输出。更小的二进制文件更容易在更多上下文中与其他语言结合。虽然很多语言觉得增加运行时来换取更多功能没有什么问题,但是 Rust 需要做到几乎没有运行时,同时为了保持高性能必需能够调用 C 语言,这点也是不能妥协的。</p>
|
||
<p>绿色线程模型功能要求更大的运行时来管理这些线程。为此,Rust 标准库只提供了 1:1 线程模型实现。因为 Rust 是这么一个底层语言,所以有相应的 crate 实现了 M:N 线程模型,如果你宁愿牺牲性能来换取例如更好的线程运行控制和更低的上下文切换成本。</p>
|
||
<p>现在我们明白了 Rust 中的线程是如何定义的,让我们开始探索如何使用标准库提供的线程相关的 API吧。</p>
|
||
<a class="header" href="#使用spawn创建新线程" name="使用spawn创建新线程"><h3>使用<code>spawn</code>创建新线程</h3></a>
|
||
<p>为了创建一个新线程,调用<code>thread::spawn</code>函数并传递一个闭包(第十三章学习了闭包),它包含希望在新线程运行的代码。列表 16-1 中的例子在新线程中打印了一些文本而其余的文本在主线程中打印:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::thread;
|
||
|
||
fn main() {
|
||
thread::spawn(|| {
|
||
for i in 1..10 {
|
||
println!("hi number {} from the spawned thread!", i);
|
||
}
|
||
});
|
||
|
||
for i in 1..5 {
|
||
println!("hi number {} from the main thread!", i);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-1: Creating a new thread to print one thing
|
||
while the main thread is printing something else</span></p>
|
||
<p>注意这个函数编写的方式,当主线程结束时,它也会停止新线程。这个程序的输出每次可能都略微不同,不过它大体上看起来像这样:</p>
|
||
<pre><code class="language-text">hi number 1 from the main thread!
|
||
hi number 1 from the spawned thread!
|
||
hi number 2 from the main thread!
|
||
hi number 2 from the spawned thread!
|
||
hi number 3 from the main thread!
|
||
hi number 3 from the spawned thread!
|
||
hi number 4 from the main thread!
|
||
hi number 4 from the spawned thread!
|
||
hi number 5 from the spawned thread!
|
||
</code></pre>
|
||
<p>这些线程可能会轮流运行,不过并不保证如此。在这里,主线程先行打印,即便新创建线程的打印语句位于程序的开头。甚至即便我们告诉新建的线程打印直到<code>i</code>等于 9 ,它在主线程结束之前也只打印到了 5。如果你只看到了一个线程,或没有出现重叠打印的现象,尝试增加 range 的数值来增加线程暂停并切换到其他线程运行的机会。</p>
|
||
<a class="header" href="#使用join等待所有线程结束" name="使用join等待所有线程结束"><h4>使用<code>join</code>等待所有线程结束</h4></a>
|
||
<p>由于主线程先于新建线程结束,不仅列表 16-1 中的代码大部分时候不能保证新建线程执行完毕,甚至不能实际保证新建线程会被执行!可以通过保存<code>thread::spawn</code>的返回值来解决这个问题,这是一个<code>JoinHandle</code>。这看起来如列表 16-2 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::thread;
|
||
|
||
fn main() {
|
||
let handle = thread::spawn(|| {
|
||
for i in 1..10 {
|
||
println!("hi number {} from the spawned thread!", i);
|
||
}
|
||
});
|
||
|
||
for i in 1..5 {
|
||
println!("hi number {} from the main thread!", i);
|
||
}
|
||
|
||
handle.join();
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-2: Saving a <code>JoinHandle</code> from <code>thread::spawn</code>
|
||
to guarantee the thread is run to completion</span></p>
|
||
<p><code>JoinHandle</code>是一个拥有所有权的值,它可以等待一个线程结束,这也正是<code>join</code>方法所做的。通过调用这个句柄的<code>join</code>,当前线程会阻塞直到句柄所代表的线程结束。因为我们将<code>join</code>调用放在了主线程的<code>for</code>循环之后,运行这个例子将产生类似这样的输出:</p>
|
||
<pre><code>hi number 1 from the main thread!
|
||
hi number 2 from the main thread!
|
||
hi number 1 from the spawned thread!
|
||
hi number 3 from the main thread!
|
||
hi number 2 from the spawned thread!
|
||
hi number 4 from the main thread!
|
||
hi number 3 from the spawned thread!
|
||
hi number 4 from the spawned thread!
|
||
hi number 5 from the spawned thread!
|
||
hi number 6 from the spawned thread!
|
||
hi number 7 from the spawned thread!
|
||
hi number 8 from the spawned thread!
|
||
hi number 9 from the spawned thread!
|
||
</code></pre>
|
||
<p>这两个线程仍然会交替执行,不过主线程会由于<code>handle.join()</code>调用会等待直到新建线程执行完毕。</p>
|
||
<p>如果将<code>handle.join()</code>放在主线程的<code>for</code>循环之前,像这样:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::thread;
|
||
|
||
fn main() {
|
||
let handle = thread::spawn(|| {
|
||
for i in 1..10 {
|
||
println!("hi number {} from the spawned thread!", i);
|
||
}
|
||
});
|
||
|
||
handle.join();
|
||
|
||
for i in 1..5 {
|
||
println!("hi number {} from the main thread!", i);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>主线程会等待直到新建线程执行完毕之后才开始执行<code>for</code>循环,所以输出将不会交替出现:</p>
|
||
<pre><code>hi number 1 from the spawned thread!
|
||
hi number 2 from the spawned thread!
|
||
hi number 3 from the spawned thread!
|
||
hi number 4 from the spawned thread!
|
||
hi number 5 from the spawned thread!
|
||
hi number 6 from the spawned thread!
|
||
hi number 7 from the spawned thread!
|
||
hi number 8 from the spawned thread!
|
||
hi number 9 from the spawned thread!
|
||
hi number 1 from the main thread!
|
||
hi number 2 from the main thread!
|
||
hi number 3 from the main thread!
|
||
hi number 4 from the main thread!
|
||
</code></pre>
|
||
<p>稍微考虑一下将<code>join</code>放置与何处会影响线程是否同时运行。</p>
|
||
<a class="header" href="#线程和move闭包" name="线程和move闭包"><h3>线程和<code>move</code>闭包</h3></a>
|
||
<p>第十三章有一个我们没有讲到的闭包功能,它经常用于<code>thread::spawn</code>:<code>move</code>闭包。第十三章中讲到:</p>
|
||
<blockquote>
|
||
<p>获取他们环境中值的闭包主要用于开始新线程的场景</p>
|
||
</blockquote>
|
||
<p>现在我们正在创建新线程,所以让我们讨论一下获取环境值的闭包吧!</p>
|
||
<p>注意列表 16-1 中传递给<code>thread::spawn</code>的闭包并没有任何参数:并没有在新建线程代码中使用任何主线程的数据。为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值。列表 16-3 展示了一个尝试在主线程中创建一个 vector 并用于新建线程的例子,不过这么写还不能工作:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">use std::thread;
|
||
|
||
fn main() {
|
||
let v = vec![1, 2, 3];
|
||
|
||
let handle = thread::spawn(|| {
|
||
println!("Here's a vector: {:?}", v);
|
||
});
|
||
|
||
handle.join();
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-3: Attempting to use a vector created by the
|
||
main thread from another thread</span></p>
|
||
<p>闭包使用了<code>v</code>,所以闭包会获取<code>v</code>并使其成为闭包环境的一部分。因为<code>thread::spawn</code>在一个新线程中运行这个闭包,所以可以在新线程中访问<code>v</code>。</p>
|
||
<p>然而当编译这个例子时,会得到如下错误:</p>
|
||
<pre><code>error[E0373]: closure may outlive the current function, but it borrows `v`,
|
||
which is owned by the current function
|
||
-->
|
||
|
|
||
6 | let handle = thread::spawn(|| {
|
||
| ^^ may outlive borrowed value `v`
|
||
7 | println!("Here's a vector: {:?}", v);
|
||
| - `v` is borrowed here
|
||
|
|
||
help: to force the closure to take ownership of `v` (and any other referenced
|
||
variables), use the `move` keyword, as shown:
|
||
| let handle = thread::spawn(move || {
|
||
</code></pre>
|
||
<p>当在闭包环境中获取某些值时,Rust 会尝试推断如何获取它。<code>println!</code>只需要<code>v</code>的一个引用,所以闭包尝试借用<code>v</code>。但是这有一个问题:我们并不知道新建线程会运行多久,所以无法知道<code>v</code>是否一直时有效的。</p>
|
||
<p>考虑一下列表 16-4 中的代码,它展示了一个<code>v</code>的引用很有可能不再有效的场景:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">use std::thread;
|
||
|
||
fn main() {
|
||
let v = vec![1, 2, 3];
|
||
|
||
let handle = thread::spawn(|| {
|
||
println!("Here's a vector: {:?}", v);
|
||
});
|
||
|
||
drop(v); // oh no!
|
||
|
||
handle.join();
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-4: A thread with a closure that attempts to
|
||
capture a reference to <code>v</code> from a main thread that drops <code>v</code></span></p>
|
||
<p>这些代码可以运行,而新建线程则可能直接就出错了并完全没有机会运行。新建线程内部有一个<code>v</code>的引用,不过主线程仍在执行:它立刻丢弃了<code>v</code>,使用了第十五章提到的显式丢弃其参数的<code>drop</code>函数。接着,新建线程开始执行,现在<code>v</code>是无效的了,所以它的引用也就是无效的。噢,这太糟了!</p>
|
||
<p>为了修复这个问题,我们可以听取错误信息的建议:</p>
|
||
<pre><code>help: to force the closure to take ownership of `v` (and any other referenced
|
||
variables), use the `move` keyword, as shown:
|
||
| let handle = thread::spawn(move || {
|
||
</code></pre>
|
||
<p>通过在闭包之前增加<code>move</code>关键字,我们强制闭包获取它使用的值的所有权,而不是引用借用。列表 16-5 中展示的对列表 16-3 代码的修改可以按照我们的预期编译并运行:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::thread;
|
||
|
||
fn main() {
|
||
let v = vec![1, 2, 3];
|
||
|
||
let handle = thread::spawn(move || {
|
||
println!("Here's a vector: {:?}", v);
|
||
});
|
||
|
||
handle.join();
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-5: Using the <code>move</code> keyword to force a closure
|
||
to take ownership of the values it uses</span></p>
|
||
<p>那么列表 16-4 中那个主线程调用了<code>drop</code>的代码该怎么办呢?如果在闭包上增加了<code>move</code>,就将<code>v</code>移动到了闭包的环境中,我们将不能对其调用<code>drop</code>了。相反会出现这个编译时错误:</p>
|
||
<pre><code>error[E0382]: use of moved value: `v`
|
||
-->
|
||
|
|
||
6 | let handle = thread::spawn(move || {
|
||
| ------- value moved (into closure) here
|
||
...
|
||
10 | drop(v); // oh no!
|
||
| ^ value used here after move
|
||
|
|
||
= note: move occurs because `v` has type `std::vec::Vec<i32>`, which does
|
||
not implement the `Copy` trait
|
||
</code></pre>
|
||
<p>Rust 的所有权规则又一次帮助了我们!</p>
|
||
<p>现在我们有一个线程和线程 API 的基本了解,让我们讨论一下使用线程实际可以<strong>做</strong>什么吧。</p>
|
||
<a class="header" href="#使用消息传递在线程间传送数据" name="使用消息传递在线程间传送数据"><h2>使用消息传递在线程间传送数据</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-02-message-passing.md">ch16-02-message-passing.md</a>
|
||
<br>
|
||
commit da15de39eaabd50100d6fa662c653169254d9175</p>
|
||
</blockquote>
|
||
<p>最近人气正在上升的一个并发方式是<strong>消息传递</strong>(<em>message passing</em>),这里线程或 actor 通过发送包含数据的消息来沟通。这个思想来源于口号:</p>
|
||
<blockquote>
|
||
<p>Do not communicate by sharing memory; instead, share memory by
|
||
communicating.</p>
|
||
<p>不要共享内存来通讯;而是要通讯来共享内存。</p>
|
||
<p>--<a href="http://golang.org/doc/effective_go.html">Effective Go</a></p>
|
||
</blockquote>
|
||
<p>实现这个目标的主要工具是<strong>通道</strong>(<em>channel</em>)。通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。代码的一部分可以调用发送者和想要发送的数据,而另一部分代码可以在接收的那一端收取消息。</p>
|
||
<p>我们将编写一个例子使用一个线程生成值并向通道发送他们。主线程会接收这些值并打印出来。</p>
|
||
<p>首先,如列表 16-6 所示,先创建一个通道但不做任何事:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::sync::mpsc;
|
||
|
||
fn main() {
|
||
let (tx, rx) = mpsc::channel();
|
||
# tx.send(()).unwrap();
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-6: Creating a channel and assigning the two
|
||
halves to <code>tx</code> and <code>rx</code></span></p>
|
||
<p><code>mpsc::channel</code>函数创建一个新的通道。<code>mpsc</code>是<strong>多个生产者,单个消费者</strong>(<em>multiple producer, single consumer</em>)的缩写。简而言之,可以有多个产生值的<strong>发送端</strong>,但只能有一个消费这些值的<strong>接收端</strong>。现在我们以一个单独的生产者开始,不过一旦例子可以工作了就会增加多个生产者。</p>
|
||
<p><code>mpsc::channel</code>返回一个元组:第一个元素是发送端,而第二个元素是接收端。由于历史原因,很多人使用<code>tx</code>和<code>rx</code>作为<strong>发送者</strong>和<strong>接收者</strong>的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个<code>let</code>语句和模式来解构了元组。第十八章会讨论<code>let</code>语句中的模式和解构。</p>
|
||
<p>让我们将发送端移动到一个新建线程中并发送一个字符串,如列表 16-7 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::thread;
|
||
use std::sync::mpsc;
|
||
|
||
fn main() {
|
||
let (tx, rx) = mpsc::channel();
|
||
|
||
thread::spawn(move || {
|
||
let val = String::from("hi");
|
||
tx.send(val).unwrap();
|
||
});
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-7: Moving <code>tx</code> to a spawned thread and sending
|
||
"hi"</span></p>
|
||
<p>正如上一部分那样使用<code>thread::spawn</code>来创建一个新线程。并使用一个<code>move</code>闭包来将<code>tx</code>移动进闭包这样新建线程就是其所有者。</p>
|
||
<p>通道的发送端有一个<code>send</code>方法用来获取需要放入通道的值。<code>send</code>方法返回一个<code>Result<T, E></code>类型,因为如果接收端被丢弃了,将没有发送值的目标,所以发送操作会出错。在这个例子中,我们简单的调用<code>unwrap</code>来忽略错误,不过对于一个真实程序,需要合理的处理它。第九章是你复习正确错误处理策略的好地方。</p>
|
||
<p>在列表 16-8 中,让我们在主线程中从通道的接收端获取值:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::thread;
|
||
use std::sync::mpsc;
|
||
|
||
fn main() {
|
||
let (tx, rx) = mpsc::channel();
|
||
|
||
thread::spawn(move || {
|
||
let val = String::from("hi");
|
||
tx.send(val).unwrap();
|
||
});
|
||
|
||
let received = rx.recv().unwrap();
|
||
println!("Got: {}", received);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-8: Receiving the value "hi" in the main thread
|
||
and printing it out</span></p>
|
||
<p>通道的接收端有两个有用的方法:<code>recv</code>和<code>try_recv</code>。这里,我们使用了<code>recv</code>,它是 <em>receive</em> 的缩写。这个方法会阻塞执行直到从通道中接收一个值。一旦发送了一个值,<code>recv</code>会在一个<code>Result<T, E></code>中返回它。当通道发送端关闭,<code>recv</code>会返回一个错误。<code>try_recv</code>不会阻塞;相反它立刻返回一个<code>Result<T, E></code>。</p>
|
||
<p>如果运行列表 16-8 中的代码,我们将会看到主线程打印出这个值:</p>
|
||
<pre><code>Got: hi
|
||
</code></pre>
|
||
<a class="header" href="#通道与所有权如何交互" name="通道与所有权如何交互"><h3>通道与所有权如何交互</h3></a>
|
||
<p>现在让我们做一个试验来看看通道与所有权如何在一起工作:我们将尝试在新建线程中的通道中发送完<code>val</code>之后再使用它。尝试编译列表 16-9 中的代码:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">use std::thread;
|
||
use std::sync::mpsc;
|
||
|
||
fn main() {
|
||
let (tx, rx) = mpsc::channel();
|
||
|
||
thread::spawn(move || {
|
||
let val = String::from("hi");
|
||
tx.send(val).unwrap();
|
||
println!("val is {}", val);
|
||
});
|
||
|
||
let received = rx.recv().unwrap();
|
||
println!("Got: {}", received);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-9: Attempting to use <code>val</code> after we have sent
|
||
it down the channel</span></p>
|
||
<p>这里尝试在通过<code>tx.send</code>发送<code>val</code>到通道中之后将其打印出来。这是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们在此使用它之前就修改或者丢弃它。这会由于不一致或不存在的数据而导致错误或意外的结果。</p>
|
||
<p>尝试编译这些代码,Rust 会报错:</p>
|
||
<pre><code>error[E0382]: use of moved value: `val`
|
||
--> src/main.rs:10:31
|
||
|
|
||
9 | tx.send(val).unwrap();
|
||
| --- value moved here
|
||
10 | println!("val is {}", val);
|
||
| ^^^ value used here after move
|
||
|
|
||
= note: move occurs because `val` has type `std::string::String`, which does
|
||
not implement the `Copy` trait
|
||
</code></pre>
|
||
<p>我们的并发错误会造成一个编译时错误!<code>send</code>获取其参数的所有权并移动这个值归接收者所有。这个意味着不可能意外的在发送后再次使用这个值;所有权系统检查一切是否合乎规则。</p>
|
||
<p>在这一点上,消息传递非常类似于 Rust 的单所有权系统。消息传递的拥护者出于相似的原因支持消息传递,就像 Rustacean 们欣赏 Rust 的所有权一样:单所有权意味着特定类型问题的消失。如果一次只有一个线程可以使用某些内存,就没有出现数据竞争的机会。</p>
|
||
<a class="header" href="#发送多个值并观察接收者的等待" name="发送多个值并观察接收者的等待"><h3>发送多个值并观察接收者的等待</h3></a>
|
||
<p>列表 16-8 中的代码可以编译和运行,不过这并不是很有趣:通过它难以看出两个独立的线程在一个通道上相互通讯。列表 16-10 则有一些改进会证明这些代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂停一段时间。</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::thread;
|
||
use std::sync::mpsc;
|
||
use std::time::Duration;
|
||
|
||
fn main() {
|
||
let (tx, rx) = mpsc::channel();
|
||
|
||
thread::spawn(move || {
|
||
let vals = vec![
|
||
String::from("hi"),
|
||
String::from("from"),
|
||
String::from("the"),
|
||
String::from("thread"),
|
||
];
|
||
|
||
for val in vals {
|
||
tx.send(val).unwrap();
|
||
thread::sleep(Duration::new(1, 0));
|
||
}
|
||
});
|
||
|
||
for received in rx {
|
||
println!("Got: {}", received);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-10: Sending multiple messages and pausing
|
||
between each one</span></p>
|
||
<p>这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历他们,单独的发送每一个字符串并通过一个<code>Duration</code>值调用<code>thread::sleep</code>函数来暂停一秒。</p>
|
||
<p>在主线程中,不再显式的调用<code>recv</code>函数:而是将<code>rx</code>当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当通道被关闭时,迭代器也将结束。</p>
|
||
<p>当运行列表 16-10 中的代码时,将看到如下输出,每一行都会暂停一秒:</p>
|
||
<pre><code>Got: hi
|
||
Got: from
|
||
Got: the
|
||
Got: thread
|
||
</code></pre>
|
||
<p>在主线程中并没有任何暂停或位于<code>for</code>循环中用于等待的代码,所以可以说主线程是在等待从新建线程中接收值。</p>
|
||
<a class="header" href="#通过克隆发送者来创建多个生产者" name="通过克隆发送者来创建多个生产者"><h3>通过克隆发送者来创建多个生产者</h3></a>
|
||
<p>差不多在本部分的开头,我们提到了<code>mpsc</code>是 <em>multiple producer, single consumer</em> 的缩写。可以扩展列表 16-11 中的代码来创建都向同一接收者发送值的多个线程。这可以通过克隆通道的发送端在来做到,如列表 16-11 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust"># use std::thread;
|
||
# use std::sync::mpsc;
|
||
# use std::time::Duration;
|
||
#
|
||
# fn main() {
|
||
// ...snip...
|
||
let (tx, rx) = mpsc::channel();
|
||
|
||
let tx1 = tx.clone();
|
||
thread::spawn(move || {
|
||
let vals = vec![
|
||
String::from("hi"),
|
||
String::from("from"),
|
||
String::from("the"),
|
||
String::from("thread"),
|
||
];
|
||
|
||
for val in vals {
|
||
tx1.send(val).unwrap();
|
||
thread::sleep(Duration::new(1, 0));
|
||
}
|
||
});
|
||
|
||
thread::spawn(move || {
|
||
let vals = vec![
|
||
String::from("more"),
|
||
String::from("messages"),
|
||
String::from("for"),
|
||
String::from("you"),
|
||
];
|
||
|
||
for val in vals {
|
||
tx.send(val).unwrap();
|
||
thread::sleep(Duration::new(1, 0));
|
||
}
|
||
});
|
||
// ...snip...
|
||
#
|
||
# for received in rx {
|
||
# println!("Got: {}", received);
|
||
# }
|
||
# }
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-11: Sending multiple messages and pausing
|
||
between each one</span></p>
|
||
<p>这一次,在创建新线程之前,我们对通道的发送端调用了<code>clone</code>方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的通道发送端传递给第二个新建线程,这样每个线程将向通道的接收端发送不同的消息。</p>
|
||
<p>如果运行这些代码,你<strong>可能</strong>会看到这样的输出:</p>
|
||
<pre><code>Got: hi
|
||
Got: more
|
||
Got: from
|
||
Got: messages
|
||
Got: for
|
||
Got: the
|
||
Got: thread
|
||
Got: you
|
||
</code></pre>
|
||
<p>虽然你可能会看到这些以不同的顺序出现。这依赖于你的系统!这也就是并发既有趣又困难的原因。如果你拿<code>thread::sleep</code>做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定并每次都会产生不同的输出。</p>
|
||
<p>现在我们见识过了通道如何工作,再看看共享内存并发吧。</p>
|
||
<a class="header" href="#共享状态并发" name="共享状态并发"><h2>共享状态并发</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-03-shared-state.md">ch16-03-shared-state.md</a>
|
||
<br>
|
||
commit 9df612e93e038b05fc959db393c15a5402033f47</p>
|
||
</blockquote>
|
||
<p>虽然消息传递是一个很好的处理并发的方式,但并不是唯一的一个。再次考虑一下它的口号:</p>
|
||
<blockquote>
|
||
<p>Do not communicate by sharing memory; instead, share memory by
|
||
communicating.</p>
|
||
<p>不要共享内存来通讯;而是要通讯来共享内存。</p>
|
||
</blockquote>
|
||
<p>那么“共享内存来通讯”是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。</p>
|
||
<p>不过 Rust 的类型系统和所有权可以很好的帮助我们,正确的管理它们。以共享内存中更常见的并发原语:互斥器(mutexes)为例,让我们看看具体的情况。</p>
|
||
<a class="header" href="#互斥器一次只允许一个线程访问数据" name="互斥器一次只允许一个线程访问数据"><h3>互斥器一次只允许一个线程访问数据</h3></a>
|
||
<p><strong>互斥器</strong>(<em>mutex</em>)是一种用于共享内存的并发原语。它是“mutual exclusion”的缩写,也就是说,任意时间,它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住:</p>
|
||
<ol>
|
||
<li>在使用数据之前尝试获取锁。</li>
|
||
<li>处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。</li>
|
||
</ol>
|
||
<p>现实中也有互斥器的例子,想象一下在一个会议中,只有一个麦克风。如果一个成员要发言,他必须请求使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一个希望讲话的成员。如果成员在没有麦克风的时候就开始叫喊,或者在其他成员发言结束之前就拿走麦克风,是很不合适的。如果这个共享的麦克风因为此类原因而出现问题,会议将无法正常进行。</p>
|
||
<p>正确的管理互斥器异常复杂,这也是许多人之所以热衷于通道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。</p>
|
||
<a class="header" href="#mutext的-api" name="mutext的-api"><h3><code>Mutex<T></code>的 API</h3></a>
|
||
<p>让我们看看列表 16-12 中使用互斥器的例子,现在不涉及多线程:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::sync::Mutex;
|
||
|
||
fn main() {
|
||
let m = Mutex::new(5);
|
||
|
||
{
|
||
let mut num = m.lock().unwrap();
|
||
*num = 6;
|
||
}
|
||
|
||
println!("m = {:?}", m);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-12: Exploring the API of <code>Mutex<T></code> in a
|
||
single threaded context for simplicity</span></p>
|
||
<p>像很多类型一样,我们使用关联函数 <code>new</code> 来创建一个 <code>Mutex<T></code>。使用<code>lock</code>方法获取锁,以访问互斥器中的数据。这个调用会阻塞,直到我们拥有锁为止。如果另一个线程拥有锁,并且那个线程 panic 了,则这个调用会失败。类似于列表 16-6 那样,我们暂时使用 <code>unwrap()</code> 进行错误处理,或者使用第九章中提及的更好的工具。</p>
|
||
<p>一旦获取了锁,就可以将返回值(在这里是<code>num</code>)作为一个数据的可变引用使用了。观察 Rust 类型系统如何保证使用值之前必须获取锁:<code>Mutex<i32></code>并不是一个<code>i32</code>,所以<strong>必须</strong>获取锁才能使用这个<code>i32</code>值。我们是不会忘记这么做的,因为类型系统不允许。</p>
|
||
<p>你也许会怀疑,<code>Mutex<T></code>是一个智能指针?是的!更准确的说,<code>lock</code>调用返回一个叫做<code>MutexGuard</code>的智能指针。类似我们在第十五章见过的智能指针,它实现了<code>Deref</code>来指向其内部数据。另外<code>MutexGuard</code>有一个用来释放锁的<code>Drop</code>实现。这样就不会忘记释放锁了。这在<code>MutexGuard</code>离开作用域时会自动发生,例如它发生于列表 16-12 中内部作用域的结尾。接着可以打印出互斥器的值并发现能够将其内部的<code>i32</code>改为 6。</p>
|
||
<a class="header" href="#在线程间共享mutext" name="在线程间共享mutext"><h4>在线程间共享<code>Mutex<T></code></h4></a>
|
||
<p>现在让我们尝试使用<code>Mutex<T></code>在多个线程间共享值。我们将启动十个线程,并在各个线程中对同一个计数器值加一,这样计数器将从 0 变为 10。注意,接下来的几个例子会出现编译错误,而我们将通过这些错误来学习如何使用
|
||
<code>Mutex<T></code>,以及 Rust 又是如何辅助我们以确保正确。列表 16-13 是最开始的例子:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">use std::sync::Mutex;
|
||
use std::thread;
|
||
|
||
fn main() {
|
||
let counter = Mutex::new(0);
|
||
let mut handles = vec![];
|
||
|
||
for _ in 0..10 {
|
||
let handle = thread::spawn(|| {
|
||
let mut num = counter.lock().unwrap();
|
||
|
||
*num += 1;
|
||
});
|
||
handles.push(handle);
|
||
}
|
||
|
||
for handle in handles {
|
||
handle.join().unwrap();
|
||
}
|
||
|
||
println!("Result: {}", *counter.lock().unwrap());
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-13: The start of a program having 10 threads
|
||
each increment a counter guarded by a <code>Mutex<T></code></span></p>
|
||
<p>这里创建了一个 <code>counter</code> 变量来存放内含 <code>i32</code> 的 <code>Mutex<T></code>,类似列表 16-12 那样。接下来使用 range 创建了 10 个线程。使用了 <code>thread::spawn</code> 并对所有线程使用了相同的闭包:他们每一个都将调用 <code>lock</code> 方法来获取 <code>Mutex<T></code> 上的锁,接着将互斥器中的值加一。当一个线程结束执行,<code>num</code> 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。</p>
|
||
<p>在主线程中,我们像列表 16-2 那样收集了所有的 join 句柄,调用它们的 <code>join</code> 方法来确保所有线程都会结束。之后,主线程会获取锁并打印出程序的结果。</p>
|
||
<p>之前提示过这个例子不能编译,让我们看看为什么!</p>
|
||
<pre><code>error[E0373]: closure may outlive the current function, but it borrows
|
||
`counter`, which is owned by the current function
|
||
-->
|
||
|
|
||
9 | let handle = thread::spawn(|| {
|
||
| ^^ may outlive borrowed value `counter`
|
||
10 | let mut num = counter.lock().unwrap();
|
||
| ------- `counter` is borrowed here
|
||
|
|
||
help: to force the closure to take ownership of `counter` (and any other
|
||
referenced variables), use the `move` keyword, as shown:
|
||
| let handle = thread::spawn(move || {
|
||
</code></pre>
|
||
<p>这类似于列表 16-5 中解决了的问题。考虑到启动了多个线程,Rust 无法知道这些线程会运行多久,而在每一个线程尝试借用 <code>counter</code> 时它是否仍然有效。帮助信息提醒了我们如何解决它:可以使用 <code>move</code> 来给予每个线程其所有权。尝试在闭包上做一点改动:</p>
|
||
<pre><code class="language-rust,ignore">thread::spawn(move || {
|
||
</code></pre>
|
||
<p>再次编译。这回出现了一个不同的错误!</p>
|
||
<pre><code>error[E0382]: capture of moved value: `counter`
|
||
-->
|
||
|
|
||
9 | let handle = thread::spawn(move || {
|
||
| ------- value moved (into closure) here
|
||
10 | let mut num = counter.lock().unwrap();
|
||
| ^^^^^^^ value captured here after move
|
||
|
|
||
= note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
|
||
which does not implement the `Copy` trait
|
||
|
||
error[E0382]: use of moved value: `counter`
|
||
-->
|
||
|
|
||
9 | let handle = thread::spawn(move || {
|
||
| ------- value moved (into closure) here
|
||
...
|
||
21 | println!("Result: {}", *counter.lock().unwrap());
|
||
| ^^^^^^^ value used here after move
|
||
|
|
||
= note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
|
||
which does not implement the `Copy` trait
|
||
|
||
error: aborting due to 2 previous errors
|
||
</code></pre>
|
||
<p><code>move</code> 并没有像列表 16-5 中那样解决问题。为什么呢?错误信息有点难懂,因为它表明 <code>counter</code> 被移动进了闭包,接着它在调用 <code>lock</code> 时被捕获。这似乎是我们希望的,然而不被允许。</p>
|
||
<p>让我们推理一下。这次不再使用 <code>for</code> 循环创建 10 个线程,只创建两个线程,看看会发生什么。将列表 16-13 中第一个<code>for</code>循环替换为如下代码:</p>
|
||
<pre><code class="language-rust,ignore">let handle = thread::spawn(move || {
|
||
let mut num = counter.lock().unwrap();
|
||
|
||
*num += 1;
|
||
});
|
||
handles.push(handle);
|
||
|
||
let handle2 = thread::spawn(move || {
|
||
let mut num2 = counter.lock().unwrap();
|
||
|
||
*num2 += 1;
|
||
});
|
||
handles.push(handle2);
|
||
</code></pre>
|
||
<p>这里创建了两个线程,并将第二个线程所用的变量改名为 <code>handle2</code> 和 <code>num2</code>。我们简化了例子,看是否能理解错误信息。此次编译给出如下信息:</p>
|
||
<pre><code class="language-text">error[E0382]: capture of moved value: `counter`
|
||
-->
|
||
|
|
||
8 | let handle = thread::spawn(move || {
|
||
| ------- value moved (into closure) here
|
||
...
|
||
16 | let mut num = counter.lock().unwrap();
|
||
| ^^^^^^^ value captured here after move
|
||
|
|
||
= note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
|
||
which does not implement the `Copy` trait
|
||
|
||
error[E0382]: use of moved value: `counter`
|
||
-->
|
||
|
|
||
8 | let handle = thread::spawn(move || {
|
||
| ------- value moved (into closure) here
|
||
...
|
||
26 | println!("Result: {}", *counter.lock().unwrap());
|
||
| ^^^^^^^ value used here after move
|
||
|
|
||
= note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
|
||
which does not implement the `Copy` trait
|
||
|
||
error: aborting due to 2 previous errors
|
||
</code></pre>
|
||
<p>啊哈!第一个错误信息中说,<code>counter</code> 被移动进了 <code>handle</code> 所代表线程的闭包中。因此我们无法在第二个线程中对其调用 <code>lock</code>,并将结果储存在 <code>num2</code> 中时捕获<code>counter</code>!所以 Rust 告诉我们不能将 <code>counter</code> 的所有权移动到多个线程中。这在之前很难看出,因为我们在循环中创建了多个线程,而 Rust 无法在每次迭代中指明不同的线程(没有临时变量 <code>num2</code>)。</p>
|
||
<a class="header" href="#多线程和多所有权" name="多线程和多所有权"><h4>多线程和多所有权</h4></a>
|
||
<p>在第十五章中,我们通过使用智能指针 <code>Rc<T></code> 来创建引用计数的值,以便拥有多所有权。同时第十五章提到了 <code>Rc<T></code> 只能在单线程环境中使用,不过还是在这里试用 <code>Rc<T></code> 看看会发生什么。列表 16-14 将 <code>Mutex<T></code> 装进了 <code>Rc<T></code> 中,并在移入线程之前克隆了 <code>Rc<T></code>。再用循环来创建线程,保留闭包中的 <code>move</code> 关键字:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::rc::Rc;
|
||
use std::sync::Mutex;
|
||
use std::thread;
|
||
|
||
fn main() {
|
||
let counter = Rc::new(Mutex::new(0));
|
||
let mut handles = vec![];
|
||
|
||
for _ in 0..10 {
|
||
let counter = counter.clone();
|
||
let handle = thread::spawn(move || {
|
||
let mut num = counter.lock().unwrap();
|
||
|
||
*num += 1;
|
||
});
|
||
handles.push(handle);
|
||
}
|
||
|
||
for handle in handles {
|
||
handle.join().unwrap();
|
||
}
|
||
|
||
println!("Result: {}", *counter.lock().unwrap());
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-14: Attempting to use <code>Rc<T></code> to allow
|
||
multiple threads to own the <code>Mutex<T></code></span></p>
|
||
<p>再一次编译并...出现了不同的错误!编译器真是教会了我们很多!</p>
|
||
<pre><code>error[E0277]: the trait bound `std::rc::Rc<std::sync::Mutex<i32>>:
|
||
std::marker::Send` is not satisfied
|
||
-->
|
||
|
|
||
11 | let handle = thread::spawn(move || {
|
||
| ^^^^^^^^^^^^^ the trait `std::marker::Send` is not
|
||
implemented for `std::rc::Rc<std::sync::Mutex<i32>>`
|
||
|
|
||
= note: `std::rc::Rc<std::sync::Mutex<i32>>` cannot be sent between threads
|
||
safely
|
||
= note: required because it appears within the type
|
||
`[closure@src/main.rs:11:36: 15:10
|
||
counter:std::rc::Rc<std::sync::Mutex<i32>>]`
|
||
= note: required by `std::thread::spawn`
|
||
</code></pre>
|
||
<p>哇哦,太长不看!说重点:第一个提示表明 <code>Rc<Mutex<i32>></code> 不能安全的在线程间传递。理由也在错误信息中,“不满足 <code>Send</code> trait bound”(<code>the trait bound Send is not satisfied</code>)。下一部分将会讨论 <code>Send</code>,它是确保许多用在多线程中的类型,能够适合并发环境的 trait 之一。</p>
|
||
<p>不幸的是,<code>Rc<T></code> 并不能安全的在线程间共享。当 <code>Rc<T></code> 管理引用计数时,它必须在每一个 <code>clone</code> 调用时增加计数,并在每一个克隆被丢弃时减少计数。<code>Rc<T></code> 并没有使用任何并发原语,来确保改变计数的操作不会被其他线程打断。在计数出错时可能会导致诡异的 bug,比如可能会造成内存泄漏,或在使用结束之前就丢弃一个值。如果有一个类型与 <code>Rc<T></code> 相似,又以一种线程安全的方式改变引用计数,会怎么样呢?</p>
|
||
<a class="header" href="#原子引用计数-arct" name="原子引用计数-arct"><h4>原子引用计数 <code>Arc<T></code></h4></a>
|
||
<p>答案是肯定的,确实有一个类似<code>Rc<T></code>并可以安全的用于并发环境的类型:<code>Arc<T></code>。字母“a”代表<strong>原子性</strong>(<em>atomic</em>),所以这是一个<strong>原子引用计数</strong>(<em>atomically reference counted</em>)类型。原子性是另一类这里还未涉及到的并发原语;请查看标准库中<code>std::sync::atomic</code>的文档来获取更多细节。其中的要点就是:原子性类型工作起来类似原始类型,不过可以安全的在线程间共享。</p>
|
||
<p>为什么不是所有的原始类型都是原子性的?为什么不是所有标准库中的类型都默认使用<code>Arc<T></code>实现?线程安全带来性能惩罚,我们希望只在必要时才为此买单。如果只是在单线程中对值进行操作,原子性提供的保证并无必要,代码可以因此运行的更快。</p>
|
||
<p>回到之前的例子:<code>Arc<T></code>和<code>Rc<T></code>除了<code>Arc<T></code>内部的原子性之外没有区别。其 API 也相同,所以可以修改<code>use</code>行和<code>new</code>调用。列表 16-15 中的代码最终可以编译和运行:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use std::sync::{Mutex, Arc};
|
||
use std::thread;
|
||
|
||
fn main() {
|
||
let counter = Arc::new(Mutex::new(0));
|
||
let mut handles = vec![];
|
||
|
||
for _ in 0..10 {
|
||
let counter = counter.clone();
|
||
let handle = thread::spawn(move || {
|
||
let mut num = counter.lock().unwrap();
|
||
|
||
*num += 1;
|
||
});
|
||
handles.push(handle);
|
||
}
|
||
|
||
for handle in handles {
|
||
handle.join().unwrap();
|
||
}
|
||
|
||
println!("Result: {}", *counter.lock().unwrap());
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">Listing 16-15: Using an <code>Arc<T></code> to wrap the <code>Mutex<T></code>
|
||
to be able to share ownership across multiple threads</span></p>
|
||
<p>这会打印出:</p>
|
||
<pre><code>Result: 10
|
||
</code></pre>
|
||
<p>成功了!我们从 0 数到了 10,这可能并不是很显眼,不过一路上我们学习了很多关于<code>Mutex<T></code>和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。能够被分解为独立部分的计算可以像这样被分散到多个线程中,并可以使用<code>Mutex<T></code>来允许每个线程在他们自己的部分更新最终的结果。</p>
|
||
<p>你可能注意到了,因为<code>counter</code>是不可变的,不过可以获取其内部值的可变引用,这意味着<code>Mutex<T></code>提供了内部可变性,就像<code>Cell</code>系列类型那样。正如第十五章中使用<code>RefCell<T></code>可以改变<code>Rc<T></code>中的内容那样,同样的可以使用<code>Mutex<T></code>来改变<code>Arc<T></code>中的内容。</p>
|
||
<p>回忆一下<code>Rc<T></code>并没有避免所有可能的问题:我们也讨论了当两个<code>Rc<T></code>相互引用时的引用循环的可能性,这可能造成内存泄露。<code>Mutex<T></code>有一个类似的 Rust 同样也不能避免的问题:死锁。<strong>死锁</strong>(<em>deadlock</em>)是一个场景中操作需要锁定两个资源,而两个线程分别拥有一个锁并永远相互等待的问题。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中<code>Mutex<T></code>和<code>MutexGuard</code>的 API 文档会提供有用的信息。</p>
|
||
<p>Rust 的类型系统和所有权规则,确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以不可预测的方式覆盖彼此的操作。虽然为了使一切正确运行而在编译器上花了一些时间,但是我们节省了未来的时间,尤其是线程以特定顺序执行才会出现的诡异错误难以重现。</p>
|
||
<p>接下来,为了丰富本章的内容,让我们讨论一下<code>Send</code>和<code>Sync</code> trait 以及如何对自定义类型使用他们。</p>
|
||
<a class="header" href="#使用sync和send-trait-的可扩展并发" name="使用sync和send-trait-的可扩展并发"><h2>使用<code>Sync</code>和<code>Send</code> trait 的可扩展并发</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-04-extensible-concurrency-sync-and-send.md">ch16-04-extensible-concurrency-sync-and-send.md</a>
|
||
<br>
|
||
commit 9430a3d28a2121a938d704ce48b15d21062f880e</p>
|
||
</blockquote>
|
||
<p>Rust 的并发模型中一个有趣的方面是:语言本身对并发知之<strong>甚少</strong>。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的。</p>
|
||
<p>我们说“<strong>几乎</strong>所有内容都不属于语言本身”,那么属于语言本身的是什么呢?是两个 trait,都位于<code>std::marker</code>: <code>Sync</code>和<code>Send</code>。</p>
|
||
<a class="header" href="#send用于表明所有权可能被传送给其他线程" name="send用于表明所有权可能被传送给其他线程"><h3><code>Send</code>用于表明所有权可能被传送给其他线程</h3></a>
|
||
<p><code>Send</code>标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是<code>Send</code>的,不过有一些例外。比如标准库中提供的 <code>Rc<T></code>:如果克隆<code>Rc<T></code>值,并尝试将克隆的所有权传递给另一个线程,这两个线程可能会同时更新引用计数。正如上一部分提到的,<code>Rc<T></code>被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。</p>
|
||
<p>因为 <code>Rc<T></code> 没有标记为 <code>Send</code>,Rust 的类型系统和 trait bound 会确保我们不会错误的把一个 <code>Rc<T></code> 值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误,<code>the trait Send is not implemented for Rc<Mutex<i32>></code>。而使用标记为 <code>Send</code> 的 <code>Arc<T></code> 时,就没有问题了。</p>
|
||
<p>任何完全由 <code>Send</code> 的类型组成的类型也会自动被标记为 <code>Send</code>:几乎所有基本类型都是 <code>Send</code> 的,大部分标准库类型是<code>Send</code>的,除了<code>Rc<T></code>,以及第十九章将会讨论的裸指针(raw pointer)。</p>
|
||
<a class="header" href="#sync-表明多线程访问是安全的" name="sync-表明多线程访问是安全的"><h3><code>Sync</code> 表明多线程访问是安全的</h3></a>
|
||
<p><code>Sync</code> 标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 <code>T</code>,如果<code>&T</code>(<code>T</code>的引用)是<code>Send</code>的话<code>T</code>就是<code>Sync</code>的,这样其引用就可以安全的发送到另一个线程。类似于 <code>Send</code> 的情况,基本类型是 <code>Sync</code> 的,完全由 <code>Sync</code> 的类型组成的类型也是 <code>Sync</code> 的。</p>
|
||
<p><code>Rc<T></code> 也不是 <code>Sync</code> 的,出于其不是<code>Send</code>的相同的原因。<code>RefCell<T></code>(第十五章讨论过)和<code>Cell<T></code>系列类型不是<code>Sync</code>的。<code>RefCell<T></code>在运行时所进行的借用检查也不是线程安全的。<code>Mutex<T></code>是<code>Sync</code>的,正如上一部分所讲的它可以被用来在多线程中共享访问。</p>
|
||
<a class="header" href="#手动实现send和sync是不安全的" name="手动实现send和sync是不安全的"><h3>手动实现<code>Send</code>和<code>Sync</code>是不安全的</h3></a>
|
||
<p>通常并不需要实现<code>Send</code>和<code>Sync</code> trait,由属于<code>Send</code>和<code>Sync</code>的类型组成的类型,自动就是<code>Send</code>和<code>Sync</code>的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。</p>
|
||
<p>实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是<code>Send</code>和<code>Sync</code>的部分构成的并发类型时需要多加小心,以确保维持其安全保证。<a href="https://doc.rust-lang.org/stable/nomicon/">The Nomicon</a> 中有更多关于这些保证以及如何维持他们的信息。</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>这不会是本书最后一个出现并发的章节;第二十章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。</p>
|
||
<p>正如我们提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。</p>
|
||
<p>Rust 提供了用于消息传递的通道,和像<code>Mutex<T></code>和<code>Arc<T></code>这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发吧!</p>
|
||
<p>接下来,让我们讨论一下当 Rust 程序变得更大时,有哪些符合语言习惯的问题建模方法和结构化解决方案,以及 Rust 的风格是如何与面向对象编程(Object Oriented Programming)中那些你所熟悉的概念相联系的。</p>
|
||
<a class="header" href="#rust-是一个面向对象的编程语言吗" name="rust-是一个面向对象的编程语言吗"><h2>Rust 是一个面向对象的编程语言吗?</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-00-oop.md">ch17-00-oop.md</a>
|
||
<br>
|
||
commit 759801361bde74b47e81755fff545c66020e6e63</p>
|
||
</blockquote>
|
||
<p>面向对象编程(Object-Oriented Programming)是一种起源于 20 世纪 60 年代的 Simula 编程语言的模式化编程方式,然后在 90 年代随着 C++ 语言开始流行。关于 OOP 是什么有很多相互矛盾的定义:在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。</p>
|
||
<a class="header" href="#什么是面向对象" name="什么是面向对象"><h2>什么是面向对象?</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-01-what-is-oo.md">ch17-01-what-is-oo.md</a>
|
||
<br>
|
||
commit 2a9b2a1b019ad6d4832ff3e56fbcba5be68b250e</p>
|
||
</blockquote>
|
||
<p>关于一个语言被称为面向对象所需的功能,在编程社区内并未达成一致意见。Rust 被很多不同的编程范式影响;我们探索了十三章提到的来自函数式编程的特性。面向对象编程语言所共享的一些特性往往是对象、封装和继承。让我们看一下这每一个概念的含义以及 Rust 是否支持他们。</p>
|
||
<a class="header" href="#对象包含数据和行为" name="对象包含数据和行为"><h3>对象包含数据和行为</h3></a>
|
||
<p><code>Design Patterns: Elements of Reusable Object-Oriented Software</code>这本书被俗称为<code>The Gang of Four book</code>,是面向对象编程模式的目录。它这样定义面向对象编程:</p>
|
||
<blockquote>
|
||
<p>Object-oriented programs are made up of objects. An <em>object</em> packages both
|
||
data and the procedures that operate on that data. The procedures are
|
||
typically called <em>methods</em> or <em>operations</em>.</p>
|
||
<p>面向对象的程序是由对象组成的。一个<strong>对象</strong>包含数据和操作这些数据的过程。这些过程通常被称为<strong>方法</strong>或<strong>操作</strong>。</p>
|
||
</blockquote>
|
||
<p>在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被<strong>称为</strong>对象,但是他们提供了与对象相同的功能,参考 Gang of Four 中对象的定义。</p>
|
||
<a class="header" href="#隐藏了实现细节的封装" name="隐藏了实现细节的封装"><h3>隐藏了实现细节的封装</h3></a>
|
||
<p>另一个通常与面向对象编程相关的方面是<strong>封装</strong>(<em>encapsulation</em>)的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。</p>
|
||
<p>就像我们在第七章讨论的那样,可以使用<code>pub</code>关键字来决定模块、类型函数和方法是公有的,而默认情况下一切都是私有的。比如,我们可以定义一个包含一个<code>i32</code>类型的 vector 的结构体<code>AveragedCollection</code>。结构体也可以有一个字段,该字段保存了 vector 中所有值的平均值。这样,希望知道结构体中的 vector 的平均值的人可以随时获取它,而无需自己计算。<code>AveragedCollection</code>会为我们缓存平均值结果。列表 17-1 有<code>AveragedCollection</code>结构体的定义:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">pub struct AveragedCollection {
|
||
list: Vec<i32>,
|
||
average: f64,
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-1: <code>AveragedCollection</code>结构体维护了一个整型列表和集合中所有元素的平均值。</span></p>
|
||
<p>注意,结构体自身被标记为<code>pub</code>,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。可以通过在结构体上实现<code>add</code>、<code>remove</code>和<code>average</code>方法来做到这一点,如列表 17-2 所示:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># pub struct AveragedCollection {
|
||
# list: Vec<i32>,
|
||
# average: f64,
|
||
# }
|
||
impl AveragedCollection {
|
||
pub fn add(&mut self, value: i32) {
|
||
self.list.push(value);
|
||
self.update_average();
|
||
}
|
||
|
||
pub fn remove(&mut self) -> Option<i32> {
|
||
let result = self.list.pop();
|
||
match result {
|
||
Some(value) => {
|
||
self.update_average();
|
||
Some(value)
|
||
},
|
||
None => None,
|
||
}
|
||
}
|
||
|
||
pub fn average(&self) -> f64 {
|
||
self.average
|
||
}
|
||
|
||
fn update_average(&mut self) {
|
||
let total: i32 = self.list.iter().sum();
|
||
self.average = total as f64 / self.list.len() as f64;
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-2: 在<code>AveragedCollection</code>结构体上实现了<code>add</code>、<code>remove</code>和<code>average</code>公有方法</span></p>
|
||
<p>公有方法<code>add</code>、<code>remove</code>和<code>average</code>是修改<code>AveragedCollection</code>实例的唯一方式。当使用<code>add</code>方法把一个元素加入到<code>list</code>或者使用<code>remove</code>方法来删除它时,这些方法的实现同时会调用私有的<code>update_average</code>方法来更新<code>average</code>字段。因为<code>list</code>和<code>average</code>是私有的,没有其他方式来使得外部的代码直接向<code>list</code>增加或者删除元素,直接操作<code>list</code>可能会引发<code>average</code>字段不同步。<code>average</code>方法返回<code>average</code>字段的值,这使得外部的代码只能读取<code>average</code>而不能修改它。</p>
|
||
<p>因为我们已经封装好了<code>AveragedCollection</code>的实现细节,将来可以轻松改变类似数据结构这些方面的内容。例如,可以使用<code>HashSet</code>代替<code>Vec</code>作为<code>list</code>字段的类型。只要<code>add</code>、<code>remove</code>和<code>average</code>公有函数的签名保持不变,使用<code>AveragedCollection</code>的代码就无需改变。如果将<code>List</code>暴露给外部代码时,未必都是这样,因为<code>HashSet</code>和<code>Vec</code>使用不同的方法增加或移除项,所以如果要想直接修改<code>list</code>的话,外部的代码可能不得不修改。</p>
|
||
<p>如果封装是一个语言被认为是面向对象语言所必要的方面的话,那么 Rust 就满足这个要求。在代码中不同的部分使用或者不使用<code>pub</code>决定了实现细节的封装。</p>
|
||
<a class="header" href="#作为类型系统的继承和作为代码共享的继承" name="作为类型系统的继承和作为代码共享的继承"><h2>作为类型系统的继承和作为代码共享的继承</h2></a>
|
||
<p><strong>继承</strong>(<em>Inheritance</em>)是一个很多编程语言都提供的机制,一个对象可以定义为继承另一个对象的定义,这使其可以获得父对象的数据和行为,而不用重新定义。一些人定义面向对象语言时,认为继承是一个特色。</p>
|
||
<p>如果一个语言必须有继承才能被称为面向对象语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承自另外一个结构体,从而获得父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,根据你希望使用继承的原因,Rust 也提供了其他的解决方案。</p>
|
||
<p>使用继承有两个主要的原因。第一个是为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。相反 Rust 代码可以使用默认 trait 方法实现来进行共享,在列表 10-14 中我们见过在<code>Summarizable</code> trait 上增加的<code>summary</code>方法的默认实现。任何实现了<code>Summarizable</code> trait 的类型都可以使用<code>summary</code>方法而无须进一步实现。这类似于父类有一个方法的实现,而通过继承子类也拥有这个方法的实现。当实现<code>Summarizable</code> trait 时也可以选择覆盖<code>summary</code>的默认实现,这类似于子类覆盖从父类继承的方法实现。</p>
|
||
<p>第二个使用继承的原因与类型系统有关:用来表现子类型可以在父类型被使用的地方使用。这也被称为<strong>多态</strong>(<em>polymorphism</em>),意味着如果多种对象有一个相同的形态大小,它们可以替代使用。</p>
|
||
<!-- PROD: START BOX -->
|
||
<blockquote>
|
||
<p>虽然很多人使用“多态”("polymorphism")来描述继承,但是它实际上是一种特殊的多态,称为“子类型多态”("sub-type polymorphism")。也有很多种其他形式的多态,在 Rust 中带有泛型参数的 trait bound 也是多态,更具体的说是“参数多态”("parametric polymorphism")。不同类型多态的确切细节在这里并不关键,所以不要过于担心细节,只需要知道 Rust 有多种多态相关的特色就好,不同于很多其他 OOP 语言。</p>
|
||
</blockquote>
|
||
<!-- PROD: END BOX -->
|
||
<p>为了支持这种模式,Rust 有 <strong>trait 对象</strong>(<em>trait objects</em>),这样就可以使用任意类型的值,只要这个值实现了指定的 trait。</p>
|
||
<p>继承最近在很多编程语言的设计方案中失宠了。使用继承来实现代码重用,会共享更多非必需的代码。子类不应该总是共享其父类的所有特性,然而继承意味着子类得到了其父类全部的数据和行为。这使得程序的设计更不灵活,并产生了无意义的方法调用或子类,以及由于方法并不适用于子类,却必需从父类继承而可能造成的错误。另外,某些语言只允许子类继承一个父类,进一步限制了程序设计的灵活性。</p>
|
||
<p>因为这些原因,Rust 选择了一个另外的途径,使用 trait 对象替代继承。让我们看一下在 Rust 中 trait 对象是如何实现多态的。</p>
|
||
<a class="header" href="#为使用不同类型的值而设计的-trait-对象" name="为使用不同类型的值而设计的-trait-对象"><h2>为使用不同类型的值而设计的 trait 对象</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-02-trait-objects.md">ch17-02-trait-objects.md</a>
|
||
<br>
|
||
commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9</p>
|
||
</blockquote>
|
||
<p>在第八章,我们谈到了 vector 只能存储同种类型元素的局限。在列表 8-1 中有一个例子,其中定义了存放包含整型、浮点型和文本型成员的枚举类型<code>SpreadsheetCell</code>,这样就可以在每一个单元格储存不同类型的数据,并使得 vector 仍然代表一行单元格。当编译时就知道类型集合全部元素的情况下,这种方案是可行的。</p>
|
||
<!-- The code example I want to reference did not have a listing number; it's
|
||
the one with SpreadsheetCell. I will go back and add Listing 8-1 next time I
|
||
get Chapter 8 for editing. /Carol -->
|
||
<p>有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用 <code>draw</code> 方法来绘制在屏幕上。我们将要创建一个叫做 <code>rust_gui</code> 的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如 <code>Button</code> 或 <code>TextField</code>。使用 <code>rust_gui</code> 的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个 <code>Image</code>,而另一个可能会增加一个 <code>SelectBox</code>。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。</p>
|
||
<p>当写 <code>rust_gui</code> 库时,我们不知道其他程序员需要什么类型,所以无法定义一个 <code>enum</code> 来包含所有的类型。然而 <code>rust_gui</code> 需要跟踪所有这些不同类型的值,需要有在每个值上调用 <code>draw</code> 方法能力。我们的 GUI 库不需要确切地知道调用 <code>draw</code> 方法会发生什么,只需要有可用的方法供我们调用。</p>
|
||
<p>在可以继承的语言里,我们会定义一个名为 <code>Component</code> 的类,该类上有一个<code>draw</code>方法。其他的类比如<code>Button</code>、<code>Image</code>和<code>SelectBox</code>会从<code>Component</code>继承并拥有<code>draw</code>方法。它们各自覆写<code>draw</code>方法以自定义行为,但是框架会把所有的类型当作是<code>Component</code>的实例,并在其上调用<code>draw</code>。</p>
|
||
<a class="header" href="#定义一个带有自定义行为的trait" name="定义一个带有自定义行为的trait"><h3>定义一个带有自定义行为的Trait</h3></a>
|
||
<p>不过,在Rust语言中,我们可以定义一个 <code>Draw</code> trait,包含名为 <code>draw</code> 的方法。我们定义一个由<em>trait对象</em>组成的vector,绑定了某种指针的trait,比如<code>&</code>引用或者一个<code>Box<T></code>智能指针。</p>
|
||
<p>之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和<code>impl</code>块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在 trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。</p>
|
||
<p>trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。列表 17-03 展示了如何定义一个名为<code>Draw</code>的带有<code>draw</code>方法的 trait。</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">pub trait Draw {
|
||
fn draw(&self);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-3:<code>Draw</code> trait 的定义</span></p>
|
||
<!-- NEXT PARAGRAPH WRAPPED WEIRD INTENTIONALLY SEE #199 -->
|
||
<p>因为我们已经在第十章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:列表 17-4 有一个名为 <code>Screen</code> 的结构体,里面有一个名为 <code>components</code> 的 vector,<code>components</code> 的类型是 <code>Box<Draw></code>。<code>Box<Draw></code> 是一个 trait 对象:它是 <code>Box</code> 内部任意一个实现了 <code>Draw</code> trait 的类型的替身。</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># pub trait Draw {
|
||
# fn draw(&self);
|
||
# }
|
||
#
|
||
pub struct Screen {
|
||
pub components: Vec<Box<Draw>>,
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-4: 一个 <code>Screen</code> 结构体的定义,它带有一个字段<code>components</code>,其包含实现了 <code>Draw</code> trait 的 trait 对象的 vector</span></p>
|
||
<p>在 <code>Screen</code> 结构体上,我们将要定义一个 <code>run</code> 方法,该方法会在它的 <code>components</code> 上的每一个元素调用 <code>draw</code> 方法,如列表 17-5 所示:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># pub trait Draw {
|
||
# fn draw(&self);
|
||
# }
|
||
#
|
||
# pub struct Screen {
|
||
# pub components: Vec<Box<Draw>>,
|
||
# }
|
||
#
|
||
impl Screen {
|
||
pub fn run(&self) {
|
||
for component in self.components.iter() {
|
||
component.draw();
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-5:在 <code>Screen</code> 上实现一个 <code>run</code> 方法,该方法在每个 component 上调用 <code>draw</code> 方法
|
||
</span></p>
|
||
<p>这与带 trait 约束的泛型结构体不同(trait 约束泛型参数)。泛型参数一次只能被一个具体类型替代,而 trait 对象可以在运行时允许多种具体类型填充 trait 对象。比如,我们已经定义了 <code>Screen</code> 结构体使用泛型和一个 trait 约束,如列表 17-6 所示:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># pub trait Draw {
|
||
# fn draw(&self);
|
||
# }
|
||
#
|
||
pub struct Screen<T: Draw> {
|
||
pub components: Vec<T>,
|
||
}
|
||
|
||
impl<T> Screen<T>
|
||
where T: Draw {
|
||
pub fn run(&self) {
|
||
for component in self.components.iter() {
|
||
component.draw();
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-6: 一种 <code>Screen</code> 结构体的替代实现,它的 <code>run</code> 方法使用通用类型和 trait 绑定
|
||
</span></p>
|
||
<p>这个例子中,<code>Screen</code> 实例所有组件类型必需全是 <code>Button</code>,或者全是 <code>TextField</code>。如果你的组件集合是单一类型的,那么可以优先使用泛型和 trait 约束,因为其使用的具体类型在编译阶段即可确定。</p>
|
||
<p>而 <code>Screen</code> 结构体内部的 <code>Vec<Box<Draw>></code> trait 对象列表,则可以同时包含 <code>Box<Button></code> 和 <code>Box<TextField></code>。我们看它是怎么工作的,然后讨论运行时性能。</p>
|
||
<a class="header" href="#来自我们或者库使用者的实现" name="来自我们或者库使用者的实现"><h3>来自我们或者库使用者的实现</h3></a>
|
||
<p>现在,我们增加一些实现了 <code>Draw</code> trait 的类型,再次提供 <code>Button</code>。实现一个 GUI 库实际上超出了本书的范围,因此 <code>draw</code> 方法留空。为了想象实现可能的样子,<code>Button</code> 结构体有 <code>width</code>、<code>height</code> 和 <code>label</code>字段,如列表 17-7 所示:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># pub trait Draw {
|
||
# fn draw(&self);
|
||
# }
|
||
#
|
||
pub struct Button {
|
||
pub width: u32,
|
||
pub height: u32,
|
||
pub label: String,
|
||
}
|
||
|
||
impl Draw for Button {
|
||
fn draw(&self) {
|
||
// Code to actually draw a button
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-7: 实一个现了<code>Draw</code> trait 的 <code>Button</code> 结构体</span></p>
|
||
<p>在 <code>Button</code> 上的 <code>width</code>、<code>height</code> 和 <code>label</code> 会和其他组件不同,比如 <code>TextField</code> 可能有 <code>width</code>、<code>height</code>,
|
||
<code>label</code> 以及 <code>placeholder</code> 字段。每个我们可以在屏幕上绘制的类型都会实现 <code>Draw</code> trait,在 <code>draw</code> 方法中使用不同的代码,定义了如何绘制 <code>Button</code>。除了 <code>Draw</code> trait,<code>Button</code> 也可能有一个 <code>impl</code> 块,包含按钮被点击时的响应方法。这类方法不适用于 <code>TextField</code> 这样的类型。</p>
|
||
<p>假定我们的库的用户相要实现一个包含 <code>width</code>、<code>height</code> 和 <code>options</code> 的 <code>SelectBox</code> 结构体。同时也在 <code>SelectBox</code> 类型上实现了 <code>Draw</code> trait,如 列表 17-8 所示:</p>
|
||
<p><span class="filename">文件名: src/main.rs</span></p>
|
||
<pre><code class="language-rust">extern crate rust_gui;
|
||
use rust_gui::Draw;
|
||
|
||
struct SelectBox {
|
||
width: u32,
|
||
height: u32,
|
||
options: Vec<String>,
|
||
}
|
||
|
||
impl Draw for SelectBox {
|
||
fn draw(&self) {
|
||
// Code to actually draw a select box
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-8: 另外一个 crate 中,在 <code>SelectBox</code> 结构体上使用 <code>rust_gui</code> 和实现了<code>Draw</code> trait
|
||
</span></p>
|
||
<p>库的用户现在可以在他们的 <code>main</code> 函数中创建一个 <code>Screen</code> 实例,然后把自身放入 <code>Box<T></code> 变成 trait 对象,向 screen 增加 <code>SelectBox</code> 和 <code>Button</code>。他们可以在这个 <code>Screen</code> 实例上调用 <code>run</code> 方法,这又会调用每个组件的 <code>draw</code> 方法。 列表 17-9 展示了实现:</p>
|
||
<p><span class="filename">文件名: src/main.rs</span></p>
|
||
<pre><code class="language-rust">use rust_gui::{Screen, Button};
|
||
|
||
fn main() {
|
||
let screen = Screen {
|
||
components: vec![
|
||
Box::new(SelectBox {
|
||
width: 75,
|
||
height: 10,
|
||
options: vec![
|
||
String::from("Yes"),
|
||
String::from("Maybe"),
|
||
String::from("No")
|
||
],
|
||
}),
|
||
Box::new(Button {
|
||
width: 50,
|
||
height: 10,
|
||
label: String::from("OK"),
|
||
}),
|
||
],
|
||
};
|
||
|
||
screen.run();
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型
|
||
</span></p>
|
||
<p>虽然我们不知道哪一天会有人增加 <code>SelectBox</code> 类型,但是我们的 <code>Screen</code> 能够操作 <code>SelectBox</code> 并绘制它,因为 <code>SelectBox</code> 实现了 <code>Draw</code> 类型,这意味着它实现了 <code>draw</code> 方法。</p>
|
||
<p>只关心值的响应,而不关心其具体类型,这类似于动态类型语言中的 <em>duck typing</em>:如果它像鸭子一样走路,像鸭子一样叫,那么它就是只鸭子!在 Listing 17-5 <code>Screen</code> 的 <code>run</code> 方法实现中,<code>run</code> 不需要知道每个组件的具体类型。它也不检查组件是 <code>Button</code> 还是 <code>SelectBox</code> 的实例,只管调用组件的 <code>draw</code> 方法。通过指定 <code>Box<Draw></code> 作为 <code>components</code> 列表中元素的类型,我们约束了 <code>Screen</code> 需要这些实现了 <code>draw</code> 方法的值。</p>
|
||
<p>Rust 类型系统使用 trait 对象来支持 duck typing 的好处是,我们无需在运行时检查一个值是否实现了特定方法,或是担心调用了一个值没有实现的方法。如果值没有实现 trait 对象需要的 trait(方法),Rust 不会编译。</p>
|
||
<p>比如,列表 17-10 展示了当我们创建一个使用 <code>String</code> 做为其组件的 <code>Screen</code> 时发生的情况:</p>
|
||
<p><span class="filename">文件名: src/main.rs</span></p>
|
||
<pre><code class="language-rust">extern crate rust_gui;
|
||
use rust_gui::Draw;
|
||
|
||
fn main() {
|
||
let screen = Screen {
|
||
components: vec![
|
||
Box::new(String::from("Hi")),
|
||
],
|
||
};
|
||
|
||
screen.run();
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-10: 尝试使用一种没有实现 trait 对象的类型</p>
|
||
<p></span></p>
|
||
<p>我们会遇到这个错误,因为 <code>String</code> 没有实现 <code>Draw</code> trait:</p>
|
||
<pre><code>error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
|
||
-->
|
||
|
|
||
4 | Box::new(String::from("Hi")),
|
||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not
|
||
implemented for `std::string::String`
|
||
|
|
||
= note: required for the cast to the object type `Draw`
|
||
</code></pre>
|
||
<p>这个错误告诉我们,要么传入 <code>Screen</code> 需要的类型,要么在 <code>String</code> 上实现 <code>Draw</code>,以便 <code>Screen</code> 调用它的 <code>draw</code> 方法。</p>
|
||
<a class="header" href="#trait-对象执行动态分发" name="trait-对象执行动态分发"><h3>Trait 对象执行动态分发</h3></a>
|
||
<p>回忆一下第十章我们讨论过的,当我们在泛型上使用 trait 约束时,编译器按单态类型处理:在需要使用范型参数的地方,编译器为每个具体类型生成非泛型的函数和方法实现。单态类型处理产生的代码实际就是做 <em>static dispatch</em>:方法的代码在编译阶段就已经决定了,当调用时,寻找那段代码非常快速。</p>
|
||
<p>当我们使用 trait 对象,编译器不能按单态类型处理,因为无法知道使用代码的所有可能类型。而是调用方法的时候,Rust 跟踪可能被使用的代码,在运行时找出调用该方法时应使用的代码。这也是我们熟知的 <em>dynamic dispatch</em>,查找过程会产生运行时开销。动态分发也会阻止编译器内联函数,失去一些优化途径。尽管获得了额外的灵活性,但仍然需要权衡取舍。</p>
|
||
<a class="header" href="#trait-对象需要对象安全" name="trait-对象需要对象安全"><h3>Trait 对象需要对象安全</h3></a>
|
||
<!-- Liz: we're conflicted on including this section. Not being able to use a
|
||
trait as a trait object because of object safety is something that
|
||
beginner/intermediate Rust developers run into sometimes, but explaining it
|
||
fully is long and complicated. Should we just cut this whole section? Leave it
|
||
(and finish the explanation of how to fix the error at the end)? Shorten it to
|
||
a quick caveat, that just says something like "Some traits can't be trait
|
||
objects. Clone is an example of one. You'll get errors that will let you know
|
||
if a trait can't be a trait object, look up object safety if you're interested
|
||
in the details"? Thanks! /Carol -->
|
||
<p>不是所有的 trait 都可以被放进 trait 对象中; 只有<em>对象安全的</em>(<em>object safe</em>)trait 才可以这样做. 一个 trait 只有同时满足如下两点时才被认为是对象安全的:</p>
|
||
<ul>
|
||
<li>该 trait 要求 <code>Self</code> 不是 <code>Sized</code>;</li>
|
||
<li>该 trait 的所有方法都是对象安全的;</li>
|
||
</ul>
|
||
<p><code>Self</code> 是一个类型的别名关键字,它表示当前正被实现的 trait 类型或者是方法所属的类型. <code>Sized</code>是一个像在第十六章中介绍的<code>Send</code>和<code>Sync</code>那样的标记 trait, 在编译时它会自动被放进大小确定的类型里,比如<code>i32</code>和引用. 大小不确定的类型有 slice(<code>[T]</code>)和 trait 对象.</p>
|
||
<p><code>Sized</code> 是一个默认会被绑定到所有常规类型参数的内隐 trait. Rust 中要求一个类型是<code>Sized</code>的最具可用性的用法是让<code>Sized</code>成为一个默认的 trait 绑定,这样我们就可以在大多数的常规的用法中不去写 <code>T: Sized</code> 了. 如果我们想在切片(slice)中使用一个 trait, 我们需要取消对<code>Sized</code>的 trait 绑定, 我们只需制定<code>T: ?Sized</code>作为 trait 绑定.</p>
|
||
<p>默认绑定到 <code>Self: ?Sized</code> 的 trait 可以被实现到是 <code>Sized</code> 或非 <code>Sized</code> 的类型上. 如果我们创建一个不绑定 <code>Self: ?Sized</code> 的 trait <code>Foo</code>,它看上去应该像这样:</p>
|
||
<pre><code class="language-rust">trait Foo: Sized {
|
||
fn some_method(&self);
|
||
}
|
||
</code></pre>
|
||
<p>Trait <code>Sized</code>现在就是 trait <code>Foo</code>的一个<em>超级 trait</em>(<em>supertrait</em>), 也就是说 trait <code>Foo</code> 需要实现了 <code>Foo</code> 的类型(即<code>Self</code>)是<code>Sized</code>. 我们将在第十九章中更详细的介绍超 trait(supertrait).</p>
|
||
<p>像<code>Foo</code>那样要求<code>Self</code>是<code>Sized</code>的 trait 不允许成为 trait 对象的原因是不可能为 trait 对象<code>Foo</code>实现 trait <code>Foo</code>: trait 对象是无确定大小的,但是 <code>Foo</code> 要求 <code>Self</code> 是 <code>Sized</code>. 一个类型不可能同时既是有大小的又是无确定大小的.</p>
|
||
<p>第二点说对象安全要求一个 trait 的所有方法必须是对象安全的. 一个对象安全的方法满足下列条件:</p>
|
||
<ul>
|
||
<li>它要求 <code>Self</code> 是 <code>Sized</code> 或者</li>
|
||
<li>它符合下面全部三点:
|
||
<ul>
|
||
<li>它不包含任意类型的常规参数</li>
|
||
<li>它的第一个参数必须是类型 <code>Self</code> 或一个引用到 <code>Self</code> 的类型(也就是说它必须是一个方法而非关联函数并且以 <code>self</code>、<code>&self</code> 或 <code>&mut self</code> 作为第一个参数)</li>
|
||
<li>除了第一个参数外它不能在其它地方用 <code>Self</code> 作为方法的参数签名</li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
<p>虽然这些规则有一点形式化, 但是换个角度想一下: 如果你的方法在它的参数签名的其它地方也需要具体的 <code>Self</code> 类型参数, 但是一个对象又忘记了它的具体类型是什么, 这时该方法就无法使用被它忘记的原先的具体类型. 当该 trait 被使用时, 被具体类型参数填充的常规类型参数也是如此: 这个具体的类型就成了实现该 trait 的类型的某一部分, 如果使用一个 trait 对象时这个类型被抹掉了, 就没有办法知道该用什么类型来填充这个常规类型参数.</p>
|
||
<p>一个 trait 的方法不是对象安全的一个例子是标准库中的 <code>Clone</code> trait. <code>Clone</code> trait 的 <code>clone</code> 方法的参数签名是这样的:</p>
|
||
<pre><code class="language-rust">pub trait Clone {
|
||
fn clone(&self) -> Self;
|
||
}
|
||
</code></pre>
|
||
<p><code>String</code> 实现了 <code>Clone</code> trait, 当我们在一个 <code>String</code> 实例上调用 <code>clone</code> 方法时, 我们会得到一个 <code>String</code> 实例. 同样地, 如果我们在一个 <code>Vec</code> 实例上调用 <code>clone</code> 方法, 我们会得到一个 <code>Vec</code> 实例. <code>clone</code> 的参数签名需要知道 <code>Self</code> 是什么类型, 因为它需要返回这个类型.</p>
|
||
<p>如果我们像列表 17-3 中列出的 <code>Draw</code> trait 那样的 trait 上实现 <code>Clone</code>, 我们就不知道 <code>Self</code> 将会是一个 <code>Button</code>, 一个 <code>SelectBox</code>, 或者是其它的在将来要实现 <code>Draw</code> trait 的类型.</p>
|
||
<p>如果你做了违反 trait 对象的对象安全性规则的事情, 编译器将会告诉你. 比如, 如果你实现在列表 17-4 中列出的 <code>Screen</code> 结构, 你想让该结构像这样持有实现了 <code>Clone</code> trait 的类型而不是 <code>Draw</code> trait:</p>
|
||
<pre><code class="language-rust">pub struct Screen {
|
||
pub components: Vec<Box<Clone>>,
|
||
}
|
||
</code></pre>
|
||
<p>我们将会得到下面的错误:</p>
|
||
<pre><code class="language-text">error[E0038]: the trait `std::clone::Clone` cannot be made into an object
|
||
-->
|
||
|
|
||
2 | pub components: Vec<Box<Clone>>,
|
||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone` cannot be
|
||
made into an object
|
||
|
|
||
= note: the trait cannot require that `Self : Sized`
|
||
</code></pre>
|
||
<!-- If we are including this section, we would explain how to fix this
|
||
problem. It involves adding another trait and implementing Clone manually for
|
||
that trait. Because this section is getting long, I stopped because it feels
|
||
like we're off in the weeds with an esoteric detail that not everyone will need
|
||
to know about. /Carol -->
|
||
<a class="header" href="#面向对象设计模式的实现" name="面向对象设计模式的实现"><h2>面向对象设计模式的实现</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-03-oo-design-patterns.md">ch17-03-oo-design-patterns.md</a>
|
||
<br>
|
||
commit 67737ff868e3347588cc832eceb8fc237afc5895</p>
|
||
</blockquote>
|
||
<p>让我们看看一个状态设计模式的例子以及如何在 Rust 中使用他们。<strong>状态模式</strong>(<em>state pattern</em>)是指一个值有某些内部状态,而它的行为随着其内部状态而改变。内部状态由一系列继承了共享功能的对象表现(我们使用结构体和 trait 因为 Rust 没有对象和继承)。每一个状态对象负责它自身的行为和当需要改变为另一个状态时的规则。持有任何一个这种状态对象的值对于不同状态的行为以及何时状态转移毫不知情。当将来需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变它的规则,或者是增加更多的状态对象。</p>
|
||
<p>为了探索这个概念,我们将实现一个增量式的发布博文的工作流。这个我们希望发布博文时所应遵守的工作流,一旦完成了它的实现,将为如下:</p>
|
||
<ol>
|
||
<li>博文从空白的草案开始。</li>
|
||
<li>一旦草案完成,请求审核博文。</li>
|
||
<li>一旦博文过审,它将被发表。</li>
|
||
<li>只有被发表的博文的内容会被打印,这样就不会意外打印出没有被审核的博文的文本。</li>
|
||
</ol>
|
||
<p>任何其他对博文的修改尝试都是没有作用的。例如,如果尝试在请求审核之前通过一个草案博文,博文应该保持未发布的状态。</p>
|
||
<p>列表 17-11 展示这个工作流的代码形式。这是一个我们将要在一个叫做 <code>blog</code> 的库 crate 中实现的 API 的使用示例:</p>
|
||
<p><span class="filename">文件名: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">extern crate blog;
|
||
use blog::Post;
|
||
|
||
fn main() {
|
||
let mut post = Post::new();
|
||
|
||
post.add_text("I ate a salad for lunch today");
|
||
assert_eq!("", post.content());
|
||
|
||
post.request_review();
|
||
assert_eq!("", post.content());
|
||
|
||
post.approve();
|
||
assert_eq!("I ate a salad for lunch today", post.content());
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-11: 展示了 <code>blog</code> crate 期望行为的代码</span></p>
|
||
<p>我们希望能够使用 <code>Post::new</code> 创建一个新的博文草案。接着希望能在草案阶段为博文编写一些文本。如果尝试立即打印出博文的内容,将不会得到任何文本,因为博文仍然是草案。这里增加的 <code>assert_eq!</code> 用于展示目的。断言草案博文的 <code>content</code> 方法返回空字符串将能作为库的一个非常好的单元测试,不过我们并不准备为这个例子编写单元测试。</p>
|
||
<p>接下来,我们希望能够请求审核博文,而在等待审核的阶段 <code>content</code> 应该仍然返回空字符串,当博文审核通过,它应该被发表,这意味着当调用 <code>content</code> 时我们编写的文本将被返回。</p>
|
||
<p>注意我们与 crate 交互的唯一的类型是 <code>Post</code>。博文可能处于的多种状态(草案,等待审核和发布)由 <code>Post</code> 内部管理。博文状态依我们在<code>Post</code>调用的方法而改变,但不必直接管理状态改变。这也意味着不会在状态上犯错,比如忘记了在发布前请求审核。</p>
|
||
<a class="header" href="#定义-post-并新建一个草案状态的实例" name="定义-post-并新建一个草案状态的实例"><h3>定义 <code>Post</code> 并新建一个草案状态的实例</h3></a>
|
||
<p>让我们开始实现这个库吧!我们知道需要一个公有 <code>Post</code> 结构体来存放一些文本,所以让我们从结构体的定义和一个创建 <code>Post</code> 实例的公有关联函数 <code>new</code> 开始,如列表 17-12 所示。我们还需定义一个私有 trait <code>State</code>。<code>Post</code> 将在私有字段 <code>state</code> 中存放一个 <code>Option</code> 中的 trait 对象 <code>Box<State></code>。稍后将会看到为何 <code>Option</code> 是必须的。<code>State</code> trait 定义了所有不同状态的博文所共享的行为,同时 <code>Draft</code>、<code>PendingReview</code> 和 <code>Published</code> 状态都会实现<code>State</code> 状态。现在这个 trait 并没有任何方法,同时开始将只定义<code>Draft</code>状态因为这是我们希望开始的状态:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">pub struct Post {
|
||
state: Option<Box<State>>,
|
||
content: String,
|
||
}
|
||
|
||
impl Post {
|
||
pub fn new() -> Post {
|
||
Post {
|
||
state: Some(Box::new(Draft {})),
|
||
content: String::new(),
|
||
}
|
||
}
|
||
}
|
||
|
||
trait State {}
|
||
|
||
struct Draft {}
|
||
|
||
impl State for Draft {}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-12: <code>Post</code>结构体的定义和新建 <code>Post</code> 实例的 <code>new</code>函数,<code>State</code> trait 和实现了 <code>State</code> 的结构体 <code>Draft</code></span></p>
|
||
<p>当创建新的 <code>Post</code> 时,我们将其 <code>state</code> 字段设置为一个 <code>Some</code> 值,它存放了指向一个 <code>Draft</code> 结构体新实例的 <code>Box</code>。这确保了无论何时新建一个 <code>Post</code> 实例,它会从草案开始。因为 <code>Post</code> 的 <code>state</code> 字段是私有的,也就无法创建任何其他状态的 <code>Post</code> 了!。</p>
|
||
<a class="header" href="#存放博文内容的文本" name="存放博文内容的文本"><h3>存放博文内容的文本</h3></a>
|
||
<p>在 <code>Post::new</code> 函数中,我们设置 <code>content</code> 字段为新的空 <code>String</code>。在列表 17-11 中,展示了我们希望能够调用一个叫做 <code>add_text</code> 的方法并向其传递一个 <code>&str</code> 来将文本增加到博文的内容中。选择实现为一个方法而不是将 <code>content</code> 字段暴露为 <code>pub</code> 是因为我们希望能够通过之后实现的一个方法来控制 <code>content</code> 字段如何被读取。<code>add_text</code> 方法是非常直观的,让我们在列表 17-13 的 <code>impl Post</code> 块中增加一个实现:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># pub struct Post {
|
||
# content: String,
|
||
# }
|
||
#
|
||
impl Post {
|
||
// ...snip...
|
||
pub fn add_text(&mut self, text: &str) {
|
||
self.content.push_str(text);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-13: 实现方法 <code>add_text</code> 来向博文的 <code>content</code> 增加文本</span></p>
|
||
<p><code>add_text</code> 获取一个 <code>self</code> 的可变引用,因为需要改变调用 <code>add_text</code> 的 <code>Post</code>。接着调用 <code>content</code> 中的 <code>String</code> 的 <code>push_str</code> 并传递 <code>text</code> 参数来保存到 <code>content</code> 中。这不是状态模式的一部分,因为它的行为并不依赖博文所处的状态。<code>add_text</code> 方法完全不与 <code>state</code> 状态交互,不过这是我们希望支持的行为的一部分。</p>
|
||
<a class="header" href="#博文草案的内容是空的" name="博文草案的内容是空的"><h3>博文草案的内容是空的</h3></a>
|
||
<p>调用 <code>add_text</code> 并像博文增加一些内容之后,我们仍然希望 <code>content</code> 方法返回一个空字符串 slice,因为博文仍然处于草案状态,如列表 17-11 的第 8 行所示。现在让我们使用能满足要求的最简单的方式来实现 <code>content</code> 方法 总是返回一个空字符 slice。当实现了将博文状态改为发布的能力之后将改变这一做法。但是现在博文只能是草案状态,这意味着其内容总是空的。列表 17-14 展示了这个占位符实现:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># pub struct Post {
|
||
# content: String,
|
||
# }
|
||
#
|
||
impl Post {
|
||
// ...snip...
|
||
pub fn content(&self) -> &str {
|
||
""
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-14: 增加一个 <code>Post</code> 的 <code>content</code> 方法的占位实现,它总是返回一个空字符串 slice</span></p>
|
||
<p>通过增加这个 <code>content</code>方法,列表 17-11 中直到第 8 行的代码能如期运行。</p>
|
||
<a class="header" href="#请求审核博文来改变其状态" name="请求审核博文来改变其状态"><h3>请求审核博文来改变其状态</h3></a>
|
||
<p>接下来是请求审核博文,这应当将其状态由 <code>Draft</code> 改为 <code>PendingReview</code>。我们希望 <code>post</code> 有一个获取 <code>self</code> 可变引用的公有方法 <code>request_review</code>。接着将调用内部存放的状态的 <code>request_review</code> 方法,而这第二个 <code>request_review</code> 方法会消费当前的状态并返回要一个状态。为了能够消费旧状态,第二个 <code>request_review</code> 方法需要能够获取状态值的所有权。这就是 <code>Option</code> 的作用:我们将 <code>take</code> 字段 <code>state</code> 中的 <code>Some</code> 值并留下一个 <code>None</code> 值,因为 Rust 并不允许结构体中有空字段。接着将博文的 <code>state</code> 设置为这个操作的结果。列表 17-15 展示了这些代码:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># pub struct Post {
|
||
# state: Option<Box<State>>,
|
||
# content: String,
|
||
# }
|
||
#
|
||
impl Post {
|
||
// ...snip...
|
||
pub fn request_review(&mut self) {
|
||
if let Some(s) = self.state.take() {
|
||
self.state = Some(s.request_review())
|
||
}
|
||
}
|
||
}
|
||
|
||
trait State {
|
||
fn request_review(self: Box<Self>) -> Box<State>;
|
||
}
|
||
|
||
struct Draft {}
|
||
|
||
impl State for Draft {
|
||
fn request_review(self: Box<Self>) -> Box<State> {
|
||
Box::new(PendingReview {})
|
||
}
|
||
}
|
||
|
||
struct PendingReview {}
|
||
|
||
impl State for PendingReview {
|
||
fn request_review(self: Box<Self>) -> Box<State> {
|
||
self
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-15: 实现 <code>Post</code> 和 <code>State</code> trait 的 <code>request_review</code> 方法</span></p>
|
||
<p>这里给 <code>State</code> trait 增加了 <code>request_review</code> 方法;所有实现了这个 trait 的类型现在都需要实现 <code>request_review</code> 方法。注意不用于使用<code>self</code>、 <code>&self</code> 或者 <code>&mut self</code> 作为方法的第一个参数,这里使用了 <code>self: Box<Self></code>。这个语法意味着这个方法调用只对这个类型的 <code>Box</code> 有效。这个语法获取了 <code>Box<Self></code> 的所有权,这是我们希望的,因为需要从老状态转换为新状态,同时希望老状态不再有效。</p>
|
||
<p><code>Draft</code> 的方法 <code>request_review</code> 的实现返回一个新的,装箱的 <code>PendingReview</code> 结构体的实例,这是新引入的用来代表博文处于等待审核状态的类型。结构体 <code>PendingReview</code> 同样也实现了 <code>request_review</code> 方法,不过它不进行任何状态转换。它返回自身,因为请求审核已经处于 <code>PendingReview</code> 状态的博文应该保持 <code>PendingReview</code> 状态。</p>
|
||
<p>现在能够看出状态模式的优势了:<code>Post</code> 的 <code>request_review</code> 方法无论 <code>state</code> 是何值都是一样的。每个状态负责它自己的规则。</p>
|
||
<p>我们将继续保持 <code>Post</code> 的 <code>content</code> 方法不变,返回一个空字符串 slice。现在可以拥有 <code>PendingReview</code> 状态而不仅仅是 <code>Draft</code> 状态的 <code>Post</code> 了,不过我们希望在 <code>PendingReview</code> 状态下其也有相同的行为。现在列表 17-11 中直到 11 行的代码是可以执行的!</p>
|
||
<a class="header" href="#批准博文并改变-content-的行为" name="批准博文并改变-content-的行为"><h3>批准博文并改变 <code>content</code> 的行为</h3></a>
|
||
<p><code>Post</code> 的 <code>approve</code> 方法将与 <code>request_review</code> 方法类似:它会将 <code>state</code> 设置为审核通过时应处于的状态。我们需要为 <code>State</code> trait 增加 <code>approve</code> 方法,并需新增实现了 <code>State</code> 的结构体, <code>Published</code> 状态。列表 17-16 展示了新增的代码:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># pub struct Post {
|
||
# state: Option<Box<State>>,
|
||
# content: String,
|
||
# }
|
||
#
|
||
impl Post {
|
||
// ...snip...
|
||
pub fn approve(&mut self) {
|
||
if let Some(s) = self.state.take() {
|
||
self.state = Some(s.approve())
|
||
}
|
||
}
|
||
}
|
||
|
||
trait State {
|
||
fn request_review(self: Box<Self>) -> Box<State>;
|
||
fn approve(self: Box<Self>) -> Box<State>;
|
||
}
|
||
|
||
struct Draft {}
|
||
|
||
impl State for Draft {
|
||
# fn request_review(self: Box<Self>) -> Box<State> {
|
||
# Box::new(PendingReview {})
|
||
# }
|
||
#
|
||
// ...snip...
|
||
fn approve(self: Box<Self>) -> Box<State> {
|
||
self
|
||
}
|
||
}
|
||
|
||
struct PendingReview {}
|
||
|
||
impl State for PendingReview {
|
||
# fn request_review(self: Box<Self>) -> Box<State> {
|
||
# Box::new(PendingReview {})
|
||
# }
|
||
#
|
||
// ...snip...
|
||
fn approve(self: Box<Self>) -> Box<State> {
|
||
Box::new(Published {})
|
||
}
|
||
}
|
||
|
||
struct Published {}
|
||
|
||
impl State for Published {
|
||
fn request_review(self: Box<Self>) -> Box<State> {
|
||
self
|
||
}
|
||
|
||
fn approve(self: Box<Self>) -> Box<State> {
|
||
self
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-16: 为 <code>Post</code> 和 <code>State</code> trait 实现 <code>approve</code> 方法</span></p>
|
||
<p>类似于 <code>request_review</code>,如果对 <code>Draft</code> 调用 <code>approve</code> 方法,并没有任何效果,因为它会返回 <code>self</code>。当对 <code>PendingReview</code> 调用 <code>approve</code> 时,它返回一个新的、装箱的 <code>Published</code> 结构体的实例。<code>Published</code> 结构体实现了 <code>State</code> trait,同时对于 <code>request_review</code> 和 <code>approve</code> 方法来说,它返回自身,因为在这两种情况博文应该保持 <code>Published</code> 状态。</p>
|
||
<p>现在更新 <code>Post</code> 的 <code>content</code> 方法:我们希望当博文处于 <code>Published</code> 时返回 <code>content</code> 字段的值,否则返回空字符串 slice。因为目标是将所有像这样的规则保持在实现了 <code>State</code> 的结构体中,我们将调用 <code>state</code> 中的值的 <code>content</code> 方法并传递博文实例(也就是 <code>self</code>)作为参数。接着返回 <code>state</code> 值的 <code>content</code> 方法的返回值,如列表 17-17 所示:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># trait State {
|
||
# fn content<'a>(&self, post: &'a Post) -> &'a str;
|
||
# }
|
||
# pub struct Post {
|
||
# state: Option<Box<State>>,
|
||
# content: String,
|
||
# }
|
||
#
|
||
impl Post {
|
||
// ...snip...
|
||
pub fn content(&self) -> &str {
|
||
self.state.as_ref().unwrap().content(&self)
|
||
}
|
||
// ...snip...
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-17: 更新 <code>Post</code> 的 <code>content</code> 方法来委托调用 <code>State</code> 的<code>content</code> 方法</span></p>
|
||
<p>这里调用 <code>Option</code> 的 <code>as_ref</code>方法是因为需要 <code>Option</code> 中值的引用。接着调用 <code>unwrap</code> 方法,这里我们知道永远也不会 panic 因为 <code>Post</code> 的所有方法都确保在他们返回时 <code>state</code> 会有一个 <code>Some</code> 值。这就是一个第十二章讨论过的我们知道 <code>None</code> 是不可能的而编译器却不能理解的情况。</p>
|
||
<p><code>State</code> trait 的 <code>content</code> 方法是博文返回什么内容的逻辑所在之处。我们将增加一个 <code>content</code> 方法的默认实现来返回一个空字符串 slice。这样就无需为 <code>Draft</code> 和 <code>PendingReview</code> 结构体实现 <code>content</code> 了。<code>Published</code> 结构体会覆盖 <code>content</code> 方法并会返回 <code>post.content</code> 的值,如列表 17-18 所示:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># pub struct Post {
|
||
# content: String
|
||
# }
|
||
trait State {
|
||
// ...snip...
|
||
fn content<'a>(&self, post: &'a Post) -> &'a str {
|
||
""
|
||
}
|
||
}
|
||
|
||
// ...snip...
|
||
struct Published {}
|
||
|
||
impl State for Published {
|
||
// ...snip...
|
||
fn content<'a>(&self, post: &'a Post) -> &'a str {
|
||
&post.content
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-18: 为 <code>State</code> trait 增加 <code>content</code> 方法</span></p>
|
||
<p>注意这个方法需要生命周期注解,如第十章所讨论的。这里获取 <code>post</code> 的引用作为参数,并返回 <code>post</code> 一部分的引用,所以返回的引用的生命周期与 <code>post</code> 参数相关。</p>
|
||
<a class="header" href="#状态模式的权衡取舍" name="状态模式的权衡取舍"><h3>状态模式的权衡取舍</h3></a>
|
||
<p>我们展示了 Rust 是能够实现面向对象的状态模式的,以便能根据博文所处的状态来封装不同类型的行为。<code>Post</code> 的方法并不知道这些不同类型的行为。这种组织代码的方式,为了找到所有已发布的博文不同行为只需查看一处代码:<code>Published</code> 的 <code>State</code> trait 的实现。</p>
|
||
<p>一个不使用状态模式的替代实现可能会在 <code>Post</code> 的方法中,甚至于在使用 <code>Post</code> 的代码中(在这里是 <code>main</code> 中)用到 <code>match</code> 语句,来检查博文状态并在这里改变其行为。这可能意味着需要查看很多位置来理解处于发布状态的博文的所有逻辑!这在增加更多状态时会变得更糟:每一个 <code>match</code> 语句都会需要另一个分支。对于状态模式来说,<code>Post</code> 的方法和使用 <code>Post</code> 的位置无需<code>match</code> 语句,同时增加新状态只涉及到增加一个新 <code>struct</code> 和为其实现 trait 的方法。</p>
|
||
<p>这个实现易于增加更多功能。这里是一些你可以尝试对本部分代码做出的修改,来亲自体会一下使用状态模式随着时间的推移维护代码是什么感觉:</p>
|
||
<ul>
|
||
<li>只允许博文处于 <code>Draft</code> 状态时增加文本内容</li>
|
||
<li>增加 <code>reject</code> 方法将博文的状态从 <code>PendingReview</code> 变回 <code>Draft</code></li>
|
||
<li>在将状态变为 <code>Published</code> 之前需要两次 <code>approve</code> 调用</li>
|
||
</ul>
|
||
<p>状态模式的一个缺点是因为状态实现了状态之间的转换,一些状态会相互联系。如果在 <code>PendingReview</code> 和 <code>Published</code> 之间增加另一个状态,比如 <code>Scheduled</code>,则不得不修改 <code>PendingReview</code> 中的代码来转移到 <code>Scheduled</code>。如果 <code>PendingReview</code> 无需因为新增的状态而改变就更好了,不过这意味着切换到另一个设计模式。</p>
|
||
<p>这个 Rust 中的实现的缺点在于存在一些重复的逻辑。如果能够为 <code>State</code> trait 中返回 <code>self</code> 的 <code>request_review</code> 和 <code>approve</code> 方法增加默认实现就好了,不过这会违反对象安全性,因为 trait 不知道 <code>self</code> 具体是什么。我们希望能够将 <code>State</code> 作为一个 trait 对象,所以需要这个方法是对象安全的。</p>
|
||
<p>另一个最好能去除的重复是 <code>Post</code> 中 <code>request_review</code> 和 <code>approve</code> 这两个类似的实现。他们都委托调用了 <code>state</code> 字段中 <code>Option</code> 值的同一方法,并在结果中为 <code>state</code> 字段设置了新值。如果 <code>Post</code> 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看附录 E 以了解宏)。</p>
|
||
<p>这个完全按照面向对象语言的定义实现的面向对象模式的缺点在于没有尽可能的利用 Rust 的优势。让我们看看一些代码中可以做出的修改,来将无效的状态和状态转移变为编译时错误。</p>
|
||
<a class="header" href="#将状态和行为编码为类型" name="将状态和行为编码为类型"><h4>将状态和行为编码为类型</h4></a>
|
||
<p>我们将展示如何稍微反思状态模式来进行一系列不同的权衡取舍。不同于完全封装状态和状态转移使得外部代码对其毫不知情,我们将将状态编码进不同的类型。当状态是类型时,Rust 的类型检查就会使任何在只能使用发布的博文的地方使用草案博文的尝试变为编译时错误。</p>
|
||
<p>让我们考虑一下列表 17-11 中 <code>main</code> 的第一部分:</p>
|
||
<p><span class="filename">文件名: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let mut post = Post::new();
|
||
|
||
post.add_text("I ate a salad for lunch today");
|
||
assert_eq!("", post.content());
|
||
}
|
||
</code></pre>
|
||
<p>我们仍然希望使用 <code>Post::new</code> 创建一个新的草案博文,并仍然希望能够增加博文的内容。不过不同于存在一个草案博文时返回空字符串的 <code>content</code> 方法,我们将使草案博文完全没有 <code>content</code> 方法。这样如果尝试获取草案博文的内容,将会得到一个方法不存在的编译错误。这使得我们不可能在生产环境意外显示出草案博文的内容,因为这样的代码甚至就不能编译。列表 17-19 展示了 <code>Post</code> 结构体、<code>DraftPost</code> 结构体以及各自的方法的定义:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust">pub struct Post {
|
||
content: String,
|
||
}
|
||
|
||
pub struct DraftPost {
|
||
content: String,
|
||
}
|
||
|
||
impl Post {
|
||
pub fn new() -> DraftPost {
|
||
DraftPost {
|
||
content: String::new(),
|
||
}
|
||
}
|
||
|
||
pub fn content(&self) -> &str {
|
||
&self.content
|
||
}
|
||
}
|
||
|
||
impl DraftPost {
|
||
pub fn add_text(&mut self, text: &str) {
|
||
self.content.push_str(text);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-19: 带有 <code>content</code> 方法的 <code>Post</code> 和没有 <code>content</code> 方法的 <code>DraftPost</code></span></p>
|
||
<p><code>Post</code> 和 <code>DraftPost</code> 结构体都有一个私有的 <code>content</code> 字段来储存博文的文本。这些结构体不再有 <code>state</code> 字段因为我们将类型编码为结构体的类型。<code>Post</code> 将代表发布的博文,它有一个返回 <code>content</code> 的 <code>content</code> 方法。</p>
|
||
<p>仍然有一个 <code>Post::new</code> 函数,不过不同于返回 <code>Post</code> 实例,它返回 <code>DraftPost</code> 的实例。现在不可能创建一个 <code>Post</code> 实例,因为 <code>content</code> 是私有的同时没有任何函数返回 <code>Post</code>。<code>DraftPost</code> 上定义了一个 <code>add_text</code> 方法,这样就可以像之前那样向 <code>content</code> 增加文本,不过注意 <code>DraftPost</code> 并没有定义 <code>content</code> 方法!所以所有博文都强制从草案开始,同时草案博文没有任何可供展示的内容。任何绕过这些限制的尝试都会产生编译错误。</p>
|
||
<a class="header" href="#实现状态转移为不同类型的转移" name="实现状态转移为不同类型的转移"><h4>实现状态转移为不同类型的转移</h4></a>
|
||
<p>那么如何得到发布的博文呢?我们希望强制的规则是草案博文在可以发布之前必须被审核通过。等待审核状态的博文应该仍然不会显示任何内容。让我们通过增加另一个结构体 <code>PendingReviewPost</code> 来实现这个限制,在 <code>DraftPost</code> 上定义 <code>request_review</code> 方法来返回 <code>PendingReviewPost</code>,并在 <code>PendingReviewPost</code> 上定义 <code>approve</code> 方法来返回 <code>Post</code>,如列表 17-20 所示:</p>
|
||
<p><span class="filename">文件名: src/lib.rs</span></p>
|
||
<pre><code class="language-rust"># pub struct Post {
|
||
# content: String,
|
||
# }
|
||
#
|
||
# pub struct DraftPost {
|
||
# content: String,
|
||
# }
|
||
#
|
||
impl DraftPost {
|
||
// ...snip...
|
||
|
||
pub fn request_review(self) -> PendingReviewPost {
|
||
PendingReviewPost {
|
||
content: self.content,
|
||
}
|
||
}
|
||
}
|
||
|
||
pub struct PendingReviewPost {
|
||
content: String,
|
||
}
|
||
|
||
impl PendingReviewPost {
|
||
pub fn approve(self) -> Post {
|
||
Post {
|
||
content: self.content,
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-20: <code>PendingReviewPost</code> 通过调用 <code>DraftPost</code> 的 <code>request_review</code> 创建,<code>approve</code> 方法将 <code>PendingReviewPost</code> 变为发布的 <code>Post</code></span></p>
|
||
<p><code>request_review</code> 和 <code>approve</code> 方法获取 <code>self</code> 的所有权,因此会消费 <code>DraftPost</code> 和 <code>PendingReviewPost</code> 实例,并分别转换为 <code>PendingReviewPost</code> 和 发布的 <code>Post</code>。这样在调用 <code>request_review</code> 之后就不会遗留任何 <code>DraftPost</code> 实例,后者同理。<code>PendingReviewPost</code> 并没有定义 <code>content</code> 方法,所以类似 <code>DraftPost</code> 尝试读取它的内容是一个编译错误。因为唯一得到定义了 <code>content</code> 方法的 <code>Post</code> 实例的途径是调用 <code>PendingReviewPost</code> 的 <code>approve</code> 方法,而得到 <code>PendingReviewPost</code> 的唯一办法是调用 <code>DraftPost</code> 的 <code>request_review</code> 方法,现在我们就将发博文的工作流编码进了类型系统。</p>
|
||
<p>这也意味着不得不对 <code>main</code>做出一些小的修改。因为 <code>request_review</code> 和 <code>approve</code> 返回新实例而不是修改被调用的结构体,我们需要增加更多的 <code>let post =</code> 覆盖赋值来保存返回的实例。也不能再断言草案和等待审核的博文的内容为空字符串了,我们也不再需要他们:不能编译尝试使用这些状态下博文内容的代码。更新后的 <code>main</code> 的代码如列表 18-21 所示:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">extern crate blog;
|
||
use blog::Post;
|
||
|
||
fn main() {
|
||
let mut post = Post::new();
|
||
|
||
post.add_text("I ate a salad for lunch today");
|
||
|
||
let post = post.request_review();
|
||
|
||
let post = post.approve();
|
||
|
||
assert_eq!("I ate a salad for lunch today", post.content());
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 17-21: <code>main</code> 中使用新的博文工作流实现的修改</span></p>
|
||
<p>不得不修改 <code>main</code> 来重新赋值 <code>post</code> 使得这个实现不再完全遵守面向对象的状态模式:状态间的转换不再完全封装在 <code>Post</code> 实现中。然而,得益于类型系统和编译时类型检查我们得到了不可能拥有无效状态的属性!这确保了特定的 bug,比如显示未发布博文的内容,将在部署到生产环境之前被发现。</p>
|
||
<p>尝试在这一部分开始所建议的增加额外需求的任务来体会使用这个版本的代码是何感觉。</p>
|
||
<p>即便 Rust 能够实现面向对象设计模式,也有其他像将状态编码进类型这样的模式存在。这些模式有着不同于面向对象模式的权衡取舍。虽然你可能非常熟悉面向对象模式,重新思考这些问题来利用 Rust 提供的像在编译时避免一些 bug 这样有益功能。在 Rust 中面向对象模式并不总是最好的解决方案,因为 Rust 拥有像所有权这样的面向对象语言所没有的功能。</p>
|
||
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
|
||
<p>阅读本章后,不管你是否认为 Rust 是一个面向对象语言,现在你都见识了 trait 对象是一个 Rust 中获取部分面向对象功能的方法。动态分发可以通过牺牲一些运行时性能来为你的代码提供一些灵活性。这些灵活性可以用来实现有助于代码可维护性的面向对象模式。Rust 也有像所有权这样不同于面向对象语言的功能。面向对象模式并不总是利用 Rust 实力的最好方式。</p>
|
||
<p>接下来,让我们看看另一个提供了很多灵活性的 Rust 功能:模式。贯穿本书我们都曾简单的见过他们,但并没有见识过他们的全部本领。让我们开始吧!</p>
|
||
<a class="header" href="#模式用来匹配值的结构" name="模式用来匹配值的结构"><h1>模式用来匹配值的结构</h1></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch18-00-patterns.md">ch18-00-patterns.md</a>
|
||
<br>
|
||
commit 3d47ebddad51b0080a19857e1495675a8e9376ef</p>
|
||
</blockquote>
|
||
<p>模式是 Rust 中特殊的语法,它用来匹配类型中的结构,无论类型是简单还是复杂。模式由一些常量组成;解构数组、枚举、结构体或者是元组;变量、通配符和占位符。这些部分描述了我们要处理的数据的“形状”。</p>
|
||
<p>我们通过将一些值与模式相比较来使用它。如果模式匹配这些值,我们对值部分进行相应处理。回忆一下第六章讨论 <code>match</code> 表达式时像硬币分类器那样使用模式。我们可以为形状中的片段命名,就像在第六章中命名出现在二十五美分硬币上的州那样,如果数据符合这个形状,就可以使用这些命名的片段。</p>
|
||
<p>本章是所有模式相关内容的参考。我们将涉及到使用模式的有效位置,<em>refutable</em> 与 <em>irrefutable</em> 模式的区别,和你可能会见到的不同类型的模式语法。</p>
|
||
<a class="header" href="#所有可能会用到模式的位置" name="所有可能会用到模式的位置"><h2>所有可能会用到模式的位置</h2></a>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch18-01-all-the-places-for-patterns.md">ch18-01-all-the-places-for-patterns.md</a>
|
||
<br>
|
||
commit 4ca9e513e532a4d229ab5af7dfcc567129623bf4</p>
|
||
</blockquote>
|
||
<p>模式出现在 Rust 的很多地方。你已经在不经意间使用了很多模式!本部分是一个所有有效模式位置的参考。</p>
|
||
<a class="header" href="#match-分支" name="match-分支"><h3><code>match</code> 分支</h3></a>
|
||
<p>如第六章所讨论的,一个模式常用的位置是 <code>match</code> 表达式的分支。在形式上 <code>match</code> 表达式由 <code>match</code> 关键字、用于匹配的值和一个或多个分支构成。这些分支包含一个模式和在值匹配分支的模式时运行的表达式:</p>
|
||
<pre><code>match VALUE {
|
||
PATTERN => EXPRESSION,
|
||
PATTERN => EXPRESSION,
|
||
PATTERN => EXPRESSION,
|
||
}
|
||
</code></pre>
|
||
<a class="header" href="#穷尽性和默认模式-_" name="穷尽性和默认模式-_"><h4>穷尽性和默认模式 <code>_</code></h4></a>
|
||
<p><code>match</code> 表达式必须是穷尽的。当我们把所有分支的模式都放在一起,<code>match</code> 表达式所有可能的值都应该被考虑到。一个确保覆盖每个可能值的方法是在最后一个分支使用捕获所有的模式,比如一个变量名。一个匹配任何值的名称永远也不会失败,因此可以覆盖之前分支模式匹配剩下的情况。</p>
|
||
<p>这有一个额外的模式经常被用于结尾的分支:<code>_</code>。它匹配所有情况,不过它从不绑定任何变量。这在例如只希望在某些模式下运行代码而忽略其他值的时候很有用。</p>
|
||
<a class="header" href="#if-let-表达式" name="if-let-表达式"><h3><code>if let</code> 表达式</h3></a>
|
||
<p>第六章讨论过了 <code>if let</code> 表达式,以及它是如何成为编写等同于只关心一个情况的 <code>match</code> 语句的简写的。<code>if let</code> 可以对应一个可选的 <code>else</code> 和代码在 <code>if let</code> 中的模式不匹配时运行。</p>
|
||
<p>列表 18-1 展示了甚至可以组合并匹配 <code>if let</code>、<code>else if</code> 和 <code>else if let</code>。这些代码展示了一系列针对不同条件的检查来决定背景颜色应该是什么。为了达到这个例子的目的,我们创建了硬编码值的变量,在真实程序中则可能由询问用户获得。如果用户指定了中意的颜色,我们将使用它作为背景颜色。如果今天是星期二,背景颜色将是绿色。如果用户指定了他们的年龄字符串并能够成功将其解析为数字的话,我们将根据这个数字使用紫色或者橙色。最后,如果没有一个条件符合,背景颜色将是蓝色:</p>
|
||
<p><span class="filename">文件名: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn main() {
|
||
let favorite_color: Option<&str> = None;
|
||
let is_tuesday = false;
|
||
let age: Result<u8, _> = "34".parse();
|
||
|
||
if let Some(color) = favorite_color {
|
||
println!("Using your favorite color, {}, as the background", color);
|
||
} else if is_tuesday {
|
||
println!("Tuesday is green day!");
|
||
} else if let Ok(age) = age {
|
||
if age > 30 {
|
||
println!("Using purple as the background color");
|
||
} else {
|
||
println!("Using orange as the background color");
|
||
}
|
||
} else {
|
||
println!("Using blue as the background color");
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 18-1: 结合 <code>if let</code>、<code>else if</code>、<code>else if let</code> 和 <code>else</code></span></p>
|
||
<p>这个条件结构允许我们支持复杂的需求。使用这里硬编码的值,例子会打印出 <code>Using purple as the background color</code>。</p>
|
||
<p>注意 <code>if let</code> 也可以像 <code>match</code> 分支那样引入覆盖变量:<code>if let Ok(age) = age</code> 引入了一个新的覆盖变量 <code>age</code>,它包含 <code>Ok</code> 成员中的值。这也意味着 <code>if age > 30</code> 条件需要位于这个代码块内部;不能将两个条件组合为 <code>if let Ok(age) = age && age > 30</code>,因为我们希望与 30 进行比较的被覆盖的 <code>age</code> 直到大括号开始的新作用域才是有效的。</p>
|
||
<p>另外注意这样有很多情况的条件并没有 <code>match</code> 表达式强大,因为其穷尽性没有为编译器所检查。如果去掉最后的 <code>else</code> 块而遗漏处理一些情况,编译器也不会报错。这个例子可能过于复杂以致难以重写为一个可读的 <code>match</code>,所以需要额外注意处理了所有的情况,因为编译器不会为我们检查穷尽性。</p>
|
||
<a class="header" href="#while-let" name="while-let"><h3><code>while let</code></h3></a>
|
||
<p>一个与 <code>if let</code> 类似的结构体是 <code>while let</code>:它允许只要模式匹配就一直进行 <code>while</code> 循环。列表 18-2 展示了一个使用 <code>while let</code> 的例子,它使用 vector 作为栈并打以先进后出的方式打印出 vector 中的值:</p>
|
||
<pre><code class="language-rust">let mut stack = Vec::new();
|
||
|
||
stack.push(1);
|
||
stack.push(2);
|
||
stack.push(3);
|
||
|
||
while let Some(top) = stack.pop() {
|
||
println!("{}", top);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 18-2: 使用 <code>while let</code> 循环只要 <code>stack.pop()</code> 返回 <code>Some</code>就打印出其值</span></p>
|
||
<p>这个例子会打印出 3、2 和 1。<code>pop</code> 方法取出 vector 的最后一个元素并返回<code>Some(value)</code>,如果 vector 是空的,它返回 <code>None</code>。<code>while</code> 循环只要 <code>pop</code> 返回 <code>Some</code> 就会一直运行其块中的代码。一旦其返回 <code>None</code>,<code>while</code>循环停止。我们可以使用 <code>while let</code> 来弹出栈中的每一个元素。</p>
|
||
<a class="header" href="#for-循环" name="for-循环"><h3><code>for</code> 循环</h3></a>
|
||
<p><code>for</code> 循环,如同第三章所讲的,是 Rust 中最常见的循环结构。那一章所没有讲到的是 <code>for</code> 可以获取一个模式。列表 18-3 中展示了如何使用 <code>for</code> 循环来解构一个元组。<code>enumerate</code> 方法适配一个迭代器来产生元组,其包含值和值的索引:</p>
|
||
<pre><code class="language-rust">let v = vec![1, 2, 3];
|
||
|
||
for (index, value) in v.iter().enumerate() {
|
||
println!("{} is at index {}", value, index);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 18-3: 在 <code>for</code> 循环中使用模式来解构 <code>enumerate</code> 返回的元组</span></p>
|
||
<p>这会打印出:</p>
|
||
<pre><code>1 is at index 0
|
||
2 is at index 1
|
||
3 is at index 2
|
||
</code></pre>
|
||
<p>第一个 <code>enumerate</code> 调用会产生元组 <code>(0, 1)</code>。当这个匹配模式 <code>(index, value)</code>,<code>index</code> 将会是 0 而 <code>value</code> 将会是 1。</p>
|
||
<a class="header" href="#let-语句" name="let-语句"><h3><code>let</code> 语句</h3></a>
|
||
<p><code>match</code> 和 <code>if let</code> 都是本书之前明确讨论过的使用模式的位置,不过他们不是仅有的<strong>使用过</strong>模式的地方。例如,考虑一下这个直白的 <code>let</code> 变量赋值:</p>
|
||
<pre><code class="language-rust">let x = 5;
|
||
</code></pre>
|
||
<p>本书进行了不下百次这样的操作。你可能没有发觉,不过你这正是在使用模式!<code>let</code> 语句更为正式的样子如下:</p>
|
||
<pre><code>let PATTERN = EXPRESSION;
|
||
</code></pre>
|
||
<p>我们见过的像 <code>let x = 5;</code> 这样的语句中变量名位于 <code>PATTERN</code> 位置;变量名不过是形式特别朴素的模式。</p>
|
||
<p>通过 <code>let</code>,我们将表达式与模式比较,并为任何找到的名称赋值。所以例如 <code>let x = 5;</code> 的情况,<code>x</code> 是一个模式代表“将匹配到的值绑定到变量 x”。同时因为名称 <code>x</code> 是整个模式,这个模式实际上等于“将任何值绑定到变量 <code>x</code>,不过它是什么”。</p>
|
||
<p>为了更清楚的理解 <code>let</code> 的模式匹配的方面,考虑列表 18-4 中使用 <code>let</code> 和模式解构一个元组:</p>
|
||
<pre><code class="language-rust">let (x, y, z) = (1, 2, 3);
|
||
</code></pre>
|
||
<p><span class="caption">列表 18-4: 使用模式解构元组并一次创建三个变量</span></p>
|
||
<p>这里有一个元组与模式匹配。Rust 会比较值 <code>(1, 2, 3)</code> 与模式 <code>(x, y, z)</code> 并发现值匹配这个模式。在这个例子中,将会把 <code>1</code> 绑定到 <code>x</code>,<code>2</code> 绑定到 <code>y</code>, <code>3</code> 绑定到 <code>z</code>。你可以将这个元组模式看作是将三个独立的变量模式结合在一起。</p>
|
||
<p>在第十六章中我们见过另一个解构元组的例子,列表 16-6 中,那里解构 <code>mpsc::channel()</code> 的返回值为 <code>tx</code>(发送者)和 <code>rx</code>(接收者)。</p>
|
||
<a class="header" href="#函数参数" name="函数参数"><h3>函数参数</h3></a>
|
||
<p>类似于 <code>let</code>,函数参数也可以是模式。列表 18-5 中的代码声明了一个叫做 <code>foo</code> 的函数,它获取一个 <code>i32</code> 类型的参数 <code>x</code>,这看起来应该很熟悉:</p>
|
||
<pre><code class="language-rust">fn foo(x: i32) {
|
||
// code goes here
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 18-5: 在参数中使用模式的函数签名</span></p>
|
||
<p><code>x</code> 部分就是一个模式!类似于之前对 <code>let</code> 所做的,可以在函数参数中匹配元组。列表 18-6 展示了如何可以将传递给函数的元组拆分为值:</p>
|
||
<p><span class="filename">文件名: src/main.rs</span></p>
|
||
<pre><code class="language-rust">fn print_coordinates(&(x, y): &(i32, i32)) {
|
||
println!("Current location: ({}, {})", x, y);
|
||
}
|
||
|
||
fn main() {
|
||
let point = (3, 5);
|
||
print_coordinates(&point);
|
||
}
|
||
</code></pre>
|
||
<p><span class="caption">列表 18-6: 一个在参数中解构元组的函数</span></p>
|
||
<p>这会打印出 <code>Current location: (3, 5)</code>。当传递值 <code>&(3, 5)</code> 给 <code>print_coordinates</code> 时,这个值会匹配模式 <code>&(x, y)</code>,<code>x</code> 得到了值 3,而 <code>y</code>得到了值 5。</p>
|
||
<p>因为如第十三章所讲闭包类似于函数,也可以在闭包参数中使用模式。</p>
|
||
<p>在这些可以使用模式的位置中的一个区别是,对于 <code>for</code> 循环、<code>let</code> 和函数参数,其模式必须是 <em>irrefutable</em> 的。接下来让我们讨论这个。</p>
|
||
|
||
</div>
|
||
|
||
<!-- Mobile navigation buttons -->
|
||
|
||
|
||
|
||
|
||
</div>
|
||
|
||
|
||
|
||
|
||
|
||
</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>
|