ch06-01-defining-an-enum.md
commit c76f1b4d011fe59fc4f5e6f258070fc40d9921e4
結(jié)構(gòu)體給予你將字段和數(shù)據(jù)聚合在一起的方法,像 Rectangle
結(jié)構(gòu)體有 width
和 height
兩個字段。而枚舉給予你將一個值成為一個集合之一的方法。比如,我們想讓 Rectangle
是一些形狀的集合,包含 Circle
和 Triangle
。為了做到這個,Rust提供了枚舉類型。
讓我們看看一個需要訴諸于代碼的場景,來考慮為何此時使用枚舉更為合適且實用。假設(shè)我們要處理 IP 地址。目前被廣泛使用的兩個主要 IP 標(biāo)準(zhǔn):IPv4(version four)和 IPv6(version six)。這是我們的程序可能會遇到的所有可能的 IP 地址類型:所以可以 枚舉 出所有可能的值,這也正是此枚舉名字的由來。
任何一個 IP 地址要么是 IPv4 的要么是 IPv6 的,而且不能兩者都是。IP 地址的這個特性使得枚舉數(shù)據(jù)結(jié)構(gòu)非常適合這個場景,因為枚舉值只可能是其中一個成員。IPv4 和 IPv6 從根本上講仍是 IP 地址,所以當(dāng)代碼在處理適用于任何類型的 IP 地址的場景時應(yīng)該把它們當(dāng)作相同的類型。
可以通過在代碼中定義一個 IpAddrKind
枚舉來表現(xiàn)這個概念并列出可能的 IP 地址類型,V4
和 V6
。這被稱為枚舉的 成員(variants):
enum IpAddrKind {
V4,
V6,
}
現(xiàn)在 IpAddrKind
就是一個可以在代碼中使用的自定義數(shù)據(jù)類型了。
可以像這樣創(chuàng)建 IpAddrKind
兩個不同成員的實例:
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
注意枚舉的成員位于其標(biāo)識符的命名空間中,并使用兩個冒號分開。這么設(shè)計的益處是現(xiàn)在 IpAddrKind::V4
和 IpAddrKind::V6
都是 IpAddrKind
類型的。例如,接著可以定義一個函數(shù)來獲取任何 IpAddrKind
:
fn route(ip_kind: IpAddrKind) {}
現(xiàn)在可以使用任一成員來調(diào)用這個函數(shù):
route(IpAddrKind::V4);
route(IpAddrKind::V6);
使用枚舉甚至還有更多優(yōu)勢。進(jìn)一步考慮一下我們的 IP 地址類型,目前沒有一個存儲實際 IP 地址 數(shù)據(jù) 的方法;只知道它是什么 類型 的??紤]到已經(jīng)在第五章學(xué)習(xí)過結(jié)構(gòu)體了,你可能會像示例 6-1 那樣處理這個問題:
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"),
};
示例 6-1:將 IP 地址的數(shù)據(jù)和 IpAddrKind
成員存儲在一個 struct
中
這里我們定義了一個有兩個字段的結(jié)構(gòu)體 IpAddr
:IpAddrKind
(之前定義的枚舉)類型的 kind
字段和 String
類型 address
字段。我們有這個結(jié)構(gòu)體的兩個實例。第一個,home
,它的 kind
的值是 IpAddrKind::V4
與之相關(guān)聯(lián)的地址數(shù)據(jù)是 127.0.0.1
。第二個實例,loopback
,kind
的值是 IpAddrKind
的另一個成員,V6
,關(guān)聯(lián)的地址是 ::1
。我們使用了一個結(jié)構(gòu)體來將 kind
和 address
打包在一起,現(xiàn)在枚舉成員就與值相關(guān)聯(lián)了。
我們可以使用一種更簡潔的方式來表達(dá)相同的概念,僅僅使用枚舉并將數(shù)據(jù)直接放進(jìn)每一個枚舉成員而不是將枚舉作為結(jié)構(gòu)體的一部分。IpAddr
枚舉的新定義表明了 V4
和 V6
成員都關(guān)聯(lián)了 String
值:
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
我們直接將數(shù)據(jù)附加到枚舉的每個成員上,這樣就不需要一個額外的結(jié)構(gòu)體了。這里也很容易看出枚舉工作的另一個細(xì)節(jié):每一個我們定義的枚舉成員的名字也變成了一個構(gòu)建枚舉的實例的函數(shù)。也就是說,IpAddr::V4()
是一個獲取 String
參數(shù)并返回 IpAddr
類型實例的函數(shù)調(diào)用。作為定義枚舉的結(jié)果,這些構(gòu)造函數(shù)會自動被定義。
用枚舉替代結(jié)構(gòu)體還有另一個優(yōu)勢:每個成員可以處理不同類型和數(shù)量的數(shù)據(jù)。IPv4 版本的 IP 地址總是含有四個值在 0 和 255 之間的數(shù)字部分。如果我們想要將 V4
地址存儲為四個 u8
值而 V6
地址仍然表現(xiàn)為一個 String
,這就不能使用結(jié)構(gòu)體了。枚舉則可以輕易的處理這個情況:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
這些代碼展示了使用枚舉來存儲兩種不同 IP 地址的幾種可能的選擇。然而,事實證明存儲和編碼 IP 地址實在是太常見了以致標(biāo)準(zhǔn)庫提供了一個開箱即用的定義!讓我們看看標(biāo)準(zhǔn)庫是如何定義 IpAddr
的:它正有著跟我們定義和使用的一樣的枚舉和成員,不過它將成員中的地址數(shù)據(jù)嵌入到了兩個不同形式的結(jié)構(gòu)體中,它們對不同的成員的定義是不同的:
struct Ipv4Addr {
// --snip--
}
struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
這些代碼展示了可以將任意類型的數(shù)據(jù)放入枚舉成員中:例如字符串、數(shù)字類型或者結(jié)構(gòu)體。甚至可以包含另一個枚舉!另外,標(biāo)準(zhǔn)庫中的類型通常并不比你設(shè)想出來的要復(fù)雜多少。
注意雖然標(biāo)準(zhǔn)庫中包含一個 IpAddr
的定義,仍然可以創(chuàng)建和使用我們自己的定義而不會有沖突,因為我們并沒有將標(biāo)準(zhǔn)庫中的定義引入作用域。第七章會講到如何導(dǎo)入類型。
來看看示例 6-2 中的另一個枚舉的例子:它的成員中內(nèi)嵌了多種多樣的類型:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
示例 6-2:一個 Message
枚舉,其每個成員都存儲了不同數(shù)量和類型的值
這個枚舉有四個含有不同類型的成員:
Quit
?沒有關(guān)聯(lián)任何數(shù)據(jù)。Move
?類似結(jié)構(gòu)體包含命名字段。Write
?包含單獨(dú)一個 ?String
?。ChangeColor
?包含三個 ?i32
?。定義一個如示例 6-2 中所示那樣的有關(guān)聯(lián)值的枚舉的方式和定義多個不同類型的結(jié)構(gòu)體的方式很相像,除了枚舉不使用 struct
關(guān)鍵字以及其所有成員都被組合在一起位于 Message
類型下。如下這些結(jié)構(gòu)體可以包含與之前枚舉成員中相同的數(shù)據(jù):
struct QuitMessage; // 類單元結(jié)構(gòu)體
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // 元組結(jié)構(gòu)體
struct ChangeColorMessage(i32, i32, i32); // 元組結(jié)構(gòu)體
不過,如果我們使用不同的結(jié)構(gòu)體,由于它們都有不同的類型,我們將不能像使用示例 6-2 中定義的 Message
枚舉那樣,輕易的定義一個能夠處理這些不同類型的結(jié)構(gòu)體的函數(shù),因為枚舉是單獨(dú)一個類型。
結(jié)構(gòu)體和枚舉還有另一個相似點(diǎn):就像可以使用 impl
來為結(jié)構(gòu)體定義方法那樣,也可以在枚舉上定義方法。這是一個定義于我們 Message
枚舉上的叫做 call
的方法:
impl Message {
fn call(&self) {
// 在這里定義方法體
}
}
let m = Message::Write(String::from("hello"));
m.call();
方法體使用了 self
來獲取調(diào)用方法的值。這個例子中,創(chuàng)建了一個值為 Message::Write(String::from("hello"))
的變量 m
,而且這就是當(dāng) m.call()
運(yùn)行時 call
方法中的 self
的值。
讓我們看看標(biāo)準(zhǔn)庫中的另一個非常常見且實用的枚舉:Option
。
這一部分會分析一個 Option
的案例,Option
是標(biāo)準(zhǔn)庫定義的另一個枚舉。Option
類型應(yīng)用廣泛因為它編碼了一個非常普遍的場景,即一個值要么有值要么沒值。
例如,如果請求一個包含項的列表的第一個值,會得到一個值,如果請求一個空的列表,就什么也不會得到。從類型系統(tǒng)的角度來表達(dá)這個概念就意味著編譯器需要檢查是否處理了所有應(yīng)該處理的情況,這樣就可以避免在其他編程語言中非常常見的 bug。
編程語言的設(shè)計經(jīng)常要考慮包含哪些功能,但考慮排除哪些功能也很重要。Rust 并沒有很多其他語言中有的空值功能。空值(Null )是一個值,它代表沒有值。在有空值的語言中,變量總是這兩種狀態(tài)之一:空值和非空值。
Tony Hoare,null 的發(fā)明者,在他 2009 年的演講 “Null References: The Billion Dollar Mistake” 中曾經(jīng)說到:
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.
我稱之為我十億美元的錯誤。當(dāng)時,我在為一個面向?qū)ο笳Z言設(shè)計第一個綜合性的面向引用的類型系統(tǒng)。我的目標(biāo)是通過編譯器的自動檢查來保證所有引用的使用都應(yīng)該是絕對安全的。不過我未能抵抗住引入一個空引用的誘惑,僅僅是因為它是這么的容易實現(xiàn)。這引發(fā)了無數(shù)錯誤、漏洞和系統(tǒng)崩潰,在之后的四十多年中造成了數(shù)十億美元的苦痛和傷害。
空值的問題在于當(dāng)你嘗試像一個非空值那樣使用一個空值,會出現(xiàn)某種形式的錯誤。因為空和非空的屬性無處不在,非常容易出現(xiàn)這類錯誤。
然而,空值嘗試表達(dá)的概念仍然是有意義的:空值是一個因為某種原因目前無效或缺失的值。
問題不在于概念而在于具體的實現(xiàn)。為此,Rust 并沒有空值,不過它確實擁有一個可以編碼存在或不存在概念的枚舉。這個枚舉是 Option<T>
,而且它定義于標(biāo)準(zhǔn)庫中,如下:
enum Option<T> {
None,
Some(T),
}
Option<T>
枚舉是如此有用以至于它甚至被包含在了 prelude 之中,你不需要將其顯式引入作用域。另外,它的成員也是如此,可以不需要 Option::
前綴來直接使用 Some
和 None
。即便如此 Option<T>
也仍是常規(guī)的枚舉,Some(T)
和 None
仍是 Option<T>
的成員。
<T>
語法是一個我們還未講到的 Rust 功能。它是一個泛型類型參數(shù),第十章會更詳細(xì)的講解泛型。目前,所有你需要知道的就是 <T>
意味著 Option
枚舉的 Some
成員可以包含任意類型的數(shù)據(jù),同時每一個用于 T
位置的具體類型使得 Option<T>
整體作為不同的類型。這里是一些包含數(shù)字類型和字符串類型 Option
值的例子:
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
some_number
的類型是 Option<i32>
。some_char
的類型是 Option<char>
,這(與 some_number
)是一個不同的類型。因為我們在 Some
成員中指定了值,Rust 可以推斷其類型。對于 absent_number
, Rust 需要我們指定 Option
整體的類型,因為編譯器只通過 None
值無法推斷出 Some
成員保存的值的類型。這里我們告訴 Rust 希望 absent_number
是 Option<i32>
類型的。
當(dāng)有一個 Some
值時,我們就知道存在一個值,而這個值保存在 Some
中。當(dāng)有個 None
值時,在某種意義上,它跟空值具有相同的意義:并沒有一個有效的值。那么,Option<T>
為什么就比空值要好呢?
簡而言之,因為 Option<T>
和 T
(這里 T
可以是任何類型)是不同的類型,編譯器不允許像一個肯定有效的值那樣使用 Option<T>
。例如,這段代碼不能編譯,因為它嘗試將 Option<i8>
與 i8
相加:
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
如果運(yùn)行這些代碼,將得到類似這樣的錯誤信息:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` due to previous error
很好!事實上,錯誤信息意味著 Rust 不知道該如何將 Option<i8>
與 i8
相加,因為它們的類型不同。當(dāng)在 Rust 中擁有一個像 i8
這樣類型的值時,編譯器確保它總是有一個有效的值。我們可以自信使用而無需做空值檢查。只有當(dāng)使用 Option<i8>
(或者任何用到的類型)的時候需要擔(dān)心可能沒有值,而編譯器會確保我們在使用值之前處理了為空的情況。
換句話說,在對 Option<T>
進(jìn)行 T
的運(yùn)算之前必須將其轉(zhuǎn)換為 T
。通常這能幫助我們捕獲到空值最常見的問題之一:假設(shè)某值不為空但實際上為空的情況。
消除了錯誤地假設(shè)一個非空值的風(fēng)險,會讓你對代碼更加有信心。為了擁有一個可能為空的值,你必須要顯式的將其放入對應(yīng)類型的 Option<T>
中。接著,當(dāng)使用這個值時,必須明確的處理值為空的情況。只要一個值不是 Option<T>
類型,你就 可以 安全的認(rèn)定它的值不為空。這是 Rust 的一個經(jīng)過深思熟慮的設(shè)計決策,來限制空值的泛濫以增加 Rust 代碼的安全性。
那么當(dāng)有一個 Option<T>
的值時,如何從 Some
成員中取出 T
的值來使用它呢?Option<T>
枚舉擁有大量用于各種情況的方法:你可以查看它的文檔。熟悉 Option<T>
的方法將對你的 Rust 之旅非常有用。
總的來說,為了使用 Option<T>
值,需要編寫處理每個成員的代碼。你想要一些代碼只當(dāng)擁有 Some(T)
值時運(yùn)行,允許這些代碼使用其中的 T
。也希望一些代碼在值為 None
時運(yùn)行,這些代碼并沒有一個可用的 T
值。match
表達(dá)式就是這么一個處理枚舉的控制流結(jié)構(gòu):它會根據(jù)枚舉的成員運(yùn)行不同的代碼,這些代碼可以使用匹配到的值中的數(shù)據(jù)。
更多建議: