mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-09 08:51:18 +08:00
4143 lines
303 KiB
HTML
4143 lines
303 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></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">
|
||
<h1>介绍</h1>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch01-00-introduction.md">ch01-00-introduction.md</a>
|
||
<br>
|
||
commit c51c14215d2ee2cb481bc8a942a3769c6d9a2e1a</p>
|
||
</blockquote>
|
||
<p>欢迎阅读“Rust 程序设计语言”,一本关于 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,并向你展示如何使用 Rust 多样的功能,同时了解它们在后台是如何执行的。</p>
|
||
<h2>为本书做出贡献</h2>
|
||
<p>本书是开源的。如果你发现任何错误,请不要犹豫,<a href="https://github.com/rust-lang/book">在 GitHub 上</a>发起 issue 或提交 pull request。</p>
|
||
<h2>安装</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch01-01-installation.md">ch01-01-installation.md</a>
|
||
<br>
|
||
commit f828919e62aa542aaaae03c1fb565da42374213e</p>
|
||
</blockquote>
|
||
<p>使用 Rust 的第一步是安装。你需要联网来执行本章的命令,因为我们要从网上下载 Rust。</p>
|
||
<p>我们将会展示很多使用终端的命令,并且这些代码都以<code>$</code>开头。并不需要真正输入<code>$</code>,它们在这里代表每行指令的开头。在网上会看到很多使用这个惯例的教程和例子:<code>$</code>代表以常规用户运行命令,<code>#</code>代表需要用管理员运行的命令。没有以<code>$</code>(或<code>#</code>)的行通常是之前命令的输出。</p>
|
||
<h3>在 Linux 或 Mac 上安装</h3>
|
||
<p>如果你使用 Linux 或 Mac,所有需要做的就是打开一个终端并输入:</p>
|
||
<pre><code class="language-sh">$ curl https://sh.rustup.rs -sSf | sh
|
||
</code></pre>
|
||
<p>这会下载一个脚本并开始安装。你可能被提示要输入密码。如果一切顺利,将会出现如下内容:</p>
|
||
<pre><code class="language-sh">Rust is installed now. Great!
|
||
</code></pre>
|
||
<p>当然,如果你不赞成<code>curl | sh</code>这种模式,可以随意下载、检查和运行这个脚本。</p>
|
||
<h3>在 Windows 上安装</h3>
|
||
<p>在 Windows 上,前往<a href="https://rustup.rs/">https://rustup.rs</a><!-- ignore -->并按照说明下载<code>rustup-init.exe</code>。运行并遵循它提供的其余指示。</p>
|
||
<p>本书其余 Windows 相关的命令假设你使用<code>cmd</code>作为你的 shell。如果你使用不同的 shell,可能能够执行 Linux 和 Mac 用户相同的命令。如果都不行,查看所使用的 shell 的文档。</p>
|
||
<h3>自定义安装</h3>
|
||
<p>如果有理由倾向于不使用 rustup.rs,请查看<a href="https://www.rust-lang.org/install.html">Rust 安装页面</a>获取其他选择。</p>
|
||
<h3>卸载</h3>
|
||
<p>卸载 Rust 同安装一样简单。在 shell 中运行卸载脚本</p>
|
||
<pre><code class="language-sh">$ rustup self uninstall
|
||
</code></pre>
|
||
<h3>故障排除</h3>
|
||
<p>安装完 Rust 后,打开 shell,输入:</p>
|
||
<pre><code class="language-sh">$ rustc --version
|
||
</code></pre>
|
||
<p>应该能看到类似这样的版本号、提交 hash 和提交日期,对应你安装时的最新稳定版本:</p>
|
||
<pre><code class="language-sh">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>如果还是不能运行,有许多可以获取帮助的地方。最简单的是 irc.mozilla.org 上的 IRC 频道 <a href="irc://irc.mozilla.org/#rust-beginners">#rust-beginners</a> 和供一般讨论之用的 <a href="irc://irc.mozilla.org/#rust">#rust</a>,我们可以使用 <a href="http://chat.mibbit.com/?server=irc.mozilla.org&channel=%23rust-beginners,%23rust">Mibbit</a> 访问。然后我们就可以和其他能提供帮助的 Rustacean(我们这些人自嘲的绰号)聊天了。其它给力的资源包括<a href="https://users.rust-lang.org/">用户论坛</a>和<a href="http://stackoverflow.com/questions/tagged/rust">Stack Overflow</a>。</p>
|
||
<h3>本地文档</h3>
|
||
<p>安装程序也包含一份本地文档的拷贝,你可以离线阅读它们。输入<code>rustup doc</code>将在浏览器中打开本地文档。</p>
|
||
<p>任何你太确认标准库提供的类型或函数是干什么的时候,使用文档 API 查找!</p>
|
||
<h2>Hello, World!</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch01-02-hello-world.md">ch01-02-hello-world.md</a>
|
||
<br>
|
||
commit ccbeea7b9fe115cd545881618fe14229d18b307f</p>
|
||
</blockquote>
|
||
<p>现在你已经安装好了 Rust,让我们来编写你的第一个 Rust 程序。当学习一门新语言的时候,编写一个在屏幕上打印 “Hello, world!” 文本的小程序是一个传统,而在这一部分,我们将遵循这个传统。</p>
|
||
<blockquote>
|
||
<p>注意:本书假设你熟悉基本的命令行操作。Rust 本身并不对你的编辑器,工具和你的代码存放在何处有什么特定的要求,所以如果你比起命令行更喜欢 IDE,请随意选择你喜欢的 IDE。</p>
|
||
</blockquote>
|
||
<h3>创建项目文件夹</h3>
|
||
<p>首先,创建一个文件夹来编写 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个<strong>项目</strong>目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹:</p>
|
||
<p>Linux 和 Mac:</p>
|
||
<pre><code class="language-sh">$ 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>
|
||
<h3>编写并运行 Rust 程序</h3>
|
||
<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 class="language-sh">$ rustc main.rs
|
||
$ ./main
|
||
Hello, world!
|
||
</code></pre>
|
||
<p>在 Windows 上,运行<code>.\main.exe</code>而不是<code>./main</code>。不管使用何种系统,你应该在终端看到<code>Hello, world!</code>字符串。如果你做到了,那么恭喜你!你已经正式编写了一个 Rust 程序。你是一名 Rust 程序员了!欢迎入坑。</p>
|
||
<h3>分析 Rust 程序</h3>
|
||
<p>现在,让我们回过头来仔细看看你的“Hello, world!”程序到底发生了什么。这是谜题的第一片:</p>
|
||
<pre><code class="language-rust">fn main() {
|
||
|
||
}
|
||
</code></pre>
|
||
<p>这几行定义了一个 Rust <strong>函数</strong>。<code>main</code> 函数是特殊的:这是每一个可执行的 Rust 程序首先运行的函数(译者注:入口点)。第一行表示“定义一个叫 <code>main</code> 的函数,没有参数也没有返回值。”如果有参数的话,它们应该出现在括号中,<code>(</code>和<code>)</code>。</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>。这叫做 Rust <strong>宏</strong>,是如何进行 Rust 元编程(metaprogramming)的关键所在。相反如果调用一个函数的话,它应该看起来像这样:<code>println</code>(没有<code>!</code>)。我们将在 24 章更加详细的讨论 Rust 宏,不过现在你只需记住当看到符号 <code>!</code> 的时候,就代表在调用一个宏而不是一个普通的函数。</p>
|
||
<p>接下来,<code>"Hello, world!"</code> 是一个 <strong>字符串</strong>。我们把这个字符串作为一个参数传递给<code>println!</code>,它负责在屏幕上打印这个字符串。轻松加愉快!(⊙o⊙)</p>
|
||
<p>这一行以一个分号结尾(<code>;</code>)。<code>;</code>代表这个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以<code>;</code>结尾。</p>
|
||
<h3>编译和运行是两个步骤</h3>
|
||
<p>在“编写并运行 Rust 程序”部分,展示了如何运行一个新创建的程序。现在我们将拆分并检查每一步操作。</p>
|
||
<p>在运行一个 Rust 程序之前,必须编译它。可以输入<code>rustc</code>命令来使用 Rust 编译器并像这样传递你源文件的名字:</p>
|
||
<pre><code class="language-sh">$ rustc main.rs
|
||
</code></pre>
|
||
<p>如果你来自 C 或 C++ 背景,你会发现这与<code>gcc</code>和<code>clang</code>类似。编译成功后,Rust 应该会输出一个二进制可执行文件,在 Linux 或 OSX 上在 shell 中你可以通过<code>ls</code>命令看到如下:</p>
|
||
<pre><code class="language-sh">$ 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 class="language-sh">$ ./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>
|
||
<h2>Hello, Cargo!</h2>
|
||
<p>Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理它们的 Rust 项目,因为它使得很多任务变得更轻松。例如,Cargo负责构建代码、下载代码依赖的库并编译这些库。我们把代码需要的库叫做 <strong>依赖</strong>(<em>dependencies</em>)。</p>
|
||
<p>最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的部分。随着你编写更加复杂的 Rust 程序,你会想要添加依赖,那么如果你使用 Cargo 开始的话,这将会变得简单许多。</p>
|
||
<p>由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo:</p>
|
||
<pre><code class="language-sh">$ cargo --version
|
||
</code></pre>
|
||
<p>如果看到了版本号,一切 OK!如果出现一个类似“<code>command not found</code>”的错误,那么你应该查看安装方式的文档来确定如何单独安装 Cargo。</p>
|
||
<h3>使用 Cargo 创建项目</h3>
|
||
<p>让我们使用 Cargo 来创建一个新项目并看看与<code>hello_world</code>项目有什么不同。回到项目目录(或者任何你决定放置代码的目录):</p>
|
||
<p>Linux 和 Mac:</p>
|
||
<pre><code class="language-sh">$ cd ~/projects
|
||
</code></pre>
|
||
<p>Windows:</p>
|
||
<pre><code class="language-cmd">> cd %USERPROFILE%\projects
|
||
</code></pre>
|
||
<p>并在任何操作系统运行:</p>
|
||
<pre><code class="language-sh">$ 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>参数。</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>
|
||
<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 期望源文件位于 src 目录,这样将项目根目录留给 README、license 信息、配置文件和其他跟代码无关的文件。这样,Cargo 帮助你保持项目干净整洁。一切井井有条。</p>
|
||
<p>如果没有使用 Cargo 开始项目,正如我们在 <em>hello_world</em> 目录中的项目,可以把它转化为一个 Cargo 使用的项目,通过将代码放入 <em>src</em> 目录并创建一个合适的 <em>Cargo.toml</em>。</p>
|
||
<h3>构建并运行 Cargo 项目</h3>
|
||
<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 class="language-sh">$ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
|
||
Hello, world!
|
||
</code></pre>
|
||
<p>好的!如果一切顺利,<code>Hello, world!</code>应该再次打印在终端上。</p>
|
||
<p>第一次运行的时候也会使 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 class="language-sh">$ cargo run
|
||
Running `target/debug/hello_cargo`
|
||
Hello, world!
|
||
</code></pre>
|
||
<p>注意这一次,并没有出现告诉我们 Cargo 正在编译 <code>hello_cargo</code> 的输出。Cargo 发现文件并没有被改变,所以只是运行了二进制文件。如果修改了源文件的话,将会出现像这样的输出:</p>
|
||
<pre><code class="language-sh">$ 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>
|
||
<h3>发布构建</h3>
|
||
<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>
|
||
<h3>把 Cargo 当作习惯</h3>
|
||
<p>对于简单项目, Cargo 并不能比<code>rustc</code>提供更多的价值,不过随着开发的进行终将体现它的价值。对于拥有多个 crate 的复杂项目,可以仅仅运行<code>cargo build</code>,然后一切将有序运行。即便这个项目很简单,现在它使用了很多接下来你 Rust 程序生涯将会用到的实用工具。事实上,无形中你可以使用下面的命令开始所有你想要从事的项目:</p>
|
||
<pre><code class="language-sh">$ git clone someurl.com/someproject
|
||
$ cd someproject
|
||
$ carg
|
||
</code></pre>
|
||
<blockquote>
|
||
<p>注意:如果你想要查看 Cargo 的更多细节,请阅读官方的 <a href="http://doc.crates.io/guide.html">Cargo guide</a>,它覆盖了其所有的功能。</p>
|
||
</blockquote>
|
||
<h1>猜猜看</h1>
|
||
<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 77370c073661548dd56bbcb43cc64713585acbba</p>
|
||
</blockquote>
|
||
<p>让我们通过自己动手的方式一起完成一个项目来快速上手 Rust!本章通过展示如何在真实的项目中运用的方式向你介绍一些常用的 Rust 概念。你将会学到<code>let</code>、<code>match</code>、方法、关联函数、使用外部 crate 等更多的知识!接下来的章节会探索这些概念的细节。在这一章,我们练习基础。</p>
|
||
<p>我们会实现一个经典新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会提示玩家输入一个猜测。当输入了一个猜测后,它会告诉提示猜测是太大了还是太小了。猜对了,它会打印出祝贺并退出。</p>
|
||
<h2>准备一个新项目</h2>
|
||
<p>要创建一个新项目,进入你在第一章创建的<strong>项目</strong>目录,并使用 Cargo 创建它,像这样:</p>
|
||
<pre><code class="language-sh">$ 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>在相同的步骤编译并运行这个“Hello, world!”程序:</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>
|
||
<h2>处理一次猜测</h2>
|
||
<p>程序的第一部分会请求用户输入,处理输入,并检查输入是否为期望的形式。首先,允许玩家输入一个猜测。在 <em>src/main.rs</em> 中输入列表 2-1 中的代码。</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 2-1: Code to get a guess from the user and print it out</p>
|
||
</figcaption>
|
||
</figure>
|
||
<p>这些代码包含很多信息,所以让我们一点一点地过一遍。为了获取用户输入并接着打印结果作为输出,我们需要从标准库(被称为<code>std</code>)中引用<code>io</code>(输入/输出)库:</p>
|
||
<pre><code class="language-rust,ignore">use std::io;
|
||
</code></pre>
|
||
<p>Rust 默认只在每个程序的 <a href="https://doc.rust-lang.org/std/prelude/"><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>
|
||
<h3>用变量储存值</h3>
|
||
<p>接下来,创建一个地方储存用户输入,像这样:</p>
|
||
<pre><code class="language-rust,ignore">let mut guess = String::new();
|
||
</code></pre>
|
||
<p>现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个<code>let</code>语句,用来创建<strong>变量</strong>。这里是另外一个例子:</p>
|
||
<pre><code class="language-rust,ignore">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="../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="../std/io/struct.Stdin.html"><code>std::io::Stdin</code></a><!-- ignore -->的实例,这是一个代表终端标准输入句柄的类型。</p>
|
||
<p>代码的下一部分,<code>.read_line(&mut guess)</code>,调用 <a href="../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>的工作是把获取任何用户键入到标准输入的字符并放入一个字符串中,所以它获取字符串作为一个参数。这个字符串需要是可变的,这样这个方法就可以通过增加用户的输入来改变字符串的内容。</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,ignore">.expect("Failed to read line");
|
||
</code></pre>
|
||
<p>当使用<code>.foo()</code>语法调用方法时,明智的选择是换行并留出空白(缩进)来把长的代码行拆开。我们可以把代码写成这样:</p>
|
||
<pre><code class="language-rust,ignore">io::stdin().read_line(&mut guess).expect("Failed to read line");
|
||
</code></pre>
|
||
<p>不过,过长的代码行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。</p>
|
||
<h3>使用<code>Result</code>类型来处理潜在的错误</h3>
|
||
<p>之前提到过,<code>read_line</code>将用户输入放入到传递给它字符串中,不过它也返回一个值————一个<a href="../std/io/type.Result.html"><code>io::Result</code></a><!-- ignore -->。Rust 标准库中有很多叫做<code>Result</code>的类型。一个<a href="../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>Ok</code>成员之中包含成功生成的值。<code>Err</code>意味着操作失败,<code>Err</code>之中包含操作是为什么或如何失败的信息。</p>
|
||
<p><code>Result</code>类型的作用是编码错误处理信息。<code>Result</code>类型的值,正如其他任何类型,拥有定义于其上的方法。<code>io::Result</code>的实例拥有<a href="../std/result/enum.Result.html#method.expect"><code>expect</code>方法</a><!-- ignore -->可供调用。如果<code>io::Result</code>实例的值是<code>Err</code>,<code>expect</code>会导致程序崩溃并显示显示你作为参数传递给<code>expect</code>的信息。如果<code>io::Result</code>实例的值是<code>Ok</code>,<code>expect</code>会获取<code>Ok</code>中的值并原原本本的返回给你,这样就可以使用它了。在本例中,返回值是用户输入到标准输入的一些字符。</p>
|
||
<p>如果不使用<code>expect</code>,程序也能编译,不过会出现一个警告:</p>
|
||
<pre><code class="language-sh">$ 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>
|
||
<h3>使用<code>println!</code>占位符打印值</h3>
|
||
<p>除了位于结尾的大括号,目前为止编写的代码就只有一行代码值得讨论一下了,就是这一行:</p>
|
||
<pre><code class="language-rust,ignore">println!("You guessed: {}", guess);
|
||
</code></pre>
|
||
<p>这行代码打印出存储了用户输入的字符串。这对<code>{}</code>是一个在特定位置预留值的占位符。可以使用<code>{}</code>打印多个值:第一个<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>
|
||
<h3>测试第一部分代码</h3>
|
||
<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>
|
||
<h2>生成一个秘密数字</h2>
|
||
<p>接下来,需要生成一个秘密数字,用户会尝试猜测它。秘密数字应该每次都不同,这样多玩几次才会有意思。生成一个 1 到 100 之间的随机数这样游戏也不会太难。Rust 标准库中还未包含随机数功能。然而,Rust 团队确实提供了一个<a href="https://crates.io/crates/rand"><code>rand</code> crate</a>。</p>
|
||
<h2>使用 crate 来增加更多功能</h2>
|
||
<p>记住 <em>crate</em> 是一个 Rust 代码的包。我们正在构建的项目是一个<strong>二进制 crate</strong>,它生成一个可执行文件。 <code>rand</code> crate 是一个 <em>库 crate</em>,它包含意在被其他程序使用的代码。</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 和需要的 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>
|
||
<figure>
|
||
<pre><code class="language-text">$ 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>
|
||
<figcaption>
|
||
<p>Listing 2-2: The output from running <code>cargo build</code> after adding the rand crate
|
||
as a dependency</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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> 文件中也没有任何相关修改。Cargo 也知道代码没有做任何修改,所以它也不会重新编译代码。因为无事可做,它简单的退出了。如果打开 <em>src/main.rs</em> 文件,并做一些普通的修改,保存并再次构建,只会出现一行输出:</p>
|
||
<pre><code class="language-sh">$ cargo build
|
||
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
|
||
</code></pre>
|
||
<p>这一行表明 Cargo 只构建了对 <em>src/main.rs</em> 文件做出的微小修改。依赖没有被修改,所以 Cargo 知道可以复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码。</p>
|
||
<h4>The <em>Cargo.lock</em> 文件确保构建是可重现的</h4>
|
||
<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> 文件中。当将来构建项目时,Cargo 发现 <em>Cargo.lock</em> 存在就会使用这里指定的版本,而不是重新进行所有版本的计算。这使得你拥有了一个自动的可重现的构建。换句话说,项目会继续使用<code>0.3.14</code>直到你显式升级,多亏了 <em>Cargo.lock</em> 文件。我们将会在这个文件编写全部的代码。</p>
|
||
<h4>更新 crate 到一个新版本</h4>
|
||
<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 class="language-sh">$ 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 中可用的 crate 并根据你指定新版本重新计算<code>rand</code>的要求。</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>
|
||
<h3>生成一个随机数</h3>
|
||
<p>让我们开始<strong>使用</strong><code>rand</code>。下一步是更新 <em>src/main.rs</em>,如列表 2-3:</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<pre><code class="language-rust,ignore">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>
|
||
<figcaption>
|
||
<p>Listing 2-3: Code changes needed in order to generate a random number</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>use rand::Rng</code>语句引入到作用域的<code>Rng</code> trait 定义。<code>gen_range</code>方法获取两个数作为参数并生成一个两者之间的随机数。它包含下限但不包含上限,所以需要指定<code>1</code>和<code>101</code>来请求一个<code>1</code>和<code>100</code>之间的数。</p>
|
||
<p>并不仅仅能够知道该引用哪个 trait 和该从 crate 中使用哪个方法。如何使用 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 class="language-sh">$ 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>
|
||
<h2>比较猜测与秘密数字</h2>
|
||
<p>现在有了用户输入和一个随机数,我们可以比较他们。这个步骤如列表 2-4:</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<pre><code class="language-rust,ignore">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>
|
||
<figcaption>
|
||
<p>Listing 2-4: Handling the possible return values of comparing two numbers</p>
|
||
</figcaption>
|
||
</figure>
|
||
<p>新代码的第一行是另一个<code>use</code>,从标准库引入了一个叫做<code>std::cmp::Ordering</code>的类型到作用域。<code>Ordering</code>是另一个枚举,像<code>Result</code>一样,不过<code>Ordering</code>的成员是<code>Less</code>、<code>Greater</code>和<code>Equal</code>。这是你比较两个值时可能出现三种结果。</p>
|
||
<p>接着在底部的五行新代码使用了<code>Ordering</code>类型:</p>
|
||
<pre><code class="language-rust,ignore">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>cmp</code>返回一个使用<code>use</code>语句引用的<code>Ordering</code>枚举的成员。我们使用一个<a href="ch06-02-match.html"><code>match</code></a><!-- ignore -->表达式根据对<code>guess</code>和<code>secret_number</code>中的值调用<code>cmp</code>后返回的哪个<code>Ordering</code>枚举成员来决定接下来干什么。</p>
|
||
<p>一个<code>match</code>表达式由 <strong>分支(arms)</strong> 构成。一个分支包含一个 <strong>模式</strong>(<em>pattern</em>)和代码,这些代码在<code>match</code>表达式开头给出的值符合分支的模式时将被执行。Rust 获取提供给<code>match</code>的值并挨个检查每个分支的模式。<code>match</code>结构和模式是 Rust 中非常强大的功能,它帮助你体现代码可能遇到的多种情形并帮助你处理全部的可能。这些功能将分别在第六章和第十九章详细介绍。</p>
|
||
<p>让我们看看一个使用这里的<code>match</code>表达式会发生什么的例子。假设用户猜了 50,这时随机生成的秘密数字是 38。当代码比较 50 与 38 时,<code>cmp</code>方法会返回<code>Ordering::Greater</code>,因为 50 比 38 要大。<code>Ordering::Greater</code>是<code>match</code>表达式得到的值。它检查第一个分支的模式,<code>Ordering::Less</code>,不过值<code>Ordering::Greater</code>并不匹配<code>Ordering::Less</code>。所以它忽略了这个分支的代码并移动到下一个分支。下一个分支的模式,<code>Ordering::Greater</code>,<strong>正确</strong>匹配了<code>Ordering::Greater</code>!这个分支关联的代码会被执行并在屏幕打印出<code>Too big!</code>。<code>match</code>表达式就此终止,因为在这个特定场景下没有检查最后一个分支的必要。</p>
|
||
<p>然而,列表 2-4 的代码并不能编译,尝试一下:</p>
|
||
<pre><code class="language-sh">$ 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 之间的值:<code>i32</code>,一个 32 位的数字;<code>u32</code>,一个 32 位无符号数字;<code>i64</code>,一个 64 位数字;等等。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,ignore">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,ignore">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>之前的值。这个功能经常用在类似需要把一个值从一种类型转换到另一种类型的场景。shadowing 允许我们复用<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>guess</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="../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>类型,非常像之前在 XX 页“使用<code>Result</code>类型来处理潜在的错误”部分讨论的<code>read_line</code>方法。这里再次类似的使用<code>expect</code>方法处理这个<code>Result</code>类型。如果<code>parse</code>因为不能从字符串生成一个数字而返回一个<code>Err</code>的<code>Result</code>成员时,<code>expect</code>会使游戏崩溃并打印提供给它的信息。如果<code>parse</code>能成功地将字符串转换为一个数字,它会返回<code>Result</code>的<code>Ok</code>成员,同时<code>expect</code>会返回<code>Ok</code>中我们需要的数字。</p>
|
||
<p>现在让我们运行程序!</p>
|
||
<pre><code class="language-sh">$ 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>
|
||
<h2>使用循环来允许多次猜测</h2>
|
||
<p><code>loop</code>关键字提供了一个无限循环。增加它后给了用户多次猜测的机会:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">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>快捷键来终止程序。不过这里还有另一个逃离这个贪得无厌的怪物的方法,就是在 XX 页“比较猜测”部分提到的<code>parse</code>:如果用户输入一个非数字回答,程序会崩溃。用户可以利用这一点来退出,如下所示:</p>
|
||
<pre><code class="language-sh">$ 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>
|
||
<h3>猜测正确后退出</h3>
|
||
<p>让我们增加一个<code>break</code>来在用户胜利时退出游戏:</p>
|
||
<p><span class="filename">Filename: src/main.rs</span></p>
|
||
<pre><code class="language-rust,ignore">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>
|
||
<h3>处理无效输入</h3>
|
||
<p>为了进一步改善游戏性,而不是在用户输入非数字时崩溃,需要让游戏忽略非数字从而用户可以继续猜测。可以通过修改<code>guess</code>从<code>String</code>转化为<code>u32</code>那部分代码来实现:</p>
|
||
<pre><code class="language-rust,ignore">let guess: u32 = match guess.trim().parse() {
|
||
Ok(num) => num,
|
||
Err(_) => continue,
|
||
};
|
||
</code></pre>
|
||
<p>从<code>expect</code>调用切换到<code>expect</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>parse</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 class="language-sh">$ 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>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<pre><code class="language-rust,ignore">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>
|
||
<figcaption>
|
||
<p>Listing 2-5: Complete code of the guessing game</p>
|
||
</figcaption>
|
||
</figure>
|
||
<h2>总结一下,</h2>
|
||
<p>此时此刻,你顺利完成了猜猜看游戏!恭喜!</p>
|
||
<p>这是一个通过动手实践的方式想你介绍许多 Rust 新知识的项目:<code>let</code>、<code>match</code>、方法、关联函数,使用外部 crate,等等。接下来的几章,我们将会详细学习这些概念。第三章涉及到大部分编程语言都有的概念,比如变量、数据类型和函数,以及如何在 Rust 中使用他们。第四章探索所有权(ownership),这是一个 Rust 同其他语言都不相同的功能。第五章讨论结构体和方法语法,而第六章侧重解释枚举。</p>
|
||
<h1>通用编程概念</h1>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch03-00-common-programming-concepts.md">ch03-00-common-programming-concepts.md</a>
|
||
<br>
|
||
commit 2067b6e2bff990bceb39ae8f35780bd3bed08644</p>
|
||
</blockquote>
|
||
<p>这一章涉及到几乎出现在所有编程语言中的概念,以及他们在 Rust 中如何工作。很多编程语言在核心概念上都是共通的。本章中展示的所有概念没有一个是 Rust 所特有的,不过我们会在 Rust 环境中讨论他们并解释他们的使用习惯。</p>
|
||
<p>具体的,我们将会学习变量,基本类型,函数,注释和控制流。这些基础知识将会出现在每一个 Rust 程序中,提早学习这些概念会使你在起步时拥有一个核心的基础。</p>
|
||
<!-- PROD: START BOX -->
|
||
<blockquote>
|
||
<h3>关键字</h3>
|
||
<p>Rust 语言有一系列被保留为只能被语言使用的<strong>关键字</strong>(<em>keywords</em>),如大部分语言一样。注意你不能使用这些关键字作为变量或函数的名称。大部分关键字有特殊的意义,并将会被用来进行 Rust 程序中的多种任务;一些关键字目前没有相关的功能不过为了将来可能添加进 Rust 的功能而被保留。可以在附录 A 中找到一份关键字的列表</p>
|
||
</blockquote>
|
||
<!-- PROD: END BOX --><h2>变量和可变性</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch03-01-variables-and-mutability.md">ch03-01-variables-and-mutability.md</a>
|
||
<br>
|
||
commit b0fab378c9c6a817d4f0080d7001d085017cdef8</p>
|
||
</blockquote>
|
||
<p>第二章中提到过,变量默认是<strong>不可变</strong>(<em>immutable</em>)的。这是 Rust 中许多鼓励以利用 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 class="language-sh">$ 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 class="language-sh">$ 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>
|
||
<h3>变量和常量的区别</h3>
|
||
<p>不能改变一个变量的值可能会使你想起另一个大部分编程语言都有的概念:<strong>常量</strong>(<em>constants</em>)。常量也是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。首先,不允许对常量使用<code>mut</code>:常量不光是默认不能改变,它总是不能改变。常量使用<code>const</code>关键字而不是<code>let</code>关键字声明,而且<em>必须</em>注明值的类型。现在我们准备在下一部分,“数据类型”,涉及到类型和类型注解,所以现在无需担心这些细节。常量可以在任何作用域声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。最后一个区别是常量只能用于常量表达式,而不能作为函数调用的结果或任何其他只在运行时使用到的值。</p>
|
||
<p>这是一个常量声明的例子,它的名称是<code>MAX_POINTS</code>而它的值是 100,000。Rust 常量的命名规范是使用大写字母和单词间使用下划线:</p>
|
||
<pre><code class="language-rust">const MAX_POINTS: u32 = 100_000;
|
||
</code></pre>
|
||
<p>常量在整个程序生命周期中都有效,位于它声明的作用域之中。这使得常量可以用作多个部分的代码可能需要知道的程序范围的值,例如一个游戏中任何玩家可以获得的最高分或者一年的秒数。</p>
|
||
<p>将用于整个程序的硬编码的值命名为常量(并编写文档)对为将来代码维护者表明值的意义是很有用的。它也能帮助你将硬编码的值至于一处以便将来可能需要修改他们。</p>
|
||
<h3>覆盖</h3>
|
||
<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 class="language-sh">$ 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 class="language-sh">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>
|
||
<h2>数据类型</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch03-02-data-types.md">ch03-02-data-types.md</a>
|
||
<br>
|
||
commit d05b7c63ff50b3f9126bb5533e0ba5dd424b83d1</p>
|
||
</blockquote>
|
||
<p>Rust 中的任何值都有一个具体的<strong>类型</strong>(<em>type</em>),这告诉了 Rust 它被指定为何种数据这样 Rust 就知道如何处理这些数据了。这一部分将讲到一些语言内建的类型。我们将这些类型分为两个子集:标量(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().unwrap();
|
||
</code></pre>
|
||
<p>如果这里不添加类型注解,Rust 会显示如下错误,它意味着编译器需要我们提供更多我们想要使用哪个可能的类型的信息:</p>
|
||
<pre><code class="language-sh">error[E0282]: unable to infer enough type information about `_`
|
||
--> src/main.rs:2:5
|
||
|
|
||
2 | let guess = "42".parse().unwrap();
|
||
| ^^^^^ cannot infer type for `_`
|
||
|
|
||
= note: type annotations or generic parameter binding required
|
||
</code></pre>
|
||
<p>在我们讨论各种数据类型时会看到不同的类型注解。</p>
|
||
<h3>标量类型</h3>
|
||
<p><strong>标量</strong>类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过他们,不过让我们深入了解他们在 Rust 中时如何工作的。</p>
|
||
<h4>整型</h4>
|
||
<p><strong>整数</strong>是一个没有小数部分的数字。我们在这一章的前面使用过一个整型,<code>i32</code>类型。这个类型声明表明在 32 位系统上它关联的值应该是一个有符号整数(因为这个<code>i</code>,与<code>u</code>代表的无符号相对)。表格 3-1 展示了 Rust 内建的整数类型。每一个变体的有符号和无符号列(例如,<em>i32</em>)可以用来声明对应的整数值。</p>
|
||
<figure>
|
||
<figcaption>
|
||
<p>Table 3-1: Integer Types in Rust</p>
|
||
</figcaption>
|
||
<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>
|
||
</figure>
|
||
<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>是可视化分隔符(visual separator),例如<code>1_000</code>位的。</p>
|
||
<figure>
|
||
<figcaption>
|
||
<p>Table 3-2: Integer Literals in Rust</p>
|
||
</figcaption>
|
||
<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>
|
||
</figure>
|
||
<p>那么如何知晓该使用哪种类型的数字呢?如果对此拿不定主意,Rust 的默认类型通常就是一个很好的选择,这个默认数字类型是<code>i32</code>:它通常是最快的,甚至是在 64 位系统上。使用<code>isize</code>或<code>usize</code>的主要场景是索引一些集合。</p>
|
||
<h4>浮点型</h4>
|
||
<p>Rust 也有两个主要的<strong>浮点数</strong>(<em>floating-point numbers</em>)类型,他们是有小数点的数字。Rust 的浮点数类型是<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>
|
||
<h4>数字运算符</h4>
|
||
<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>
|
||
<h4>布尔型</h4>
|
||
<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>
|
||
<h4>字符类型</h4>
|
||
<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>
|
||
<h3>复合类型</h3>
|
||
<p><strong>复合类型</strong>可以将多个其他类型的值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。</p>
|
||
<h4>将值组合进元组</h4>
|
||
<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>
|
||
<h4>数组</h4>
|
||
<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>
|
||
<h5>访问数组元素</h5>
|
||
<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>
|
||
<h5>无效的数组元素访问</h5>
|
||
<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 class="language-sh">$ 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>
|
||
<h2>函数如何工作</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch03-03-how-functions-work.md">ch03-03-how-functions-work.md</a>
|
||
<br>
|
||
commit 52b7fcbfdd35915cb21e6d492fb6c86764f53b47</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 class="language-sh">$ 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>
|
||
<h3>函数参数</h3>
|
||
<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>
|
||
<h3>函数体</h3>
|
||
<p>函数体由一系列的语句和一个可选的表达式构成。目前为止,我们只涉及到了没有结尾表达式的函数,不过我们见过表达式作为了语句的一部分。因为 Rust 是一个基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及他们是如何影响函数体的。</p>
|
||
<h3>语句与表达式</h3>
|
||
<p>我们已经用过语句与表达式了。<strong>语句</strong>(<em>Statements</em>)是执行一些操作但不返回值的指令。表达式(<em>Expressions</em>)计算并产生一个值。让我们看看一些例子:</p>
|
||
<p>使用<code>let</code>关键字创建变量并绑定一个值是一个语句。在列表 3-3 中,<code>let y = 6;</code>是一个语句:</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<pre><code class="language-rust">fn main() {
|
||
let y = 6;
|
||
}
|
||
</code></pre>
|
||
<figcaption>
|
||
<p>Listing 3-3: A <code>main</code> function declaration containing one statement.</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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 class="language-sh">$ 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>
|
||
<h3>函数的返回值</h3>
|
||
<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 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>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>
|
||
<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 class="language-sh">error[E0269]: not all control paths return a value
|
||
--> src/main.rs:7:1
|
||
|
|
||
7 | fn plus_one(x: i32) -> i32 {
|
||
| ^
|
||
|
|
||
help: consider removing this semicolon:
|
||
--> src/main.rs:8:10
|
||
|
|
||
8 | x + 1;
|
||
| ^
|
||
</code></pre>
|
||
<p>主要的错误信息,“并非所有控制路径都返回一个值”(“not all control paths return a value,”),揭示了代码的核心问题。函数<code>plus_one</code>的定义说明它要返回一个<code>i32</code>,不过语句并不返回一个值。因此,这个函数没有返回任何值,这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。</p>
|
||
<h2>注释</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch03-04-comments.md">ch03-04-comments.md</a>
|
||
<br>
|
||
commit 74d6fc999b986b74bf94edd6dcbb5a08a16c12de</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>
|
||
<h2>控制流</h2>
|
||
<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 784a3ec5e8b9c6bff456ab9f0efd4dabcc180dda</p>
|
||
</blockquote>
|
||
<p>通过条件是不是真来决定是否某些代码,或者根据条件是否为真来重复运行一段代码是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是<code>if</code>表达式和循环。</p>
|
||
<h3><code>if</code>表达式</h3>
|
||
<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>
|
||
<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 class="language-sh">$ 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 class="language-sh">$ 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 class="language-sh"> Compiling branches v0.1.0 (file:///projects/branches)
|
||
error[E0308]: mismatched types
|
||
--> src/main.rs:4:8
|
||
|
|
||
4 | if number {
|
||
| ^^^^^^ expected bool, found integral variable
|
||
|
|
||
= note: expected type `bool`
|
||
= note: found type `{integer}`
|
||
|
||
error: aborting due to previous error
|
||
Could not compile `branches`.
|
||
</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>
|
||
<h4>使用<code>else if</code>实现多重条件</h4>
|
||
<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 class="language-sh">$ 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>
|
||
<h4>在<code>let</code>语句中使用<code>if</code></h4>
|
||
<p>因为<code>if</code>是一个表达式,我们可以在<code>let</code>语句的右侧使用它,例如列表 3-4:</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 3-4: Assigning the result of an <code>if</code> expression to a variable</p>
|
||
</figcaption>
|
||
</figure>
|
||
<p><code>number</code>变量将会绑定到基于<code>if</code>表达式结果的值。运行这段代码看看会出现什么:</p>
|
||
<pre><code class="language-sh">$ 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 class="language-sh"> Compiling branches v0.1.0 (file:///projects/branches)
|
||
error[E0308]: if and else have incompatible types
|
||
--> src/main.rs:4:18
|
||
|
|
||
4 | let number = if condition {
|
||
| ^ expected integral variable, found reference
|
||
|
|
||
= note: expected type `{integer}`
|
||
= note: 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>
|
||
<h3>使用循环重复执行</h3>
|
||
<p>多次执行一段代码是很常用的。为了这个功能,Rust 提供了多种<strong>循环</strong>(<em>loops</em>)。一个循环执行循环体中的代码直到结尾并紧接着从回到开头继续执行。为了实验一下循环,让我们创建一个叫做 <em>loops</em> 的新项目。</p>
|
||
<p>Rust 有三种循环类型:<code>loop</code>、<code>while</code>和<code>for</code>。让我们每一个都试试。</p>
|
||
<h4>使用<code>loop</code>重复执行代码</h4>
|
||
<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>
|
||
<h4><code>while</code>条件循环</h4>
|
||
<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>
|
||
<h4>使用<code>for</code>遍历集合</h4>
|
||
<p>可以使用<code>while</code>结构来遍历一个元素集合,比如数组。例如:</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 3-5: Looping through each element of a collection using a <code>while</code> loop</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 3-6: Looping through each element of a collection using a <code>for</code> loop</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<h2>总结</h2>
|
||
<p>你做到了!这是一个相当可观的章节:你学习了变量,标量和<code>if</code>表达式,还有循环!如果你想要实践本章讨论的概念,尝试构建如下的程序:</p>
|
||
<ul>
|
||
<li>相互转换摄氏与华氏温度</li>
|
||
<li>生成 n 阶斐波那契数列</li>
|
||
<li>打印圣诞颂歌“The Twelve Days of Christmas”的歌词,并利用歌曲中的重复部分(编写循环)</li>
|
||
</ul>
|
||
<p>当你准备好继续的时候,让我们讨论一个其他语言中<em>并不</em>常见的概念:所有权(ownership)。</p>
|
||
<h1>认识所有权</h1>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch04-00-understanding-ownership.md">ch04-00-understanding-ownership.md</a>
|
||
<br>
|
||
commit 759067b651a48a4a66485fe0876d318d398fb4fe</p>
|
||
</blockquote>
|
||
<p>所有权(系统)是 Rust 最独特的功能,它令 Rust 可以无需垃圾回收(garbage collector)就能保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能:借用、slices 以及 Rust 如何在内存中安排数据。</p>
|
||
<h2>什么是所有权</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch04-01-what-is-ownership.md">ch04-01-what-is-ownership.md</a>
|
||
<br>
|
||
commit cc053d91f41793e54d5321abe027b0c163d735b8</p>
|
||
</blockquote>
|
||
<p>Rust 的核心功能(之一)是<strong>所有权</strong>(<em>ownership</em>)。虽然这个功能理解起来很直观,不过它对语言的其余部分有着更深层的含义。</p>
|
||
<p>所有程序都必须管理他们运行时使用计算机内存的方式。一些语言中使用垃圾回收在程序运行过程中来时刻寻找不再被使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:内存被一个所有权系统管理,它拥有一系列的规则使编译器在编译时进行检查。任何所有权系统的功能都不会导致运行时开销。</p>
|
||
<p>因为所有权对很多程序员都是一个新概念,需要一些时间来适应。好消息是随着你对 Rust 和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒!</p>
|
||
<p>当你理解了所有权系统,你就会对这个使 Rust 如此独特的功能有一个坚实的基础。在本章中,你将会通过一些例子来学习所有权,他们关注一个非常常见的数据结构:字符串。</p>
|
||
<!-- PROD: START BOX -->
|
||
<blockquote>
|
||
<h3>栈(Stack)与堆(Heap)</h3>
|
||
<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 -->
|
||
<h3>所有权规则</h3>
|
||
<p>首先,让我们看一下所有权的规则。记住这些规则正如我们将完成一些说明这些规则的例子:</p>
|
||
<blockquote>
|
||
<ol>
|
||
<li>Rust 中的每一个值都有一个叫做它的<strong>所有者</strong>(<em>owner</em>)的变量。</li>
|
||
<li>同时一次只能有一个所有者</li>
|
||
<li>当所有者变量离开作用域,这个值将被丢弃。</li>
|
||
</ol>
|
||
</blockquote>
|
||
<h3>变量作用域</h3>
|
||
<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>
|
||
<figure>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 4-1: A variable and the scope in which it is valid</p>
|
||
</figcaption>
|
||
</figure>
|
||
<p>换句话说,这里有两个重要的点:</p>
|
||
<ol>
|
||
<li>当<code>s</code><strong>进入作用域</strong>,它就是有效的。</li>
|
||
<li>这一直持续到它<strong>离开作用域</strong>为止。</li>
|
||
</ol>
|
||
<p>目前为止,变量是否有效与作用域的关系跟其他变成语言是类似的。现在我们要在此基础上介绍<code>String</code>类型。</p>
|
||
<h3><code>String</code>类型</h3>
|
||
<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>
|
||
<h3>内存与分配</h3>
|
||
<p>字符串字面值的情况,我们在编译时就知道内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速和高效。不过这些属性都只来源于它的不可变形。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。</p>
|
||
<p>对于<code>String</code>类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:</p>
|
||
<ol>
|
||
<li>内存必须在运行时向操作系统请求</li>
|
||
<li>需要一个当我们处理完<code>String</code>时将内存返回给操作系统的方法</li>
|
||
</ol>
|
||
<p>第一部分由我们完成:当调用<code>String::from</code>时,它的实现请求它需要的内存。这在编程语言中是非常通用的。</p>
|
||
<p>然而,第二部分实现起来就各有区别了。在有**垃圾回收(GC)**的语言中, 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>
|
||
<h4>变量与数据交互:移动</h4>
|
||
<p>Rust 中的多个变量以一种独特的方式与同一数据交互。让我们看看列表 4-2 中一个使用整型的例子:</p>
|
||
<figure>
|
||
<pre><code class="language-rust">let x = 5;
|
||
let y = x;
|
||
</code></pre>
|
||
<figcaption>
|
||
<p>Listing 4-2: Assigning the integer value of variable <code>x</code> to <code>y</code></p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<figure>
|
||
<img alt="String in memory" src="img/trpl04-01.svg" class="center" style="width: 50%;" />
|
||
<figcaption>
|
||
<p>Figure 4-3: Representation in memory of a <code>String</code> holding the value <code>"hello"</code>
|
||
bound to <code>s1</code></p>
|
||
</figcaption>
|
||
</figure>
|
||
<p>长度代表当前<code>String</code>的内容使用了多少字节的内存。容量是<code>String</code>从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过目前为止的场景中并不重要,所以可以暂时忽略容量。</p>
|
||
<p>当我们把<code>s1</code>赋值给<code>s2</code>,<code>String</code>的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制堆上指针所指向的数据。换句话说,内存中数据的表现如图 4-4 所示。</p>
|
||
<figure>
|
||
<img alt="s1 and s2 pointing to the same value" src="img/trpl04-02.svg" class="center" style="width: 50%;" />
|
||
<figcaption>
|
||
<p>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></p>
|
||
</figcaption>
|
||
</figure>
|
||
<p>这个表现形式看起来<strong>并不像</strong>图 4-5 中的那样,它是如果 Rust 也拷贝了堆上的数据后内存看起来是怎么样的。如果 Rust 这么做了,那么操作<code>s2 = s1</code>在堆上数据比较大的时候可能会对运行时性能造成非常大的影响。</p>
|
||
<figure>
|
||
<img alt="s1 and s2 to two places" src="img/trpl04-03.svg" class="center" style="width: 50%;" />
|
||
<figcaption>
|
||
<p>Figure 4-5: Another possibility of what <code>s2 = s1</code> might do if Rust copied the
|
||
heap data as well</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<figure>
|
||
<img alt="s1 moved to s2" src="img/trpl04-04.svg" class="center" style="width: 50%;" />
|
||
<figcaption>
|
||
<p>Figure 4-6: Representation in memory after <code>s1</code> has been invalidated</p>
|
||
</figcaption>
|
||
</figure>
|
||
<p>这样就解决了我们的麻烦!因为只有<code>s2</code>是有效的,当其离开作用域,它就释放自己的内存,完毕。</p>
|
||
<p>另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的“深拷贝”。因此,任何<strong>自动</strong>的复制可以被认为对运行时性能影响较小。</p>
|
||
<h4>变量与数据交互:克隆</h4>
|
||
<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>
|
||
<h4>只在栈上的数据:拷贝</h4>
|
||
<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>注解,将会出现一个编译时错误。</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>
|
||
<h3>所有权与函数</h3>
|
||
<p>将值传递给函数在语言上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。列表 4-7 是一个带有变量何时进入和离开作用域标注的例子:</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 4-7: Functions with ownership and scope annotated</p>
|
||
</figcaption>
|
||
</figure>
|
||
<p>当尝试在调用<code>takes_ownership</code>后使用<code>s</code>时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在<code>main</code>函数中添加使用<code>s</code>和<code>x</code>的代码来看看哪里能使用他们,和哪里所有权规则会阻止我们这么做。</p>
|
||
<h3>返回值与作用域</h3>
|
||
<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>
|
||
<h2>引用与借用</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch04-02-references-and-borrowing.md">ch04-02-references-and-borrowing.md</a>
|
||
<br>
|
||
commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c</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>
|
||
<figure>
|
||
<img alt="&String s pointing at String s1" src="img/trpl04-05.svg" class="center" />
|
||
<figcaption>
|
||
<p>Figure 4-8: <code>&String s</code> pointing at <code>String s1</code></p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 4-9: Attempting to modify a borrowed value</p>
|
||
</figcaption>
|
||
</figure>
|
||
<p>这里是错误:</p>
|
||
<pre><code class="language-sh">error: cannot borrow immutable borrowed content `*some_string` as mutable
|
||
--> error.rs:8:5
|
||
|
|
||
8 | some_string.push_str(", world");
|
||
| ^^^^^^^^^^^
|
||
</code></pre>
|
||
<p>正如变量默认是不可变的,引用也一样。不允许修改引用的值。</p>
|
||
<h3>可变引用</h3>
|
||
<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 class="language-text">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 class="language-sh">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>
|
||
<h3>悬垂引用</h3>
|
||
<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>
|
||
<h3>引用的规则</h3>
|
||
<p>简要的概括一下对引用的讨论:</p>
|
||
<ol>
|
||
<li>特定时间,<strong>只能</strong>拥有如下中的一个:</li>
|
||
</ol>
|
||
<ul>
|
||
<li>一个可变引用。</li>
|
||
<li>任意属性的不可变引用。</li>
|
||
</ul>
|
||
<ol start="2">
|
||
<li>引用必须总是有效的。</li>
|
||
</ol>
|
||
<p>接下来,我们来看看一种不同类型的引用:slices。</p>
|
||
<h2>Slices</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch04-03-slices.md">ch04-03-slices.md</a>
|
||
<br>
|
||
commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c</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>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 4-10: The <code>first_word</code> function that returns a byte index value into
|
||
the <code>String</code> parameter</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>Next, we create an iterator over the array of bytes using the <code>iter</code> method :</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>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 4-11: Storing the result from calling the <code>first_word</code> function then
|
||
changing the <code>String</code> contents</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<h3>字符串 slice</h3>
|
||
<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>
|
||
<figure>
|
||
<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%;" />
|
||
<figcaption>
|
||
<p>Figure 4-12: String slice referring to part of a <code>String</code></p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<h4>字符串字面值就是 slice</h4>
|
||
<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>
|
||
<h4>字符串 slice 作为参数</h4>
|
||
<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>
|
||
<h3>其他 slice</h3>
|
||
<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>
|
||
<h2>总结</h2>
|
||
<p>所有权、借用和 slice 这些概念是 Rust 何以在编译时保障内存安全的关键所在。Rust 像其他系统编程语言那样给予你对内存使用的控制,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。</p>
|
||
<p>所有权系统影响了 Rust 中其他很多部分如何工作,所以我们会继续讲到这些概念,贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个<code>struct</code>中。</p>
|
||
<h1>结构体</h1>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch05-00-structs.md">ch05-00-structs.md</a>
|
||
<br>
|
||
commit 255b44b409585e472e14c396ebc75d28f540a1ac</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>fields</em>),并定义字段类型。例如,列表 5-1 展示了一个储存用户账号信息的结构体:</p>
|
||
<figure>
|
||
<pre><code class="language-rust">struct User {
|
||
username: String,
|
||
email: String,
|
||
sign_in_count: u64,
|
||
active: bool,
|
||
}
|
||
</code></pre>
|
||
<figcaption>
|
||
<p>Listing 5-1: A <code>User</code> struct definition</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<h2>结构体数据的所有权</h2>
|
||
<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>
|
||
<h2>一个示例程序</h2>
|
||
<p>为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。</p>
|
||
<p>使用 Cargo 来创建一个叫做 <em>rectangles</em> 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。列表 5-2 中是项目的 <em>src/main.rs</em> 文件中为此实现的一个小程序:</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 5-2: Calculating the area of a rectangle specified by its length and
|
||
width in separate variables</p>
|
||
</figcaption>
|
||
</figure>
|
||
<p>尝试使用<code>cargo run</code>运行程序:</p>
|
||
<pre><code>The area of the rectangle is 1500 square pixels.
|
||
</code></pre>
|
||
<h3>使用元组重构</h3>
|
||
<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>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 5-3: Specifying the length and width of the rectangle with a tuple</p>
|
||
</figcaption>
|
||
</figure>
|
||
<!-- 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>
|
||
<h3>使用结构体重构:增加更多意义</h3>
|
||
<p>现在引入结构体。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如列表 5-4 所示:</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 5-4: Defining a <code>Rectangle</code> struct</p>
|
||
</figcaption>
|
||
</figure>
|
||
<!-- 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>Rectangle</code>的面积,通过其<code>length</code>和<code>width</code>字段。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值<code>0</code>和<code>1</code>。这是明确性的胜利。</p>
|
||
<h3>通过衍生 trait 增加实用功能</h3>
|
||
<p>如果能够在调试程序时打印出<code>Rectangle</code>实例来查看其所有字段的值就更好了。列表 5-5 尝试像往常一样使用<code>println!</code>宏:</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 5-5: Attempting to print a <code>Rectangle</code> instance</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<figure>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 5-6: Adding the annotation to derive the <code>Debug</code> trait and printing the
|
||
<code>Rectangle</code> instance using debug formatting</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<h2>方法语法</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch05-01-method-syntax.md">ch05-01-method-syntax.md</a>
|
||
<br>
|
||
commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c</p>
|
||
</blockquote>
|
||
<p><strong>方法</strong>与函数类似:他们使用<code>fn</code>关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与方法是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十三章讲解)的上下文中被定义,并且他们第一个参数总是<code>self</code>,它代表方法被调用的结构体的实例。</p>
|
||
<h3>定义方法</h3>
|
||
<p>让我们将获取一个<code>Rectangle</code>实例作为参数的<code>area</code>函数改写成一个定义于<code>Rectangle</code>结构体上的<code>area</code>方法,如列表 5-7 所示:</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 5-7: Defining an <code>area</code> method on the <code>Rectangle</code> struct</p>
|
||
</figcaption>
|
||
</figure>
|
||
<!-- 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>块中,而不是让将来的用户在我们的代码中到处寻找`Rectangle的功能。</p>
|
||
<!-- PROD: START BOX -->
|
||
<blockquote>
|
||
<h3><code>-></code>运算符到哪去了?</h3>
|
||
<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 -->
|
||
<h3>带有更多参数的方法</h3>
|
||
<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>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 5-8: Demonstration of using the as-yet-unwritten <code>can_hold</code> method</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>块中增加这个新方法:</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>
|
||
<!-- Will add ghosting here in libreoffice /Carol -->
|
||
<p>如果结合列表 5-8 的<code>main</code>函数来运行,就会看到想要得到的输出!方法可以在<code>self</code>后增加多个参数,而且这些参数就像函数中的参数一样工作。</p>
|
||
<h3>关联函数</h3>
|
||
<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>
|
||
<h2>总结</h2>
|
||
<p>结构体让我们可以在自己的范围内创建有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名他们来使得代码更清晰。方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。</p>
|
||
<p>结构体并不是创建自定义类型的唯一方法;让我们转向 Rust 的<code>enum</code>功能并为自己的工具箱再填一个工具。</p>
|
||
<h1>枚举和模式匹配</h1>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch06-00-enums.md">ch06-00-enums.md</a>
|
||
<br>
|
||
commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d</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>
|
||
<h1>定义枚举</h1>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch06-01-defining-an-enum.md">ch06-01-defining-an-enum.md</a>
|
||
<br>
|
||
commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d</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>
|
||
<h3>枚举值</h3>
|
||
<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>
|
||
<figure>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 6-1: Storing the data and <code>IpAddrKind</code> variant of an IP address using a
|
||
<code>struct</code></p>
|
||
</figcaption>
|
||
</figure>
|
||
<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="../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>
|
||
<figure>
|
||
<pre><code class="language-rust">enum Message {
|
||
Quit,
|
||
Move { x: i32, y: i32 },
|
||
Write(String),
|
||
ChangeColor(i32, i32, i32),
|
||
}
|
||
</code></pre>
|
||
<figcaption>
|
||
<p>Listing 6-2: A <code>Message</code> enum whose variants each store different amounts and
|
||
types of values</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<h3><code>Option</code>枚举和其相对空值的优势</h3>
|
||
<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>我称之为我万亿美元的错误。当时,我在在一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的应有都应该是绝对安全的。不过我未能抗拒引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数以万计美元的苦痛和伤害。</p>
|
||
</blockquote>
|
||
<p>空值的为题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。</p>
|
||
<p>然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。</p>
|
||
<p>问题不在于实际的概念而在于具体的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是<code>Option<T></code>,而且它<a href="../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="../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>
|
||
<h2><code>match</code>控制流运算符</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch06-02-match.md">ch06-02-match.md</a>
|
||
<br>
|
||
commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d</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>
|
||
<figure>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 6-3: An enum and a <code>match</code> expression that has the variants of the enum
|
||
as its patterns.</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>表达式的返回值。</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>
|
||
<h3>绑定值的模式</h3>
|
||
<p>匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值。</p>
|
||
<p>作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的<code>enum</code>,通过改变<code>Quarter</code>成员来包含一个<code>State</code>值,列表 6-4 中完成了这些修改:</p>
|
||
<figure>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 6-4: A <code>Coin</code> enum where the <code>Quarter</code> variant also holds a <code>UsState</code>
|
||
value</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<h3>匹配<code>Option<T></code></h3>
|
||
<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>
|
||
<figure>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 6-5: A function that uses a <code>match</code> expression on an <code>Option<i32></code></p>
|
||
</figcaption>
|
||
</figure>
|
||
<h4>匹配<code>Some(T)</code></h4>
|
||
<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>
|
||
<h4>匹配<code>None</code></h4>
|
||
<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>
|
||
<h3>匹配是穷尽的</h3>
|
||
<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>
|
||
<h3><code>_</code>通配符</h3>
|
||
<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>
|
||
<h2><code>if let</code>简单控制流</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch06-03-if-let.md">ch06-03-if-let.md</a>
|
||
<br>
|
||
commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d</p>
|
||
</blockquote>
|
||
<p><code>if let</code>语法让我们以一种不那么冗长的方式结合<code>if</code>和<code>let</code>,来处理匹配一个模式的值而忽略其他的值。考虑列表 6-6 中的程序,它匹配一个<code>Option<u8></code>值并只希望当值是三时执行代码:</p>
|
||
<figure>
|
||
<pre><code class="language-rust">let some_u8_value = Some(0u8);
|
||
match some_u8_value {
|
||
Some(3) => println!("three"),
|
||
_ => (),
|
||
}
|
||
</code></pre>
|
||
<figcaption>
|
||
<p>Listing 6-6: A <code>match</code> that only cares about executing code when the value is
|
||
<code>Some(3)</code></p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<h2>总结</h2>
|
||
<p>现在我们涉及到了如何使用枚举来创建有一系列可列举值的自定义类型。我们也展示了标准库的<code>Option<T></code>类型是如何帮助你利用类型系统来避免出错。当枚举值包含数据时,你可以根据需要处理多少情况来选择使用<code>match</code>或<code>if let</code>来获取并使用这些值。</p>
|
||
<p>你的 Rust 程序现在能够使用结构体和枚举在自己的作用域内表现其内容了。在你的 API 中使用自定义类型保证了类型安全:编译器会确保你的函数只会得到它期望的类型的值。</p>
|
||
<p>为了向你的用户提供一个组织良好的 API,它使用直观且只向用户暴露他们确实需要的部分,那么让我们转向 Rust 的模块系统吧。</p>
|
||
<h1>模块</h1>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch07-00-modules.md">ch07-00-modules.md</a>
|
||
<br>
|
||
commit e2a129961ae346f726f8b342455ec2255cdfed68</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>
|
||
<h2><code>mod</code>和文件系统</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch07-01-mod-and-the-filesystem.md">ch07-01-mod-and-the-filesystem.md</a>
|
||
<br>
|
||
commit e2a129961ae346f726f8b342455ec2255cdfed68</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>
|
||
<h3>模块定义</h3>
|
||
<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>
|
||
<figure>
|
||
<span class="filename">Filename: src/lib.rs</span>
|
||
<pre><code class="language-rust">mod network {
|
||
fn connect() {
|
||
}
|
||
}
|
||
|
||
mod client {
|
||
fn connect() {
|
||
}
|
||
}
|
||
</code></pre>
|
||
<figcaption>
|
||
<p>Listing 7-1: The <code>network</code> module and the <code>client</code> module defined side-by-side
|
||
in <em>src/lib.rs</em></p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<figure>
|
||
<span class="filename">Filename: src/lib.rs</span>
|
||
<pre><code class="language-rust">mod network {
|
||
fn connect() {
|
||
}
|
||
|
||
mod client {
|
||
fn connect() {
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<figcaption>
|
||
<p>Listing 7-2: Moving the <code>client</code> module inside of the <code>network</code> module</p>
|
||
</figcaption>
|
||
</figure>
|
||
<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>
|
||
<h3>将模块移动到其他文件</h3>
|
||
<p>位于层级结构中的模块,非常类似计算机领域的另一个我们非常熟悉的结构:文件系统!我们可以利用 Rust 的模块系统连同多个文件一起分解 Rust 项目,这样就不是所有的内容都落到 <em>src/lib.rs</em> 中了。作为例子,我们将从列表 7-3 中的代码开始:</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/lib.rs</span>
|
||
<pre><code class="language-rust">mod client {
|
||
fn connect() {
|
||
}
|
||
}
|
||
|
||
mod network {
|
||
fn connect() {
|
||
}
|
||
|
||
mod server {
|
||
fn connect() {
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<figcaption>
|
||
<p>Listing 7-3: Three modules, <code>client</code>, <code>network</code>, and <code>network::server</code>, all
|
||
defined in <em>src/lib.rs</em></p>
|
||
</figcaption>
|
||
</figure>
|
||
<p>这是模块层次结构:</p>
|
||
<pre><code>communicator
|
||
├── client
|
||
└── network
|
||
└── server
|
||
</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>模块的内容。如果在这里加上一个<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>
|
||
<figure>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 7-4: Error when trying to extract the <code>server</code> submodule into
|
||
<em>src/server.rs</em></p>
|
||
</figcaption>
|
||
</figure>
|
||
<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 class="language-text">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>
|
||
<h3>模块文件系统的规则</h3>
|
||
<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>
|
||
<h2>使用<code>pub</code>控制可见性</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch07-02-controlling-visibility-with-pub.md">ch07-02-controlling-visibility-with-pub.md</a>
|
||
<br>
|
||
commit e2a129961ae346f726f8b342455ec2255cdfed68</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>使用,而不一定要被项目自身使用,所以不应该担心这些函数是未被使用的。创建他们的意义就在于被另一个项目而不是被自己使用。</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>
|
||
<h3>标记函数为公有</h3>
|
||
<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>
|
||
<h3>私有性规则</h3>
|
||
<p>总的来说,有如下项的可见性规则:</p>
|
||
<ol>
|
||
<li>如果一个项是公有的,它能被任何父模块访问</li>
|
||
<li>如果一个项是私有的,它只能被当前模块或其子模块访问</li>
|
||
</ol>
|
||
<h3>私有性示例</h3>
|
||
<p>让我们看看更多例子作为练习。创建一个新的库项目并在新项目的 <em>src/lib.rs</em> 输入列表 7-5 中的代码:</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/lib.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 7-5: Examples of private and public functions, some of which are
|
||
incorrect</p>
|
||
</figcaption>
|
||
</figure>
|
||
<p>在尝试编译这些代码之前,猜测一下<code>try_me</code>函数的哪一行会出错。接着编译项目来看看是否猜对了,然后继续阅读后面关于错误的讨论!</p>
|
||
<h4>检查错误</h4>
|
||
<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>
|
||
<h4>修改错误</h4>
|
||
<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>
|
||
<h2>导入命名</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch07-03-importing-names-with-use.md">ch07-03-importing-names-with-use.md</a>
|
||
<br>
|
||
commit e2a129961ae346f726f8b342455ec2255cdfed68</p>
|
||
</blockquote>
|
||
<p>我们已经讲到了如何使用模块名称作为调用的一部分,来调用模块中的函数,如列表 7-6 中所示的<code>nested_modules</code>函数调用。</p>
|
||
<figure>
|
||
<span class="filename">Filename: src/main.rs</span>
|
||
<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>
|
||
<figcaption>
|
||
<p>Listing 7-6: Calling a function by fully specifying its enclosing module’s
|
||
namespaces</p>
|
||
</figcaption>
|
||
</figure>
|
||
<p>如你所见,指定函数的完全限定名称可能会非常冗长。所幸 Rust 有一个关键字使得这些调用显得更简洁。</p>
|
||
<h3>使用<code>use</code>的简单导入</h3>
|
||
<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>
|
||
<h3>使用<code>*</code>的全局引用导入</h3>
|
||
<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>
|
||
<h3>使用<code>super</code>访问父模块</h3>
|
||
<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>
|
||
<h2>总结</h2>
|
||
<p>现在你掌握了组织代码的核心科技!利用他们将相关的代码组合在一起、防止代码文件过长并将一个整洁的公有 API 展现给库的用户。</p>
|
||
<p>接下来,让我们看看一些标准库提供的集合数据类型,你可以利用他们编写出漂亮整洁的代码。</p>
|
||
<h1>通用集合类型</h1>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch08-00-common-collections.md">ch08-00-common-collections.md</a>
|
||
<br>
|
||
commit 0d229cc5a3da341196e15a6761735b2952281569</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="../std/collections">文档</a>。</p>
|
||
<p>我们将讨论如何创建和更新 vector、字符串和哈希 map,以及他们何以如此特殊。</p>
|
||
<h2>vector</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch08-01-vectors.md">ch08-01-vectors.md</a>
|
||
<br>
|
||
commit 0d229cc5a3da341196e15a6761735b2952281569</p>
|
||
</blockquote>
|
||
<p>我们要讲到的第一个类型是<code>Vec<T></code>,也被称为 <em>vector</em>。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。</p>
|
||
<h3>新建 vector</h3>
|
||
<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>
|
||
<h3>更新 vector</h3>
|
||
<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>
|
||
<h3>丢弃 vector 时也会丢弃其所有元素</h3>
|
||
<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>
|
||
<h3>读取 vector 的元素</h3>
|
||
<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>
|
||
<h4>无效引用</h4>
|
||
<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>
|
||
<h3>使用枚举来储存多种类型</h3>
|
||
<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>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>
|
||
<h2>字符串</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch08-02-strings.md">ch08-02-strings.md</a>
|
||
<br>
|
||
commit 4dc0001ccba4189e210ba47d6fe6c3c5fa729da6</p>
|
||
</blockquote>
|
||
<p>第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解一下它。字符串是新晋 Rustacean 们通常会被困住的领域。这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。</p>
|
||
<p>字符串出现在集合章节的原因是,字符串是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到<code>String</code>那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论<code>String</code>于其他集合不一样的地方,例如索引<code>String</code>是很复杂的,由于人和计算机理解<code>String</code>数据的不同方式。</p>
|
||
<h3>什么是字符串?</h3>
|
||
<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>
|
||
<h3>新建字符串</h3>
|
||
<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>
|
||
<h3>更新字符串</h3>
|
||
<p><code>String</code>的大小可以增长其内容也可以改变,就像可以放入更多数据来改变<code>Vec</code>的内容一样。另外,<code>String</code>实现了<code>+</code>运算符作为级联运算符以便于使用。</p>
|
||
<h4>附加字符串</h4>
|
||
<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>
|
||
<h4>使用 + 运算符或<code>format!</code>宏级联字符串</h4>
|
||
<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>&String</code>是如何被强转为<code>&str</code>的:写成<code>&s2</code>的话<code>String</code>将会被强转成一个合适的类型<code>&str</code>。又因为方法没有获取参数的所有权,所以<code>s2</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>
|
||
<h3>索引字符串</h3>
|
||
<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>
|
||
<h4>内部表示</h4>
|
||
<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>
|
||
<h4>字节、标量值和字形簇!天呐!</h4>
|
||
<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>
|
||
<h3>字符串 slice</h3>
|
||
<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>
|
||
<h3>遍历字符串的方法</h3>
|
||
<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>
|
||
<h3>字符串并不简单</h3>
|
||
<p>总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理<code>String</code>数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何在前台处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期中免于处理涉及非 ASCII 字符的错误。</p>
|
||
<p>现在让我们转向一些不太复杂的集合:哈希 map!</p>
|
||
<h2>哈希 map</h2>
|
||
<blockquote>
|
||
<p><a href="https://github.com/rust-lang/book/blob/master/src/ch08-03-hash-maps.md">ch08-03-hash-maps.md</a>
|
||
<br>
|
||
commit 0d229cc5a3da341196e15a6761735b2952281569</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>
|
||
<h3>新建一个哈希 map</h3>
|
||
<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>。在这三个常用集合中,这个是最不常用的,所以并不包含在被 prelude 自动引用的功能中。标准库中对哈希 map 的支持也相对较少;例如,并没有内建的用于构建的宏。</p>
|
||
<p>就像 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 中数据的类型推断出哈希 map 所包含的类型。</p>
|
||
<h3>哈希 map 和所有权</h3>
|
||
<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>
|
||
<h3>访问哈希 map 中的值</h3>
|
||
<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>
|
||
<h3>更新哈希 map</h3>
|
||
<p>虽然键值对的数量是可以增长的,不过每个单独的键同时只能关联一个值。当你想要改变哈希 map 中的数据时,必须选择是用新值替代旧值,还是完全无视旧值。我们也可以选择保留旧值而忽略新值,并只在键<strong>没有</strong>对应一个值时增加新值。或者可以结合新值和旧值。让我们看看着每一种方式是如何工作的!</p>
|
||
<h4>覆盖一个值</h4>
|
||
<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>
|
||
<h4>只在键没有对应值时插入</h4>
|
||
<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>
|
||
<h4>根据旧值更新一个值</h4>
|
||
<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>
|
||
<h3>哈希函数</h3>
|
||
<p><code>HashMap</code>默认使用一个密码学上是安全的哈希函数,它可以提供抵抗拒绝服务(Denial of Service, DoS)攻击的能力。这并不是现有最快的哈希函数,不过为了更好的安全性带来一些性能下降也是值得的。如果你监控你的代码并发现默认哈希函数对你来说非常慢,可以通过指定一个不同的 <em>hasher</em> 来切换为另一个函数。hasher 是一个实现了<code>BuildHasher</code> trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。</p>
|
||
<h2>总结</h2>
|
||
<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>
|
||
|
||
</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>
|