rust 的枚舉
在講述Result或Option之前,我們有必要先了解一下rust的枚舉;因為Result和Option都是枚舉類型。
概念上rust的枚舉與C語言的枚舉是一致:定義一個類型,可以窮舉所有可能的值。比如,定義一個IP地址類型:
enum IpAddrKind {
IPV4,
IPV6,
}
IP地址要么是IPV4, 要么是IPV6。 對于rust ip :IpAddrKind , ip 的值也是IPV4 或IPV6之一。
枚舉類型在使用時,通常也需要和數據關聯。在C語言中,當我們需要將枚舉類型和和數據關聯時,通常是額外定義一個結構體,將類型標識和數據封裝在一起:
struct IpAddr {
enum IpAddrKind kind;
char data[16];
}
如此,當我們獲得一個IpAddr實例時,可以根據其kind值先確認其地址類型;然后再根據其地址類型,從data中解析出具體數地址數據。
注意:這里data使用了共享存儲的方法;原因是這里將IPv6 和IPv4的數據都定義為了字符數組,并且IPv6的長度可以覆蓋IPv4。假如,重新定義IP的數據類型為 IPv4為一個U32整形,IPv6為一個字符數組,那么IpAddr的結構體可能會設計為這樣:
struct IpAddr {
enum IpAddrKind kind;
union {
unsigned int ipv4;
char ipv6[16];
} data;
}
rust 相對于C的枚舉,對枚舉類型做了大幅優化,允許我們直接將關聯數據類型直接嵌入到枚舉的變體中。比如,rust定義的IpAddr 可能是這樣:
enum IpAddr {
IPV4 (String),
IPV6 (String),
}
使用:
let loopback = IpAddr::IPV4("127.0.0.1".to_string()); // 定義了一個ipv4地址,其值“127.0.0.1”
簡單起見,可以理解為rust 的枚舉,融合了C枚舉和聯合體,實現了數據類型和關聯數據的定義和綁定。
一個稍微復雜一點的枚舉類型:
enum Message {
Quit, // 無綁定數據
Move {x: i32, y:i32}, // 綁定了一個匿名結構體 struct {x:i32, y:i32}
Write(string), // 綁定了一個字符串數據
ChangeColor(i32, i32, i32), // 綁定了一個元祖,由三個i32 組成
}
枚舉方法
在rust 里面您還可以為枚舉實現方法。這就像在面向對象編程時,為class (java)或結構體(rust, golang)綁定方法一樣。和rust 的struct 實現方法一樣,用impl關鍵字為指定的枚舉類型添加方法:
impl Message {
fn call(&self) {
// do_something()
}
}
// example
let msg = Message::Write(String::from("notice: processing going down"));
msg.call();
枚舉與match(控制流運輸符號)
rust中有一個強大的控制流運算符:match,它允許將一個值與一些列模式進行匹配,并根據匹配的模式執行相關代碼(關于rust的模式匹配,本文不深入,讀者自行補充);而其中枚舉是模式匹配中最為常用的:
impl Message {
fn call(&self) {
// do_something()
}
fn to_string(&self) -> String {
match self {
Message::Quit => String::from("quit"),
Message::Move { x, y } => format!("Move: <{},{}>", x, y),
Message::Write(s) => format!("String: {}", s),
Message::ChangeColor(x, y, z) => format!("ChangeColor: ({},{},{})", x, y, z),
}
}
}
上述示例中,為Message枚舉實現的to_string方法返回某個具體示例的字符串值;其中就使用了match模式匹配。match 和C中的switch關鍵字比較類似,但比switch更為強大。
與其他語言不一樣,rust在匹配枚舉時,要求務必窮盡所有可能(當然,可以用通配的方法忽略不在意的變體)。
簡單控制流 if let
前面已經提到,match 在遍歷枚舉時,要求務必窮盡所有可能。但有時候,我們確實只關注某一種匹配的情況,而忽略其他情況。當然,這種場景可以用match 的 '_' 通配的方式,來忽略其它不關心的變體,只是多寫了了幾行廢代碼而已。
幸運的是,rust 提供了一個if let 語法,可以簡化這種場景的表達:
impl Message {
fn on_quit(&self) {
if let Message::Quit = self {
std::process::exit(0);
}
}
}
Option
在其他語言中,大部分都支持空值(Null,nil):本身是一個值,卻表示‘沒有值’。在支持空值的語言中,一個值可能處于兩種狀態:空值或非空值。
比如 c語言中,定義一個變量 char* ptr ,那么默認情況下ptr 就是空值(null)。
空值的問題在于,當你嘗試像使用非空值那樣去使用空值的時候,就會觸發某種程度的錯誤(通常可能導致程序崩潰,比如訪問空指針)。另一方面,因為這種存在雙狀態的值被廣泛應用于程序中時,你很難避免引發類似問題。
但是,不管怎樣,空值本身所嘗試表達的概念任然具有意義:它代表了因某種原因而無法獲取、或者變為無效的值。
rust語言中沒有空值,但卻提供了一個擁有類似概念的枚舉:`Option<T>`。我們可以用它來表示任意一個可能存在空值的值。
enum Option<T> {
Some(T),
None,
}
在對待空值上,rust和其他支持空值的語言上有所差異。一般支持空值的語言,對于數據是否為空值,由程序員自己保證,語言上并不限制。但在rust 中`Option<T>` 包裹的值,需要特別處理。例如:
let x = 5;
let y: Option<i32> = Some(8);
let sum = x+y;
println!(“{}”, sum);
上面代碼看上去沒有問題,但實際上卻無法通過編譯。編譯器指出,i32 和`Option<T>`,不支持相加行為,因為他們是不同類型。
rust中,對于一個 給定類型的變量(基礎類型或者結構體),例子中的x,編譯器保證它是有效的;但相反,一個`Option<T>`的變量,rust要求我們必須確認它是具有值的情況下,才可以使用。
換句話說,`Option<T> `中可能存在T,也可能是空值;我們必須確認它有值,并且將其轉換為T才能夠使用它。經過這個過程,就幫助我們甄別了值是否真實存在,從而避免了“使用了一個值,但它卻是空值”的陷阱!
let x = 5;
let y: Option<i32> = Some(8);
let sum: i32 = 0;
match y {
Some(t) => {
sum = x+t; // 確保有值,并使用該值
},
- => (),
}
println!("sum={}", sum)
使用模式匹配來處理返回值,調用者必須處理結果為None的情況。這往往是一個好的編程習慣,可以減少潛在的bug。Option 包含一些方法來簡化模式匹配,畢竟過多的match會使代碼變得臃腫,這也是滋生bug的原因之一。
unwrap
impl<T> Option<T> {
fn unwrap(self) -> T {
match self {
Option::Some(val) => val,
Option::None => {
panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
unwrap 是Option的一個工具函數。當遇到None值時會panic。
通常panic 并不是一個良好的工程實踐,不過有些時候卻非常有用:
- 在例子和簡單快速的編碼中 有的時候你只是需要一個小例子或者一個簡單的小程序,輸入輸出已經確定,你根本沒必要花太多時間考慮錯誤處理,使用unwrap變得非常合適。
- 當程序遇到了致命的bug,panic是最優選擇
map
pub fn map<U, F>(self, f: F) -> Option<U>
where
F: FnOnce(T) -> U,
{
match self {
Some(x) => Some(f(x)),
None => None,
}
}
map 是Option的一個工具函數:對一個Option類型的值,如果其值非空,那么通過一個映射函數,映射為一個新類型;否則返回為None。
假如我們要在一個字符串中找到文件的擴展名,比如foo.rs中的rs, 我們可以這樣:
fn extension_explicit(file_name: &str) -> Option<&str> {
match find(file_name, '.') {
None => None,
Some(i) => Some(&file_name[i+1..]),
}
}
fn main() {
match extension_explicit("foo.rs") {
None => println!("no extension"),
Some(ext) => assert_eq!(ext, "rs"),
}
}
// 使用map去掉match
fn extension(file_name: &str) -> Option<&str> {
find(file_name, '.').map(|i| &file_name[i+1..])
}
注意上面 “|i| &file_name[i+1..]” 的寫法是一個閉包函數。關于rust的閉包函數,請讀者自行了解學習。
unwrap_or
fn unwrap_or<T>(option: Option<T>, default: T) -> T {
match option {
None => default,
Some(value) => value,
}
}
unwrap_or 提供了一個默認值default,當值為None時返該默認值。
and_then
fn and_then<F, T, A>(option: Option<T>, f: F) -> Option<A>
where F: FnOnce(T) -> Option<A> {
match option {
None => None,
Some(value) => f(value),
}
}
看起來and_then和map差不多, 當Option 非空時調用f函數,對傳輸數據進行處理,否則返回None。
與map的差異一方面是語義上的差異,map側重于映射,而and_then表達豐富的后續處理;另一方面,在返回類型上and_then不限制,而map 保持輸入和輸出一致。可以認為,map 是and_then的一種特例。
Result
編程實踐中,對于程序中的錯誤,通常分為兩類: 不可恢復的錯誤 和可以恢復的錯誤。對于可恢復的錯誤,比如文件未找到,一般是報告給用戶,讓其重試;而不可恢復錯誤,比如數組訪問越界了,則會引起程序進入異常狀態。
在有異常處理的編程語言中,通常并不詳細區分這兩種錯誤,而是統一交由異常處理機制處理。rust沒有異常處理機制,通常對于不可恢復錯誤,會采用panic結束程序;而對于可恢復錯誤,則更傾向通過顯示的機制進行錯誤捕獲和傳遞。對于可恢復錯誤,rust采用Result類型來描述。
Result 是一個枚舉,有Ok 和 Err兩個變體:
enum Result<T, E> {
Ok(T),
Err(E),
}
其中,T和E均為泛型類型。
有了前兩節的知識鋪墊,理解這個枚舉并不困難,可以描述為:
1. 一個可能處理失敗的過程,其結果用Result來表示;
2. 如果處理成功,那么返回Result的Ok 變體,并且攜帶返回數據;
3. 如果處理失敗,那么返回Result的Err變體,并且攜帶錯誤信息。
示例:
let ret = File::open("test.txt");
let f = match ret {
Ok(file) => file,
Err(err) = {
panic!("fail to open test.txt, error: {:?}", err );
}
}
// f.XXXX()
工具函數
Result 和Option 非常相似,甚至可以理解為,Result是Option更為通用的版本,在異常的時候,返回了更多的錯誤信息;而Option 只是Result Err 為空的特例。
type Option<T> = Result<T, ()>;
和Option一樣,Result 也提供了 unwrap,unwrap_or, map,and_then 等系列工具方法。比如 unwarp實現:
impl<T, E: ::std::fmt::Debug> Result<T, E> {
fn unwrap(self) -> T {
match self {
Result::Ok(val) => val,
Result::Err(err) =>
panic!("called `Result::unwrap()` on an `Err` value: {:?}", err),
}
}
}
沒錯和Option一樣,不同的是,Result包括了錯誤的詳細描述,這對于調試人員來說,這是友好的。
除此之外,相比于Option, Result也有一些特有的針對錯誤類型的方法map_err和or_else等。
其中:
map_err 處理一個Result,當前是某種錯誤類型時,通過傳入的op方法,轉換其錯誤類型; 如果是非錯誤類型,則不受影響。
pub fn map_err<F, O: FnOnce(E) -> F>(self, op: O) -> Result<T, F> {
match self {
Ok(t) => Ok(t),
Err(e) => Err(op(e)),
}
}
or_else 處理一個Result并返回一個Result,當前是某種錯誤時,通過傳入的op方法,處理錯誤;如果是非錯誤類型,則不受影響。
pub fn or_else<F, O: FnOnce(E) -> Result<T, F>>(self, op: O) -> Result<T, F> {
match self {
Ok(t) => Ok(t),
Err(e) => op(e),
}
}
or_else 通常用于鏈式調用的流程控制。例如:
fn auto_fix(e: u32) -> Result<u32, u32> { Ok(e * e) }
fn keep(e: u32) -> Result<u32, u32> { Err(e) }
// 用例1和2,由于 原始Result值非 錯誤,所以不受or_else影響
assert_eq!(Ok(2).or_else(auto_fix).or_else(auto_fix), Ok(2));
assert_eq!(Ok(2).or_else(keep).or_else(auto_fix), Ok(2));
// 用例3, Err類型的Result 經過auto_fix 后已經轉為Ok(9);經過第二個or_else 不受影響
assert_eq!(Err(3).or_else(auto_fix).or_else(keep), Ok(9));
// 用例4, Err類型的Result 連續調用or_else 的keep,由于keep實現保留err返回為Err(3); 注意實際上Result實例時變化了的
assert_eq!(Err(3).or_else(keep).or_else(keep), Err(3));
Result別名
在Rust的標準庫中會經常出現Result的別名,用來默認確認其中Ok(T)或者Err(E)的類型,這能減少重復編碼。比如io::Result
use std::num::ParseIntError;
use std::result;
type Result<T> = result::Result<T, ParseIntError>;
fn double_number(number_str: &str) -> Result<i32> {
unimplemented!();
}
組合Option和Result
Option的方法ok_or:
fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> {
match option {
Some(val) => Ok(val),
None => Err(err),
}
}
可以在值為None的時候返回一個Result::Err(E),值為Some(T)的時候返回Ok(T),利用它我們可以組合Option和Result:
use std::env;
fn double_arg(mut argv: env::Args) -> Result<i32, String> {
argv.nth(1)
.ok_or("Please give at least one argument".to_owned())
.and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
.map(|n| 2 * n)
}
fn main() {
match double_arg(env::args()) {
Ok(n) => println!("{}", n),
Err(err) => println!("Error: {}", err),
}
}
double_arg將傳入的命令行參數轉化為數字并翻倍,ok_or將Option類型轉換成Result,map_err當值為Err(E)時調用作為參數的函數處理錯誤。
try! 宏
macro_rules! try {
($e:expr) => (match $e {
Ok(val) => val,
Err(err) => return Err(::std::convert::From::from(err)),
});
}
try!事實上就是match Result的封裝,當遇到Err(E)時會提早返回, ::std::convert::From::from(err)可以將不同的錯誤類型返回成最終需要的錯誤類型,因為所有的錯誤都能通過From轉化成`Box<Error>`,所以下面的代碼是正確的:
use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::path::Path;
fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Box<Error>> {
let mut file = try!(File::open(file_path));
let mut contents = String::new();
try!(file.read_to_string(&mut contents));
let n = try!(contents.trim().parse::<i32>());
Ok(2 * n)
}
在新版本中 try!宏被進一步簡化為 一個?:
fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Error> {
let mut file = File::open(file_path)?; // 注意這里的?, 和try功能一致,遇到錯誤,提前返回
let mut contents = String::new();
try!(file.read_to_string(&mut contents));
let n = try!(contents.trim().parse::<i32>());
Ok(2 * n)
}
總結
rust的Option 和Result 為返回、檢測、處理錯誤,提供了系統支撐,這一點和golang的errors 設計比價類似。
熟練使用Option和Result是編寫 Rust 代碼的關鍵,Rust 優雅的錯誤處理離不開值返回的錯誤形式,編寫代碼時提供給使用者詳細的錯誤信息是值得推崇的。