Rust 語言學習筆記(二)

(一)在這裏,下面是(二)。html

引用與生命期

寫了一天的 Rust 代碼下來,發現根本沒寫幾行,萬事開頭難啊。仔細想來,多半功夫花在學習最佳實踐和調試編譯錯誤上了。說到這編譯錯誤我就氣不打一出來,放着好生生的 Python 不用跑這來糟心~~呵呵,開玩笑,Rust 的語法——尤爲是對於內存管理——可謂是至關精密,在調好了許多編譯錯誤以後,每每會發出「原來是這樣」的感嘆。對於初學者的我來講,着實不該該指望着一上來就能順風順水地寫出一次性編譯經過的代碼——固然我相信對語言的熟悉程度最終會致使這一結果。python

廢話很少說了,聊一下碰到的一個問題和學習到的東西。以前大部分編譯錯誤都能磕磕絆絆修過去學明白,直到碰到了這個問題(片斷):git

pub fn parse_uri(uri: &str) -> Result<(&str, &str), int> {
    match uri.find_str("://") {
        Some(pos) => {
            let protocol = uri.slice_to(pos);
            let address = uri.slice_from(pos + 3);
            if protocol.len() == 0 || address.len() == 0 {
                Err(consts::EINVAL)
            } else {
                Ok((protocol, address))
            }
        },
        None => Err(consts::EINVAL),
    }
}

這一段其實很簡單,就是 ZMQ 裏面解析一個 endpoint 的代碼。好比給一個 endpoint tcp://127.0.0.1:8890 做爲參數,正確解析的話就應該返回兩個字符串:tcp127.0.0.1:8890。不得不用 Python 寫一下示意:github

def parse_uri(uri):
    return uri.split('://', 1)

回到 Rust。我指望作到的就是在不拷貝字符串的狀況下,經過 borrowed pointer aka reference 來實現字符串的解析切分,即借進去一個字符串的引用,返回來該字符串上不一樣位置的兩個不一樣的引用。理想和現實的差距見下:編程

...rs:19:28: 19:31 error: cannot infer an appropriate lifetime for region in type/impl due to conflicting requirements
...rs:19             let protocol = uri.slice_to(pos);
                                             ^~~

貌似意思是說,在這個函數裏面,Rust 編譯器不能確保返回回去的引用到時候還能用。segmentfault

這下麻煩了。安全

怎麼搞呢?仍是得看文檔。書上說,這種狀況下應該給生命期起個名字。數據結構

原來,Rust 對於每個引用的生命期都是嚴格控制的,在編譯期就保證了引用的陽壽不會長過它引用的對象,從而確保了「野指針」的不可發生性。在剛纔的代碼中,對於新建立的兩個引用 protocoladdress,編譯器沒有拿到足夠的信息以說明它們倆不會變成「野指針」,故而出於安全報了錯誤。修好這個錯誤異常簡單:app

- pub fn parse_uri(uri: &str) -> Result<(&str, &str), int> {
+ pub fn parse_uri<'r>(uri: &'r str) -> Result<(&'r str, &'r str), int> {

這個長的像泛型的東西就叫作命名生命期,就是一個單引號 ' 加隨便一個名字。尖括號裏的能夠認爲是聲明,就是說當前這個函數將會涉及到一個叫作 r 的生命期;參數裏面出現的算是定義,好比說這個例子裏面,uri: &'r str 的意思就是說,uri 是一個字符串引用,生命期 r 就恰好跟 uri 的同樣;最後返回值類型裏面就是使用這個生命期的地方,意思就是說,這個引用和這個引用的聲明期跟 r 一致。這樣一來,咱們就把編譯器缺失的信息補上了。tcp

值得注意的是,並非全部返回的引用都能像這樣套用生命期定義,只有從參數裏的引用派生出來的引用才能夠——好比說這樣是不行的:

fn test_ref<'r>(input: &'r str) -> &'r int {
    let num = ~123;
    &*num
}

編譯結果:

t.rs:3:5: 3:10 error: borrowed value does not live long enough
t.rs:3     &*num
           ^~~~~
t.rs:1:44: 4:2 note: reference must be valid for the lifetime &'r  as defined on the block at 1:43...
t.rs:1 fn test_ref<'r>(input: &'r str) -> &'r int {
t.rs:2     let num = ~123;
t.rs:3     &*num
t.rs:4 }
t.rs:1:44: 4:2 note: ...but borrowed value is only valid for the block at 1:43
t.rs:1 fn test_ref<'r>(input: &'r str) -> &'r int {
t.rs:2     let num = ~123;
t.rs:3     &*num
t.rs:4 }

關於參數傳遞的更多

上次說到不用 box 或是引用的參數傳遞是內存拷貝,這確實是這樣,但若是數據量比較小,官方仍是建議直接作內存拷貝,而不是使用引用或 box ——爲了細微的性能提高而增長代碼的複雜性是不值得的;除非必要,若是怕影響性能,能夠先作個性能測試。

相反,對於返回值,相似的「值傳遞」卻能在強大的編譯器的幫助下「變成」零拷貝。改自官方例子:

fn foo(x: ~int) -> int {
    return *x;
}

fn main() {
    let x = ~5;
    let y = ~foo(x);
}

main() 先建立一個 owned box,放進去一個 5,而後把這個 box 扔進 foo() 裏,foo() 把 box 打開扔掉,把內容 5 返回,最後在 main() 裏再給 5 套一個盒子 y

問:這個過程當中,5 一共被拷貝了幾回?

兩次?foo() 裏拆包一次,返回以後套盒子又一次?

只有一次!原來,在 foo() 返回以前,y 的盒子空間就已經準備好了,foo() 作的其實只是把 5 從一個盒子拷貝到另外一個盒子。聽說這都是編譯器乾的好事。

因此呢,官方文檔建議你們,在返回數據結構的時候,大膽返回整個值就行了,不用考慮返回前先放到盒子裏(用 ~),讓調用的地方靈活地來選擇是應該放盒子仍是怎麼樣。

可愛的 result

想必你們都知道,Rust 沒有提供空指針,也沒有提供異常處理。那那那,這代碼還怎麼寫啊,人家明天還要上班呢!

其實前面已經有一個例子了,就是 parse_uri——它會把一個 ZMQ 的地址拆成兩部分,不然報錯。這裏咱們看看怎麼調用它:(摘自剛寫的 zmq.rs 的 bind

fn bind(&self, addr: &str) -> Result<(), int> {
        parse_uri(addr).and_then(|(protocol, address)| {
            match protocol {
                "tcp" => Ok(()),
                _ => Err(consts::EINVAL),
            }
        })
    }

哈哈,其實很簡單明瞭嘛。and_then 這裏起到了承上啓下的做用,意思是「先解析 URI,而後返回(執行)下面的;除非解析 URI 失敗了,跳過下面直接返回錯誤」,大大地體現了函數式編程的特色有沒有。這個 and_then 其實就是日常有異常的語言中的順序執行嘛,不去抓異常,依次執行,只要出錯就跳出,沒錯就執行到底。

簡單總結一下哈。

  • Result 枚舉有兩種值,OkErr,適合於可能出錯的函數來返回。

  • Option 枚舉也有兩種值,SomeNone,適合於意義上須要返回「虛無」的函數。

  • Condition 好像給刪掉了?

它們都有一堆很方便的小函數能夠用,請參考上述文檔連接。

相關文章
相關標籤/搜索