trpl-zh-cn/ch06-01-defining-an-enum.html

490 lines
50 KiB
HTML
Raw Normal View History

<!DOCTYPE HTML>
<html lang="en" class="light" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>枚举的定义 - Rust 程序设计语言 简体中文版</title>
<!-- Custom HTML head -->
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="favicon.svg">
<link rel="shortcut icon" href="favicon.png">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" href="css/chrome.css">
<link rel="stylesheet" href="css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<link rel="stylesheet" href="ayu-highlight.css">
<!-- Custom theme stylesheets -->
<link rel="stylesheet" href="ferris.css">
<link rel="stylesheet" href="theme/2018-edition.css">
<link rel="stylesheet" href="theme/semantic-notes.css">
<link rel="stylesheet" href="theme/listing.css">
</head>
<body class="sidebar-visible no-js">
<div id="body-container">
<!-- Provide site root to javascript -->
<script>
var path_to_root = "";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('light')
html.classList.add(theme);
var body = document.querySelector('body');
body.classList.remove('no-js')
body.classList.add('js');
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var body = document.querySelector('body');
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
body.classList.remove('sidebar-visible');
body.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded affix "><a href="title-page.html">Rust 程序设计语言</a></li><li class="chapter-item expanded affix "><a href="foreword.html">前言</a></li><li class="chapter-item expanded affix "><a href="ch00-00-introduction.html">简介</a></li><li class="chapter-item expanded "><a href="ch01-00-getting-started.html"><strong aria-hidden="true">1.</strong> 入门指南</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch01-01-installation.html"><strong aria-hidden="true">1.1.</strong> 安装</a></li><li class="chapter-item expanded "><a href="ch01-02-hello-world.html"><strong aria-hidden="true">1.2.</strong> Hello, World!</a></li><li class="chapter-item expanded "><a href="ch01-03-hello-cargo.html"><strong aria-hidden="true">1.3.</strong> Hello, Cargo!</a></li></ol></li><li class="chapter-item expanded "><a href="ch02-00-guessing-game-tutorial.html"><strong aria-hidden="true">2.</strong> 写个猜数字游戏</a></li><li class="chapter-item expanded "><a href="ch03-00-common-programming-concepts.html"><strong aria-hidden="true">3.</strong> 常见编程概念</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch03-01-variables-and-mutability.html"><strong aria-hidden="true">3.1.</strong> 变量与可变性</a></li><li class="chapter-item expanded "><a href="ch03-02-data-types.html"><strong aria-hidden="true">3.2.</strong> 数据类型</a></li><li class="chapter-item expanded "><a href="ch03-03-how-functions-work.html"><strong aria-hidden="true">3.3.</strong> 函数</a></li><li class="chapter-item expanded "><a href="ch03-04-comments.html"><strong aria-hidden="true">3.4.</strong> 注释</a></li><li class="chapter-item expanded "><a href="ch03-05-control-flow.html"><strong aria-hidden="true">3.5.</strong> 控制流</a></li></ol></li><li class="chapter-item expanded "><a href="ch04-00-understanding-ownership.html"><strong aria-hidden="true">4.</strong> 认识所有权</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch04-01-what-is-ownership.html"><strong aria-hidden="true">4.1.</strong> 什么是所有权?</a></li><li class="chapter-item expanded "><a href="ch04-02-references-and-borrowing.html"><strong aria-hidden="true">4.2.</strong> 引用与借用</a></li><li class="chapter-item expanded "><a href="ch04-03-slices.html"><strong aria-hidden="true">4.3.</strong> Slice 类型</a></li></ol></li><li class="chapter-item expanded "><a href="ch05-00-structs.html"><strong aria-hidden="true">5.</strong> 使用结构体组织相关联的数据</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch05-01-defining-structs.html"><strong aria-hidden="true">5.1.</strong> 结构体的定义和实例化</a></li><li class="chapter-item expanded "><a href="ch05-02-example-structs.html"><strong aria-hidden="true">5.2.</strong> 结构体示例程序</a></li><li class="chapter-item expanded "><a href="ch05-03-method-syntax.html"><strong aria-hidden="true">5.3.</strong> 方法语法</a></li></ol></li><li class="chapter-item expanded "><a href="ch06-00-enums.html"><strong aria-hidden="true">6.</strong> 枚举和模式匹配</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch06-01-defining-an-enum.html" class="active"><strong aria-hidden="true">6.1.</strong> 枚举的定义</a></li><li class="chapter-item expanded "><a href="ch06-02-match.html"><strong aria-hidden="true">6.2.</strong> match 控制流结构</a></li><li class="chapter-item expanded "><a href="ch06-03-if-let.html"><strong aria-hidden="true">6.3.</strong> if let 简洁控制流</a></li></ol></li><li class="chapter-item expanded "><a href="ch07-00-managing-growing-projects-with-packages-crates-and-modules.html"><strong aria-hidden="true">7.</strong> 使用包、Crate 和模块管理不断增长的项目</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch07-01-packages-and-crates.html"><strong aria-hidden="true">7.1.</strong> 包和 Crate</a></li><li class="chapter-item
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Rust 程序设计语言 简体中文版</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/KaiserY/trpl-zh-cn/tree/main" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
<a href="https://github.com/KaiserY/trpl-zh-cn/edit/main/src/ch06-01-defining-an-enum.md" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h2 id="枚举的定义"><a class="header" href="#枚举的定义">枚举的定义</a></h2>
<blockquote>
<p><a href="https://github.com/rust-lang/book/blob/main/src/ch06-01-defining-an-enum.md">ch06-01-defining-an-enum.md</a>
<br>
commit bb7e429ad6b59d9a0c37db7434976364cbb9c6da</p>
</blockquote>
<p>结构体给予你将字段和数据聚合在一起的方法,像 <code>Rectangle</code> 结构体有 <code>width</code><code>height</code> 两个字段。而枚举给予你一个途径去声明某个值是一个集合中的一员。比如,我们想让 <code>Rectangle</code> 是一些形状的集合,包含 <code>Circle</code><code>Triangle</code> 。为了做到这个Rust 提供了枚举类型。</p>
<p>让我们看看一个需要诉诸于代码的场景,来考虑为何此时使用枚举更为合适且实用。假设我们要处理 IP 地址。目前被广泛使用的两个主要 IP 标准IPv4version four和 IPv6version 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><pre class="playground"><code class="language-rust edition2021">enum IpAddrKind {
V4,
V6,
}
<span class="boring">
</span><span class="boring">fn main() {
</span><span class="boring"> let four = IpAddrKind::V4;
</span><span class="boring"> let six = IpAddrKind::V6;
</span><span class="boring">
</span><span class="boring"> route(IpAddrKind::V4);
</span><span class="boring"> route(IpAddrKind::V6);
</span><span class="boring">}
</span><span class="boring">
</span><span class="boring">fn route(ip_kind: IpAddrKind) {}</span></code></pre></pre>
<p>现在 <code>IpAddrKind</code> 就是一个可以在代码中使用的自定义数据类型了。</p>
<h3 id="枚举值"><a class="header" href="#枚举值">枚举值</a></h3>
<p>可以像这样创建 <code>IpAddrKind</code> 两个不同成员的实例:</p>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">enum IpAddrKind {
</span><span class="boring"> V4,
</span><span class="boring"> V6,
</span><span class="boring">}
</span><span class="boring">
</span><span class="boring">fn main() {
</span> let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
<span class="boring">
</span><span class="boring"> route(IpAddrKind::V4);
</span><span class="boring"> route(IpAddrKind::V6);
</span><span class="boring">}
</span><span class="boring">
</span><span class="boring">fn route(ip_kind: IpAddrKind) {}</span></code></pre></pre>
<p>注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在 <code>IpAddrKind::V4</code><code>IpAddrKind::V6</code> 都是 <code>IpAddrKind</code> 类型的。例如,接着可以定义一个函数来接收任何 <code>IpAddrKind</code>类型的参数:</p>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">enum IpAddrKind {
</span><span class="boring"> V4,
</span><span class="boring"> V6,
</span><span class="boring">}
</span><span class="boring">
</span><span class="boring">fn main() {
</span><span class="boring"> let four = IpAddrKind::V4;
</span><span class="boring"> let six = IpAddrKind::V6;
</span><span class="boring">
</span><span class="boring"> route(IpAddrKind::V4);
</span><span class="boring"> route(IpAddrKind::V6);
</span><span class="boring">}
</span><span class="boring">
</span>fn route(ip_kind: IpAddrKind) {}</code></pre></pre>
<p>现在可以使用任一成员来调用这个函数:</p>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">enum IpAddrKind {
</span><span class="boring"> V4,
</span><span class="boring"> V6,
</span><span class="boring">}
</span><span class="boring">
</span><span class="boring">fn main() {
</span><span class="boring"> let four = IpAddrKind::V4;
</span><span class="boring"> let six = IpAddrKind::V6;
</span><span class="boring">
</span> route(IpAddrKind::V4);
route(IpAddrKind::V6);
<span class="boring">}
</span><span class="boring">
</span><span class="boring">fn route(ip_kind: IpAddrKind) {}</span></code></pre></pre>
<p>使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个存储实际 IP 地址 <strong>数据</strong> 的方法;只知道它是什么 <strong>类型</strong> 的。考虑到已经在第五章学习过结构体了,你可能会像示例 6-1 那样处理这个问题:</p>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">fn main() {
</span> 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"),
};
<span class="boring">}</span></code></pre></pre>
<p><span class="caption">示例 6-1将 IP 地址的数据和 <code>IpAddrKind</code> 成员存储在一个 <code>struct</code></span></p>
<p>这里我们定义了一个有两个字段的结构体 <code>IpAddr</code><code>IpAddrKind</code>(之前定义的枚举)类型的 <code>kind</code> 字段和 <code>String</code> 类型 <code>address</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><pre class="playground"><code class="language-rust edition2021"><span class="boring">fn main() {
</span> enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
<span class="boring">}</span></code></pre></pre>
<p>我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。这里也很容易看出枚举工作的另一个细节:每一个我们定义的枚举成员的名字也变成了一个构建枚举的实例的函数。也就是说,<code>IpAddr::V4()</code> 是一个获取 <code>String</code> 参数并返回 <code>IpAddr</code> 类型实例的函数调用。作为定义枚举的结果,这些构造函数会自动被定义。</p>
<p>用枚举替代结构体还有另一个优势每个成员可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将 <code>V4</code> 地址存储为四个 <code>u8</code> 值而 <code>V6</code> 地址仍然表现为一个 <code>String</code>,这就不能使用结构体了。枚举则可以轻易的处理这个情况:</p>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">fn main() {
</span> enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
<span class="boring">}</span></code></pre></pre>
<p>这些代码展示了使用枚举来存储两种不同 IP 地址的几种可能的选择。然而,事实证明存储和编码 IP 地址实在是太常见了<a href="https://doc.rust-lang.org/std/net/enum.IpAddr.html">以致标准库提供了一个开箱即用的定义!</a><!-- ignore -->让我们看看标准库是如何定义 <code>IpAddr</code> 的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员中的地址数据嵌入到了两个不同形式的结构体中,它们对不同的成员的定义是不同的:</p>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>struct Ipv4Addr {
// --snip--
}
struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
<span class="boring">}</span></code></pre></pre>
<p>这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你设想出来的要复杂多少。</p>
<p>注意虽然标准库中包含一个 <code>IpAddr</code> 的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。第七章会讲到如何导入类型。</p>
<p>来看看示例 6-2 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型:</p>
<pre><pre class="playground"><code class="language-rust edition2021">enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
<span class="boring">
</span><span class="boring">fn main() {}</span></code></pre></pre>
<p><span class="caption">示例 6-2一个 <code>Message</code> 枚举,其每个成员都存储了不同数量和类型的值</span></p>
<p>这个枚举有四个含有不同类型的成员:</p>
<ul>
<li><code>Quit</code> 没有关联任何数据。</li>
<li><code>Move</code> 类似结构体包含命名字段。</li>
<li><code>Write</code> 包含单独一个 <code>String</code></li>
<li><code>ChangeColor</code> 包含三个 <code>i32</code></li>
</ul>
<p>定义一个如示例 6-2 中所示那样的有关联值的枚举的方式和定义多个不同类型的结构体的方式很相像,除了枚举不使用 <code>struct</code> 关键字以及其所有成员都被组合在一起位于 <code>Message</code> 类型下。如下这些结构体可以包含与之前枚举成员中相同的数据:</p>
<pre><pre class="playground"><code class="language-rust edition2021">struct QuitMessage; // 类单元结构体
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体
<span class="boring">
</span><span class="boring">fn main() {}</span></code></pre></pre>
<p>不过,如果我们使用不同的结构体,由于它们都有不同的类型,我们将不能像使用示例 6-2 中定义的 <code>Message</code> 枚举那样,轻易的定义一个能够处理这些不同类型的结构体的函数,因为枚举是单独一个类型。</p>
<p>结构体和枚举还有另一个相似点:就像可以使用 <code>impl</code> 来为结构体定义方法那样,也可以在枚举上定义方法。这是一个定义于我们 <code>Message</code> 枚举上的叫做 <code>call</code> 的方法:</p>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">fn main() {
</span><span class="boring"> enum Message {
</span><span class="boring"> Quit,
</span><span class="boring"> Move { x: i32, y: i32 },
</span><span class="boring"> Write(String),
</span><span class="boring"> ChangeColor(i32, i32, i32),
</span><span class="boring"> }
</span><span class="boring">
</span> impl Message {
fn call(&amp;self) {
// 在这里定义方法体
}
}
let m = Message::Write(String::from("hello"));
m.call();
<span class="boring">}</span></code></pre></pre>
<p>方法体使用了 <code>self</code> 来获取调用方法的值。这个例子中,创建了一个值为 <code>Message::Write(String::from("hello"))</code> 的变量 <code>m</code>,而且这就是当 <code>m.call()</code> 运行时 <code>call</code> 方法中的 <code>self</code> 的值。</p>
<p>让我们看看标准库中的另一个非常常见且实用的枚举:<code>Option</code></p>
<h3 id="option-枚举和其相对于空值的优势"><a class="header" href="#option-枚举和其相对于空值的优势"><code>Option</code> 枚举和其相对于空值的优势</a></h3>
<p>这一部分会分析一个 <code>Option</code> 的案例,<code>Option</code> 是标准库定义的另一个枚举。<code>Option</code> 类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。</p>
<p>例如,如果请求一个非空列表的第一项,会得到一个值,如果请求一个空的列表,就什么也不会得到。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。</p>
<p>编程语言的设计经常要考虑包含哪些功能但考虑排除哪些功能也很重要。Rust 并没有很多其他语言中有的空值功能。<strong>空值</strong><em>Null</em> )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。</p>
<p>Tony Hoarenull 的发明者,在他 2009 年的演讲 “Null References: The Billion Dollar Mistake” 中曾经说到:</p>
<blockquote>
<p>I call it my billion-dollar mistake. At that time, I was designing the first
comprehensive type system for references in an object-oriented language. My
goal was to ensure that all use of references should be absolutely safe, with
checking performed automatically by the compiler. But I couldn't resist the
temptation to put in a null reference, simply because it was so easy to
implement. This has led to innumerable errors, vulnerabilities, and system
crashes, which have probably caused a billion dollars of pain and damage in
the last forty years.</p>
<p>我称之为我十亿美元的错误。当时,我在为一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过我未能抵抗住引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。</p>
</blockquote>
<p>空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性无处不在,非常容易出现这类错误。</p>
<p>然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。</p>
<p>问题不在于概念而在于具体的实现。为此Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 <code>Option&lt;T&gt;</code>,而且它<a href="https://doc.rust-lang.org/std/option/enum.Option.html">定义于标准库中</a><!-- ignore -->,如下:</p>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>enum Option&lt;T&gt; {
None,
Some(T),
}
<span class="boring">}</span></code></pre></pre>
<p><code>Option&lt;T&gt;</code> 枚举是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要 <code>Option::</code> 前缀来直接使用 <code>Some</code><code>None</code>。即便如此 <code>Option&lt;T&gt;</code> 也仍是常规的枚举,<code>Some(T)</code><code>None</code> 仍是 <code>Option&lt;T&gt;</code> 的成员。</p>
<p><code>&lt;T&gt;</code> 语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,第十章会更详细的讲解泛型。目前,所有你需要知道的就是 <code>&lt;T&gt;</code> 意味着 <code>Option</code> 枚举的 <code>Some</code> 成员可以包含任意类型的数据,同时每一个用于 <code>T</code> 位置的具体类型使得 <code>Option&lt;T&gt;</code> 整体作为不同的类型。这里是一些包含数字类型和字符串类型 <code>Option</code> 值的例子:</p>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">fn main() {
</span> let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option&lt;i32&gt; = None;
<span class="boring">}</span></code></pre></pre>
<p><code>some_number</code> 的类型是 <code>Option&lt;i32&gt;</code><code>some_char</code> 的类型是 <code>Option&lt;char&gt;</code>,这(与 <code>some_number</code>)是一个不同的类型。因为我们在 <code>Some</code> 成员中指定了值Rust 可以推断其类型。对于 <code>absent_number</code>Rust 需要我们指定 <code>Option</code> 整体的类型,因为编译器只通过 <code>None</code> 值无法推断出 <code>Some</code> 成员保存的值的类型。这里我们告诉 Rust 希望 <code>absent_number</code><code>Option&lt;i32&gt;</code> 类型的。</p>
<p>当有一个 <code>Some</code> 值时,我们就知道存在一个值,而这个值保存在 <code>Some</code> 中。当有个 <code>None</code> 值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么,<code>Option&lt;T&gt;</code> 为什么就比空值要好呢?</p>
<p>简而言之,因为 <code>Option&lt;T&gt;</code><code>T</code>(这里 <code>T</code> 可以是任何类型)是不同的类型,编译器不允许像一个肯定有效的值那样使用 <code>Option&lt;T&gt;</code>。例如,这段代码不能编译,因为它尝试将 <code>Option&lt;i8&gt;</code><code>i8</code> 相加:</p>
<pre><code class="language-rust ignore does_not_compile"><span class="boring">fn main() {
</span> let x: i8 = 5;
let y: Option&lt;i8&gt; = Some(5);
let sum = x + y;
<span class="boring">}</span></code></pre>
<p>如果运行这些代码,将得到类似这样的错误信息:</p>
<pre><code class="language-console">$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option&lt;i8&gt;` to `i8`
--&gt; src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option&lt;i8&gt;`
|
= help: the trait `Add&lt;Option&lt;i8&gt;&gt;` is not implemented for `i8`
= help: the following other types implement trait `Add&lt;Rhs&gt;`:
`&amp;'a i8` implements `Add&lt;i8&gt;`
`&amp;i8` implements `Add&lt;&amp;i8&gt;`
`i8` implements `Add&lt;&amp;i8&gt;`
`i8` implements `Add`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error
</code></pre>
<p>很好!事实上,错误信息意味着 Rust 不知道该如何将 <code>Option&lt;i8&gt;</code><code>i8</code> 相加,因为它们的类型不同。当在 Rust 中拥有一个像 <code>i8</code> 这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。只有当使用 <code>Option&lt;i8&gt;</code>(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。</p>
<p>换句话说,在对 <code>Option&lt;T&gt;</code> 进行运算之前必须将其转换为 <code>T</code>。通常这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。</p>
<p>消除了错误地假设一个非空值的风险,会让你对代码更加有信心。为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 <code>Option&lt;T&gt;</code> 中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 <code>Option&lt;T&gt;</code> 类型,你就 <strong>可以</strong> 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。</p>
<p>那么当有一个 <code>Option&lt;T&gt;</code> 的值时,如何从 <code>Some</code> 成员中取出 <code>T</code> 的值来使用它呢?<code>Option&lt;T&gt;</code> 枚举拥有大量用于各种情况的方法:你可以查看<a href="https://doc.rust-lang.org/std/option/enum.Option.html">它的文档</a><!-- ignore -->。熟悉 <code>Option&lt;T&gt;</code> 的方法将对你的 Rust 之旅非常有用。</p>
<p>总的来说,为了使用 <code>Option&lt;T&gt;</code> 值,需要编写处理每个成员的代码。你想要一些代码只当拥有 <code>Some(T)</code> 值时运行,允许这些代码使用其中的 <code>T</code>。也希望一些代码只在值为 <code>None</code> 时运行,这些代码并没有一个可用的 <code>T</code> 值。<code>match</code> 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="ch06-00-enums.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="ch06-02-match.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="ch06-00-enums.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="ch06-02-match.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="elasticlunr.min.js"></script>
<script src="mark.min.js"></script>
<script src="searcher.js"></script>
<script src="clipboard.min.js"></script>
<script src="highlight.js"></script>
<script src="book.js"></script>
<!-- Custom JS scripts -->
<script src="ferris.js"></script>
</div>
</body>
</html>