當前位置:
首頁 > 最新 > Rust 妙用:增補標準庫和統一錯誤

Rust 妙用:增補標準庫和統一錯誤

Meta

本系列文是 wayslog 我本人的一些關於 Rust 的總結,發上來將這些作為騙贊的資本。想起來就更新,想不起來也不要催更,log 很懶的。

Trait and Generic

初次接觸 Rust 的同學可能對其 trait 的概念表示不能理解,其實這個概念在其他 fp 語言里早已被應用的很廣泛了,Rust 作為一門 System Programming Language 引入這個概念,自然是想要讓它自己的 type system 擁有更好的表達能力。trait 被國內的翻譯者翻譯成了 特徵、特性, 但是我覺得都不能表達其原有意思,乾脆在這這裡保留原名字。它規定了一個類型能做的一系列 `動作`,可以看做約束,也可以認為是另一種形式上的 `標記`。具體怎麼使用呢,我們可以來看一下下面。

增補標準庫

Rust 提供了一個功能不算多的標準庫(就這還天天被吐槽標準庫太大了),於是很自然的,我們需要的某些功能在標準庫里就可能找不到了。但是,有的時候我們又希望對標準庫做一些增補。比如,我想對所有所有實現了 `std::io::Read` 的類型增補一個 `read_u8` 功能,想要從一個 `Read` 里直接讀出一個 `u8` 類型(對應 C++ 裡面的 char, Go 裡面的 byte 類型)。

對於其他 OOP 語言,比如 Java 這種,顯式繼承 interface 的語言,除非我們能拿到 JVM 的源碼,修改,並自己編譯一版標準庫,不然是不行的。但在 Rust 里,我們可以這麼做,先上代碼:

use std::io::; trait ReadExt: Read { fn read_u8(&mut self) -> Result { // 示例代碼,效率問題不在考慮之內 let mut bytes = [0u8; 1]; let size = try!(self.read(&mut bytes[..])); if size != 1 { return Err(Error::new(ErrorKind::UnexpectedEof, "read not one bytes")); } Ok(bytes[0]) } }

這裡有些點我們需要解釋下:

trait inherit

Rust 沒有提供 struct 的繼承機制,但是,提供了一個被官方稱為 `trait inherit` 的機制,具體表現就是上面代碼里的 `trait ReadExt: Read {...}` 。這裡,`ReadExt` 其實表示了一個類型範圍,這個範圍會比 `Read` 更廣泛。在 Rust 的其他地方,我們可以看到,除非是類型聲明,不然 `A: B` 符號總表示一個類型範圍: A 的(type Kind)範圍里包含 B。此條同樣適用於生命周期限制,因為生命周期也是 Rust 的一種 `Kind` 。好了,話題扯遠了,回歸主題。

我們看到,這裡我們規定了一個類型,所有實現 `Read` 的類型都可以實現一個 `ReadExt` 來實現 `讀取u8` 這麼一個操作。但是,僅僅這樣就能用了么?答案當然是不能。

我們附上上測試代碼

use std::io::; use std::io::Cursor; trait ReadExt: Read { fn read_u8(&mut self) -> Result {...} } fn main() { let mut cursor = Cursor::new(vec![0u8, 1, 2]); let value = cursor.read_u8().unwrap(); println!("read u8: {}", value); }

嘗試運行一下,我們得到了這麼個錯誤:

瓦塔?編譯信息說的明白:你必須對你使用的類型(這裡是`Cursor`) 實現 ReadExt。 所以,我們需要加上這麼一行:

trait ReadExt: Read {...} impl ReadExt for T {} fn main() {...}

這一行:impl ReadExt for T {}的意思再明白不過了,就是對所有實現了 trait Read 的類型 T 去實現 ReadExt。再次運行,成功。而且,這裡我們的 ReadExt 其實是沒有任何 *需要用戶提供的函數* 的。

Error Handle

異常,在我們目前常見的語言中,幾乎都有內建的機制。

無論是 C++/Java 的 try/catch, Python 的 try/expect,其實其本質就是一個終止執行並有機會 failover 的過程。與異常對應而生的,還有一個異常安全的問題。這一點,我們不展開。

異常,其實是一個非常經典而且直觀的錯誤解決方式。

而 Rust 採用了更加函數式而且統一的 Result/Option 來進行錯誤處理。這裡, Result 和 Option 不再是需要 runtime 參與的特殊類型,而是牢牢的嵌入到了其類型系統里。

繼續以上面的例子來看,我們將完整代碼附上:

use std::io::; use std::io::Cursor; trait ReadExt: Read { fn read_u8(&mut self) -> Result { let mut bytes = [0u8; 1]; let size = try!(self.read(&mut bytes[..])); if size != 1 { return Err(Error::new(ErrorKind::UnexpectedEof, "read not one bytes")); } Ok(bytes[0]) } } impl ReadExt for T {} fn main() { let mut cursor = Cursor::new(vec![0u8, 1, 2]); let value = cursor.read_u8().unwrap(); println!("read u8: {}", value); }

對於一個 io 操作,我們認為其在操作的時候很有可能會失敗,於是 `std::io` 默認提供了 Result 類型。翻開標準庫,我們可以看到:

// in std::io source code use std::result; pub type Result = result::Result;

好吧,我們知道了 std::io 下的 Result 類型其實是 std::result 類型下的別名,只不過限定死了錯誤類型是 std::io::Error。

對於一個 Result, 我們可以進行 `unwrap`, 這個操作其實和如下操作等效:

match some_func() { Ok(val) => val, Err(err) => { panic!("reason {:?}", err); } }

即:取出正確的值,一旦錯誤,則panic當前線程。

這其實是 Rust 的所有錯誤處理中最不值得推薦的一種處理,但是在程序原型構建期,或者 demo 里還是比較有用的。

相對的, 其實我們還可以進行 `match` 操作,這個關鍵字在 Rust 以及其他函數式語言里被稱為 `模式匹配`:

let value = match cursor.read_u8() { Ok(val) => val, Err(err) => { println!("read faild: {:?}", err); 0 } };

關於模式匹配的概念,這裡不再贅述,有想要深入了解的可以參見這裡 [模式匹配](模式匹配 · RustPrimer)。但是,就算有模式匹配,其實對於錯誤處理來說,也是略繁瑣,我不想在每個地方都進行一次模式匹配的,很多時候,其實模式匹配就是想拿出值來,但是遇到錯誤則將錯誤返回,比如下面:

let size = match self.read(&mut bytes[..]) { Ok(val) => val, Err(err) => { return Err(err); } };

為此,Rust 提供了一個簡寫的寫法:`try!` 宏。這個宏提供了如上的功能,於是我們在代碼里就看到了 try! 的用法:

let size = try!(self.read(&mut bytes[..]));

相對來說是簡潔了一點點,但是,Rust 的 Result 的很多操作是可以串聯起來的比如:

let value = try!(try!(try!(call0()).call1()).call2());try!(try(try!(....))) .....

這麼寫起來就太麻煩了,於是 Rust 提供了一個名叫 Carrier 的語法糖,在語法上用 `?` 代替,於是我們就可以這麼寫:

let value = call0()?.call1()?.call2()?;

其作用是和 try! 是一樣的。

Uniform Error Type (personal best practice)

這裡我們要說的,是 Rust 的兩個 trait : `From` 和 `Into`。 顧名思義,這是一對相反操作,表示 一種類型可以無任何風險的轉換成另一種類型變數(這裡其實還有一個限制:前後變數都是 ownership 的,但是這個在這裡不是重點)。重要的是, try! 和 ? 里,默認都調用了 Into 操作。前面說,From 和 Into 其實是反操作,那麼, Rust 庫里自然有互相實現他們的反操作的地方:

/// (註:摘自標準庫注釋) /// From for U implies Intofor T

預備知識介紹完畢,我們可以開始了。首先,準備一個錯誤類型和自己的 Result 類型:

#[derive(Debug)] pub enum MyError { } pub type MyResult = result::Result;

然後,我們讓這個類型能承包 `std::io::Error`:

#[derive(Debug)] pub enum MyError { IoError(Error), } impl From for MyError { fn from(oe: io::Error) -> MyError { MyError::IoError(oe) } }

PS: 我喜歡實現 From,當然你也可以去實現 Into ,一樣的。

這樣,我們實際上得到了一個 `std::io::Error` 的超集。我們最終,整理到的代碼就是:

use std::io::; use std::io::Cursor; use std::result; use std::convert::From; #[derive(Debug)] pub enum MyError { IoError(Error), } impl From for MyError { fn from(oe: io::Error) -> MyError { MyError::IoError(oe) } } pub type MyResult = result::Result; trait ReadExt: Read { fn read_u8(&mut self) -> MyResult { // 示例代碼,效率問題不在考慮之內 let mut bytes = [0u8; 1]; let size = self.read(&mut bytes[..])?; if size != 1 { return Err(Error::new(ErrorKind::UnexpectedEof, "read not one bytes"))?; } Ok(bytes[0]) } } impl ReadExt for T {} fn main() { let mut cursor = Cursor::new(vec![0u8, 1, 2]); let value = match cursor.read_u8() { Ok(val) => val, Err(err) => { println!("read faild: {:?}", err); 0 } }; println!("read u8: {}", value); }

基本上對原代碼破壞很小就實現了錯誤類型的統一。

以上。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 推酷 的精彩文章:

那個一站到底中的汪仔機器人,來眾測了?人機大戰誰會獲勝?
如何使用谷歌Chrome瀏覽器竊取Windows密碼
微軟直播應用 Beam 更名 Mixer,新增同屏多遊戲畫面直播功能
如何向面試官介紹「交互設計」作品?這篇絕對是最全面的!
推特的前 CEO 認為,Medium 是互聯網內容的希望

TAG:推酷 |

您可能感興趣

Python 標準庫之 collections 使用教程
Python每日一題:標準庫
Python 標準庫精華: collections.Counter
大佬第一步,Python標準庫
Python3標準庫簡介
Python標準庫可能準備大清洗了!
python標準庫:base64 模塊
Python標準庫——走馬觀花
Python 標準庫系列之模塊介紹
最全Python數據工具箱:標準庫、第三方庫和外部工具都在這裡了