楔子 枚舉類型,通常也被簡稱為枚舉,它允許我們列舉所有可能的值來定義一個類型。我們知道 C 里面也有枚舉,但 Rust 的枚舉要遠比 C 的枚舉更加強大。 下面我們來學(xué)習(xí)一下 Rust 的枚舉。 枚舉值 讓我們來嘗試處理一個實際的編碼問題,并接著討論在這種情形下,為什么使用枚舉要比結(jié)構(gòu)體更加合適。假設(shè)我們需要對 IP 地址進行處理,而目前有兩種被廣泛使用的 IP 地址標準:IPv4 和 IPv6。因為我們只需要處理這兩種情形,因此可以將所有可能的值枚舉出來,這也正是枚舉名字的由來。 另外,一個 IP 地址要么是 IPv4 的,要么是 IPv6 的,沒有辦法同時滿足兩種標準。這個特性使得 IP 地址非常適合使用枚舉結(jié)構(gòu)來進行描述,因為枚舉的值最終只能是這些值當中的一個。但無論是 IPv4 還是 IPv6,它們都屬于基礎(chǔ)的 IP 地址協(xié)議,所以當我們需要在代碼中處理 IP 地址時,應(yīng)該將它們視作同一種類型。
我們通過定義枚舉 IpAddrKind 來表達這樣的概念,聲明該枚舉需要列舉出所有可能的 IP 地址種類:V4 和 V6,這也就是所謂的枚舉變體(variant),或者說人話就是枚舉里面的成員。 現(xiàn)在,IpAddrKind 就是一個可以在代碼中隨處使用的自定義數(shù)據(jù)類型了,我們可以像下面的代碼一樣分別使用 IpAddrKind 中的兩個成員來創(chuàng)建實例:
需要注意的是,枚舉的成員全都位于其標識符的命名空間中,并使用兩個冒號來將標識符和成員分隔開來。由于 IpAddrKind::V4 和 IpAddrKind::V6 擁有相同的類型 IpAddrKind,所以我們可以定義一個接收 IpAddrKind 類型參數(shù)的函數(shù)來統(tǒng)一處理它們。
除此之外,使用枚舉還有很多優(yōu)勢。讓我們繼續(xù)考察這個 IP 地址類型,到目前為止,我們只能知道 IP 地址的種類,卻還沒有辦法去存儲實際的 IP 地址數(shù)據(jù)。不過剛剛學(xué)習(xí)了結(jié)構(gòu)體,我們可以這么做。
辦法總比困難多,我們將枚舉類型和一個字符串組合成一個結(jié)構(gòu)體不就可以了嗎,這是一個解決問題的辦法,不過實際上,枚舉允許我們直接將其關(guān)聯(lián)的數(shù)據(jù)嵌入枚舉成員內(nèi)。我們可以使用枚舉來更簡捷地表達出上述概念,而不用將枚舉集成至結(jié)構(gòu)體中。 在新的 IpAddr 枚舉定義中,V4 和 V6 兩個成員都被關(guān)聯(lián)上了一個 String 值:
我們直接將數(shù)據(jù)附加到了枚舉的每個成員中,這樣便不需要額外地使用結(jié)構(gòu)體。另外一個使用枚舉代替結(jié)構(gòu)體的優(yōu)勢在于:每個成員可以擁有不同類型和數(shù)量的關(guān)聯(lián)數(shù)據(jù)。 還是以 IP 地址為例,IPv4 地址總是由 4 個 0~255 之間的整數(shù)部分組成。假如我們希望使用 4 個 u8 值來代表 V4 地址,并依然使用 String 值來代表 V6 地址,那么結(jié)構(gòu)體就無法輕易實現(xiàn)這一目的了,而枚舉則可以輕松地處理此類情形:
可以看到非常方便,然后繼續(xù)來看另外一個關(guān)于枚舉的例子,它的成員包含了各式各樣的數(shù)據(jù)類型。
和單獨定義結(jié)構(gòu)體不同,如果我們使用了不同的結(jié)構(gòu)體,那么每個結(jié)構(gòu)體都會擁有自己的類型,我們無法輕易定義一個能夠統(tǒng)一處理這些類型數(shù)據(jù)的函數(shù)。而我們上面定義的 Message 枚舉則不同,因為它是單獨的一個類型,也就是說變量 x, y, z, w 都是 Message 類型。 因此 Rust 枚舉非常強大,當我們需要處理的數(shù)據(jù)彼此獨立、但類型又不同時,使用枚舉再合適不過了。比如處理 Excel 的時候,每個單元格存儲的數(shù)據(jù)可能是整數(shù)、浮點數(shù)、字符串,也就是類型是不同的,而且每個單元格之間也沒啥關(guān)系,這個時候枚舉是非常適合的。
枚舉和結(jié)構(gòu)體還有一點相似的地方在于:正如我們可以使用 impl 關(guān)鍵字為結(jié)構(gòu)體定義方法一樣,我們同樣也可以給枚舉定義方法。下面的代碼在 Cell 枚舉中實現(xiàn)了一個名為 call 的方法:
以上就是枚舉的實現(xiàn),然后標準庫中也提供了一種非常常見并且大量使用的枚舉:Option。 用于空值處理的 Option 枚舉 在設(shè)計編程語言時往往會規(guī)劃出各式各樣的功能,但思考應(yīng)該避免設(shè)計哪些功能也是一門非常重要的功課。Rust 并沒有像許多其他語言一樣支持空值,因為空值本身是一個值,但它的含義卻是沒有值。在設(shè)計有空值的語言中,一個變量往往處于這兩種狀態(tài):空值或非空值。
空值的問題在于,當你嘗試像使用非空值那樣使用空值時,就會觸發(fā)某種程度上的錯誤。因為空或非空的屬性被廣泛散布在程序中,所以你很難避免引起類似的問題。但是不管怎么說,空值本身所嘗試表達的概念仍然是有意義的:它代表了因為某種原因而變?yōu)闊o效或缺失的值。 所以引發(fā)這些問題的關(guān)鍵并不是概念本身,而是那些具體的實現(xiàn)措施。因此 Rust 中雖然沒有空值,但卻提供了一個擁有類似概念的枚舉,我們可以用它來標識一個值無效或缺失。這個枚舉就是 Option<T>,它在標準庫中被定義為如下所示的樣子:
由于 Option<T> 非常常見且很有用,所以它被包含在了預(yù)導(dǎo)入模塊中,這意味著我們不需要顯式地將它引入作用域。另外它的成員也是這樣的:我們可以在不加 Option:: 前綴的情況下直接使用 Some 或 None,但 Option<T> 枚舉依然只是一個普通的枚舉類型,Some(T) 和 None 也依然只是 Option<T> 類型的成員。 然后里面的語法 <T> 是一個我們還沒有學(xué)到的 Rust 功能,它是一個泛型參數(shù),我們將會在后續(xù)討論關(guān)于泛型的更多細節(jié)?,F(xiàn)在只需要知道 <T> 意味著 Option 枚舉中的 Some 成員可以包含任意類型的數(shù)據(jù),或者說 Option<T> 表示變量類型為 T,但允許為空值。下面是一些使用 Option 包含數(shù)值類型和字符串類型的示例:
比如一個整數(shù)、但可以為空值,那么類型就是 Option<i32>;字符串、但可以為空值,那么類型就是 Option<String>。 然后我們在賦值的時候就可以使用 Some,比如 Some(5),編譯器看到 Some 就知道這是一個 Option<T>,看到 5 就知道這是一個 i32,結(jié)合起來會將變量類型設(shè)置為 Option<i32>。 使用 Some 則是我們已經(jīng)想好變量的值了,而使用 None 則是我們還不知道要給變量賦什么值,但只知道它允許為空,所以先設(shè)置為 None。并且設(shè)置為 None 的時候我們需要顯式地指明變量的類型,否則光憑一個 None 的話,Rust 無法推斷。 總結(jié):當我們有了一個 Some 值時,我們就可以確定值是存在的;而當我們有了一個 None 值時,我們就知道當前并不存在一個有效的值,但它們都是 Option<T> 類型。那么問題來了,這看上去與空值沒有什么差別,那為什么 Option<T> 的設(shè)計就比空值好呢? 簡單來講,因為 Option<T> 和 T(這里的 T 可以是任意類型)是不同的類型,所以編譯器不會允許我們像使用普通值一樣去使用 Option<T>。例如下面的代碼在嘗試將 i8 與 Option<i8> 相加時無法通過編譯:
運行這段代碼會編譯錯誤,提示信息:no implementation for `i8 + Option<i8>`。這段錯誤提示信息指出了 Rust 無法理解 i8 和 Option<T> 相加的行為,因為它們擁有不同的類型。 如果我們在 Rust 中擁有一個 i8 類型的值,編譯器是能夠確保我們所持有的值是有效的,可以充滿信心地去使用它而無須在使用前進行空值檢查。而只有當我們持有的類型是 Option<i8>(將 i8 換成其它類型同理)時,我們才必須要考慮值不存在的情況,同時編譯器會迫使我們在使用值之前正確地做出處理操作。 也就是說 Some(5) 雖然是有值的,但它的類型是 Option<T>,而該類型還包含了 None,如果為 None 則是無法相加的,所以 Rust 會迫使我們進行處理。換句話說,為了使用 Option<T> 中可能存在的 T,我們必須要將它轉(zhuǎn)換為 T。一般而言,這能幫助我們避免使用空值時最常見的一個問題:假設(shè)某個值存在,實際上卻為空。
不過當我們持有了一個 Option<T> 類型的 Some 值時,應(yīng)該怎樣將其中的 T 值取出來使用呢? 總的來說,為了使用一個 Option<T> 值,我們必須要編寫處理每個成員的代碼,某些代碼只會在持有 Some(T) 值時運行,它們可以使用成員中存儲的 T;而另外一些代碼則只會在持有 None 值時運行,這些代碼將沒有可用的 T 值。 match 表達式就是這么一個可以用來處理枚舉的控制流結(jié)構(gòu):它允許我們基于枚舉擁有的成員來決定運行的代碼分支,并允許代碼通過匹配值來獲取成員內(nèi)的數(shù)據(jù)。 控制流運算符 match Rust 中有一個異常強大的控制流運算符:match,它允許將一個值與一系列的模式相比較,并根據(jù)匹配的模式執(zhí)行相應(yīng)代碼。模式可由字面量、變量名、通配符和許多其它東西組成;后面會詳細介紹所有不同種類的模式及它們的工作機制。match 的能力不僅來自模式豐富的表達力,也來自編譯器的安全檢查,它確保了所有可能的情況都會得到處理。 你可以將 match 表達式想象成一臺硬幣分類機:硬幣滑入有著不同大小孔洞的軌道,并且掉入第一個符合大小的孔洞。同樣,值也會依次通過 match 中的模式,并且在遇到第一個符合的模式時進入相關(guān)聯(lián)的代碼塊,并在執(zhí)行過程中被代碼所使用。
讓我們先來逐步分析一下 value_in_operator 函數(shù)中的 match 塊,首先我們使用的 match 關(guān)鍵字后面會跟隨一個表達式,也就是本例中的 op。初看上去,這與 if 表達式的使用十分相似,但這里有個巨大的區(qū)別:在 if 語句中,表達式需要返回一個布爾值,而這里的表達式則可以返回任何類型,例子中 op 的類型正是我們在首行定義的 Operator 枚舉。 接下來是 match 的分支,一個分支由模式和它所關(guān)聯(lián)的代碼組成。第一個分支采用了值 Operator::LT 作為模式,并緊跟著一個 => 運算符用于將模式和代碼區(qū)分開來。這里的代碼簡單地返回了值 1,不同分支之間使用了逗號分隔。 當這個 match 表達式執(zhí)行時,它會將產(chǎn)生的結(jié)果值依次與每個分支中的模式相比較。假如模式匹配成功,則與該模式相關(guān)聯(lián)的代碼就會被繼續(xù)執(zhí)行;而假如模式匹配失敗,則會繼續(xù)匹配下一個分支,就像上面提到過的硬幣分類機一樣。分支可以有任意多個,在上述示例中,match 有 6 個分支。 每個分支所關(guān)聯(lián)的代碼同時也是一個表達式,而這個表達式運行所得到的結(jié)果值,同時也會被作為整個 match 表達式的結(jié)果返回。 如果分支代碼足夠短,就像上述示例中僅返回一個值的話,那么通常不需要使用花括號。但如果我們想要在一個匹配分支中包含多行代碼,那么就可以使用花括號將它們包起來。舉個例子:
需要注意的是,使用 match 的時候,所有可能出現(xiàn)的情況都需要被處理。比如上面的 op,它是 Operator 枚舉類型,該枚舉有 6 個成員,那么 match 里面就應(yīng)該有 6 個分支。 匹配值 匹配分支另外一個有趣的地方在于它們可以綁定被匹配對象的部分值,而這也正是我們用于從枚舉變體中提取值的方法。上面的 Operator 枚舉是不帶數(shù)據(jù)的,如果帶數(shù)據(jù)了該怎么辦呢?以我們之前的 IP 地址為例。
然后我們就可以處理之前的 Option<T> 了,做法也很簡單:
將 match 與枚舉相結(jié)合在許多情形下都是非常有用的,后續(xù)會在 Rust 代碼中看到許多類似的套路:使用 match 來匹配枚舉值,并將其中的值綁定到某個變量上,接著根據(jù)這個值執(zhí)行相應(yīng)的代碼。這初看起來可能會有些復(fù)雜,不過一旦你習(xí)慣了它的用法,就會希望在所有的語言中都有這個特性,這一特性一直以來都是社區(qū)用戶的最愛。 _ 通配符 先看個例子:
如果傳過來的是 Some(666),那么打印 bingo,否則什么也不做。注意:None 這個分支要返回空元祖,因為所有分支的最后一個表達式返回的值都要是同一種類型。但是上面這種做法有些麻煩,可以簡化一下:
Some(i) 里面的 i 可以是任意值,所以 Some(i) 相當于包含了所有可能出現(xiàn)的情況,只要不為 None 就都會走這個分支,然后值會傳遞給 i,再對 i 進行操作。而 Some(666) 則表示只有當 Some(666) 的時候才會走這個分支,如果為 None 或者 Some 里面的值不為 666 時,那么走默認分支。所以此時需要有 _ 分支,因為 match 要處理所有可能出現(xiàn)的情況。
Some(666) 里面是一個常量,因此它只能處理 Some(666) 的情況;而 Some(i) 里面的 i 不是一個具體的常量,所以它包含了包括 666 在內(nèi)的所有可能出現(xiàn)的值(這里是 i32)。 但注意 Some(666) 應(yīng)該放在 Some(i) 的上面,否則永遠不會匹配到。另外,既然有了 Some(i),那么也就不需要有默認分支了,因為 i 是一個變量,它已經(jīng)包含了所有的情況,所以此時只剩 None 這一個分支。當然把 None 換成 _ 也可以,只不過此時的默認分支只可能匹配 None。 另外值得一提的是,非枚舉類型也可以使用 match,比如整型。
或者換一種方式:
不過說實話,我們使用 match 基本上都是用來匹配枚舉類型,像整型這些使用 if else 不香嗎? 簡單控制流 if let if let 能讓我們通過一種不那么煩瑣的語法結(jié)合使用 if 與 let,并處理那些只用關(guān)心某一種匹配而忽略其他匹配的情況。回到之前的例子:
這個例子已經(jīng)夠簡單了,但我們還可以更進一步簡化:
所以我們可以將 if let 視作 match 的語法糖,它只在值滿足某一特定模式時運行代碼,而忽略其他所有的可能性。因此使用 if let 意味著可以編寫更少的代碼,使用更少的縮進,使用更少的模板代碼。但同時也放棄了 match 所附帶的窮盡性檢查,因為 match 需要處理所有可能出現(xiàn)的分支,否則編譯不通過。究竟應(yīng)該使用 match 還是 if let 取決于你當時所處的環(huán)境,這是一個在代碼簡捷性與窮盡性檢查之間進行取舍的過程。 不過問題來了,我們記得非枚舉也可以使用 match,而 if let 又是 match 的語法糖,那么非枚舉類型是不是同樣也可以使用 if let 呢?
答案是可以的,if let x = 666 和 if x == 666 作用是相似的,并且都可以搭配 else 語句。如果你在編寫程序的過程中,覺得在某些情形下使用 match 會過分煩瑣,要記得在 Rust 工具箱中還有 if let 的存在。 小結(jié) 前面介紹的結(jié)構(gòu)體可以讓我們基于特定領(lǐng)域創(chuàng)建有意義的自定義類型,通過使用結(jié)構(gòu)體可以將相關(guān)聯(lián)的數(shù)據(jù)組合起來,并為每條數(shù)據(jù)賦予名字,從而使代碼變得更加清晰。方法可以讓我們?yōu)榻Y(jié)構(gòu)體實例指定行為,而關(guān)聯(lián)函數(shù)則可以將那些不需要實例的特定功能放置到結(jié)構(gòu)體的命名空間中。 而枚舉可以包含一系列可被列舉的值,在面對數(shù)據(jù)類型單一、但不同數(shù)據(jù)的類型可能不同的場景時非常有用。同時我們也展示了如何使用標準庫中的 Option<T> 類型,以及它會如何幫助我們利用類型系統(tǒng)去避免錯誤。 最后當枚舉中包含數(shù)據(jù)時,我們可以使用 match 或 if let 來抽取并使用這些值,具體應(yīng)該使用哪個工具則取決于我們想要處理的情形有多少。 |
|