我爲什麼 Rust ?

Rust icon

過去我比較常接觸的程式語言大概就是 C 跟 C++,許多設計的想法也都是從這兩者來的。不過越是瞭解C++ 跟 C,就發現很多很惱人的問題。像是 effective C++ 就花了好幾個章節討論 如何正確的在constructor 中處理例外,討論哪些 operator 應該要限制呼叫,那些要給於實作。

說真的,這些原理我懂,但是整個過程就是很麻煩,更不要說這件事必須整個團隊一同維護才能長久維持。一個本來不會丟例外的函式現在會丟了,C++ 也沒有能力在編譯時期偵測 (除非本來有 noexcept 標記),所有行爲仰賴語言以外的文件,只要一環出錯就全部爆炸。但無奈之前也沒找到什麼比較適合的替代品,就這麼用了好一陣子。

初見

第一次聽到 Rust 這個程式語言也忘記是在什麼時候了,只記得當時看了一下官網寫的那幾條特點:

  • 零消耗抽象 (zero cost abstraction)
  • move 語法
  • 記憶體安全保證
  • 有夠快

喔,很好。看完就關掉了

下一次看記得是在 1.0 發佈的時候,那時候就比較深入,有翻了一下官方的 book 作了一些練習,不過做到哲學家這學家問題就真的有點乏味,然後又回頭了。(後來新版的 book 就沒有哲學家問題了,大概有不少人抱怨吧。回頭想想倒是能理解放哲學家問題的目的,畢竟它很好的展現了 Rust 處理資料跟線程的思維)

真正接觸就是後來硬着頭皮嗑完哲學家問題,把整本書看完了。

特點

我自己最喜歡 Rust 的幾點大概是:

  • 很 C 的語法
  • 現代語法
  • 型別判定
  • 顯性操作
  • 沒有例外處理
  • 內建 tagged enum 型別
  • 使用 trait
  • 強大的 pattern matching
  • 所有權和生命週期 (ownership & lifetime)
  • 活躍且開放的社群

下面就花些篇幅介紹一下吧 :)

很 C 的語法

先來段 Rust 程式碼

enum TicketType {
    NORMAL,
    VIP,
    UNKNOWN,
}

struct Ticket {
    ticket_type: TicketType,
}

impl Ticket {
    fn new(ticket_type: TicketType) -> Ticket {
        Ticket { ticket_type: ticket_type }
    }
}

fn get_reward(ticket: Ticket) -> Option<String> {
    match ticket.ticket_type {
        TicketType::NORMAL => Some(String::from("a small box")),
        TicketType::VIP => Some(String::from("a golden box")),
        TicketType::UNKNOWN => None,
    }
}

fn main() {
    let ticket = Ticket::new(TicketType::VIP);
    let gift = get_reward(ticket);

    if gift.is_some() {
        println!("you got: {}", gift.unwrap());
    } else {
        println!("yout got nothing...");
    }
}

(這裏有線上編譯器可以執行,有興趣可以玩玩看)

看看語法,其實跟 C 相去不遠。函式定義就是每個參數的名字跟型別,函式名稱跟回傳值。match 語法看上去也跟 switch 有些相似,大致上來說,就是不會有很明顯鎮痛。(在說你,Haskell)

現代語法

雖然說這點可能見仁見智,不過跟 C++ 相比,我是更偏好 Rust 這類偏現代風格的語法 API 設計 (我就不多說什麼了)

型別判定

Rust 編譯器有相當強大 (或者說方便) 的型別推斷能力,C++ 的 auto 關鍵字僅能推斷賦值右側的型別,但是 Rust 甚至能夠推斷 generic 型別內的型別,用 C++ 來說就是 vector 裏的 int 是可以被推斷出來而不需要明說的

下面是一段使用 Rust 標準函式庫內 hash map 的範例。這裏我們使用 i32 當作 key,String 當作 value。由於 Rust 型別推斷的能力,我們在建立 HashMap 時可以省略 key/value 的型別,因爲在執行 insert 時就會用到 key/value,Rust 編譯器便能夠從此推斷 HashMap 的內容物型別。

use std::collections::HashMap;

fn main() {
        // 等同於 HashMap::<i32, String>::new()

        let mut map = HashMap::new();

        // 我們呼叫 insert(i32, String), 所以可以推導 map 屬於 HashMap<i32, String> 型別

        map.insert(0, String::from("zero"));
        map.insert(1, String::from("one"));

顯性操作

Rust 跟 C++ 不同,你沒說的事情 Rust 就不會做。物件不會因爲參數符合就被建造,物件不會沒事被複製一份,型別不會因爲可以就被偷偷轉換。你說什麼,Rust 才做什麼。例如在 Rust 中,甚至連不同大小的整數型別也需要明說才會進行轉換

let a: i32 = 10;
let b: i8: 1;
let c: u64 = (a as u64) + (b as u64);

另外在運算子多載的實現方式,Rust 也是維持 "有說才做" 的原則。對於使用者自創的物件,如果沒有對運算子進行實作,那麼該物件就不用有該運算子的實作。這點跟 C++ 有很大的不同,C++ 的理念是我他媽就是要幫你做個預設實作,你不爽再複寫。

沒有例外處理

Rust 並不使用拋出例外的方式來處理錯誤,而是用 C 傳統的回傳值判斷。到這裏可能有人就會發問,例外處理的出現,不就是爲了要強迫程式撰寫者處理錯誤嗎,Rust 這樣豈不是走了回頭路?

這就是我喜歡 Rust 的特點了。Rust 中有兩種常見的資料結構:Option 跟 Result,簡單來說,當程式撰寫者呼叫的函式可能會失敗導致沒東西回來時,C 的慣用方法是回傳 null pointer,C++ 則可能拋出例外要求處理,在 Rust 則會回傳 Option<T> 或是 Result<T, E>

Option<T> 表示回傳的類別 T 可能不存在,在把東西拿出來前必須先確定,否則沒東西卻強制拿取,程式會直接離開。

Result<T, E> 也是類似 Option<T> 的功能,只是在物件 T 不存在時,多存了一份物件 E 當作錯誤的參考,好提供更多 T 沒有被建造/傳回的資訊。

藉由 Option 跟 Result,Rust 便用回傳值達到了 逼迫使用者處理錯誤 這件事。使用者不檢查就拿不到物件,明明沒有東西在裏面又硬拿出來程式就中斷。

(當然,這樣的設計依然有缺點,錯誤的傳遞容易變得冗長)

內建 tagged enum 型別

Rust 的 enum 型別可以這樣寫:

enum Event {
        MouseEvent(i32, i32, MouseButton),
        KeyEvent(Key),
        JoystickEvent(JoyButton),

這裏的程式碼是這樣的:Event 是一種 enum 型別,含有欄位:

   - MouseEvent
   - KeyEvent
   - JoystickEvent

然後每個欄位又還有各自所需的額外資訊,例如滑鼠事件能個存放點擊的 x 跟 y 軸座標以及滑鼠按鈕。這項特點配上 pattern matching 後除了能夠讓程式碼撰寫跟閱讀相當容易以外,也能防止錯誤的發生。

這樣的技巧並非 Rust 獨創,在 C 當中可以使用 enum 跟 union 來達成這件事情,大概是這樣:

struct Event {
        enum EventType type;

        union {
                MouseEvent mouseEvent;
                KeyEvent keyEvent;
                JoystickEvent joystickEvent;
        };
}

但是 C 的實作並無法保證執行時期的正確存取,即使 type 告知是 MouseEvent,不小心仍然可以把union 的資料當作 KeyEvent 讀出來用 (當然,那會是一團垃圾)。

使用 trait

trait 的概念約略等同於 Java 中的 interface。當物件實作某個 trait 時,我們就能在該物件身上呼叫該 trait 擁有的 method。

例如運算子的多載,在 Rust 中便是替該物件實作對應的 trait。如果你想要讓你的物件能夠使用 +運算子,那就幫他實作吧!

use std::ops::Add;

struct Foo {}

impl Add for Foo {
    type Output = Self;
    fn add(self, rhs: Self) -> Self::Output {
        Foo {}
    }
}

除了實作 method 外,trait 也能當作該物件 是否擁有某種特性 的標記。像是,某某物件是否能夠安全的在 thread 間交換,或是某某物件是否能被合法拷貝。

trait 更強大的部分在於組合能力,使用者建立的 generic 物件或是函式,可以針對物件的 trait 進行限制。程式撰寫者可以很直覺的作出 參數要是有實作 Add 而且能被拷貝的型別 或是 容器存放的物件都要能在 Thread 間中安全傳遞 這類的東西。

強大的 pattern matching

受夠了 string 型別不能放在 switch 裏面的日子了嗎?

繼續使用剛剛事件的例子,在 Rust 中你可以寫出這樣的程式碼:

    match event {
            MouseEvent(666, 666, button) => println!("hell"),
            MouseEvent(x, y, button) => println!("world"),
            KeyEvent(Key::X) => println!("x pressed"),
            KeyEvent(_) => println!("key pressed),
            _ => println!("I don't care anymore"),
    }

在 Rust 中,match 語句除了可以篩選 enum 的類型之外,還可以針對 enum 欄位的數值做更進一步的過濾。例如上面的例子,僅有當滑鼠點擊到 666/666 時,才會觸發特定事件,其餘則一律輸出 "world" 字串。

match 第一眼看上去會讓人覺得是個長得不一樣的 switch,實際使用跟習慣後才會發現 match 語句帶來的好處不僅僅是你終於可以比對 String 了,而是我們可以用同一種語句,針對更多的特殊案例做處理,而不需要在 switch-case 內繼續加入好幾層的 if-else 或是 switch-case。

所有權和生命週期 (ownership & lifetime)

先從所有權來談好了,試想下面的 C 程式碼:

SomeObj* someOperation()

嗯,所以我要不要 free 掉回傳回來的指標?這就是所有權的問題。在 C++ 11 smart pointer 納入標準後其實這樣的問題已經好很多了,但是還是有某些語句難以用 C++ 語法來正確描述,例如一顆被引爆的炸彈,在 C++ 中這個炸彈物件即便被呼叫了爆炸函式,接下來的程式中這顆炸彈依然是有效可被呼叫的物件,而 Rust 中允許某 method 吞噬 物件本身,讓炸彈爆炸後,物件變成不可被呼叫的狀態:

struct Bomb;
impl Bomb {
...
    // 傳入 self 而不是 &self

    fn ignite(self) {
        ...
    }
...
}

生命週期就更好理解了,請看下面的 C++ 程式碼:

Foo* fooPtr = nullptr;
{
    Foo f;
    fooPtr = &f;
}

這個例子是有點故意,但是實際上類似的問題確實很惱人。Rust 在處理生命問題上使用了兩種解法,一種是編譯時期檢查,一種是執行時期檢查。細節我就留給各位看官自行查閱吧。

活躍且開放的社群

這是跟語言本身無關的點,但也是相當吸引人的一點。

其實我也沒深入參與多少語言的社群,所以說不定等等提到的點其他語言也有。

Rust 是由 Mozilla 主導,社群推動的計劃,而 Mozilla 幾乎是由社群建立起來的組織,說真的Rust 能夠培育出如此活躍而且開放的氣氛是滿不意外的。Rust 不僅僅是活躍,他們甚至歡迎新朋友加入,舉前陣子 Rust 編譯器更新報錯格式的例子吧,他們把每一條編譯器錯誤的格式更新都開了一條issue,並在 Rust 週報跟 reddit 上邀請有興趣的開發者幫忙更新。說真的,這件事對開發編譯器來說根本是吃力不討好,更新完成的事件被拉長,需要耗費人力指導,但是這項活動卻展現了 Rust 社群的不同:他們歡迎大家參與!

如果你對語言有問題上 IRC 求救,你不用擔心得到 RTFM 這種帶有排他性質的回應,反而,你會得到來自兩三個人的解釋,指引。必須說這樣的環境是不容易營造的,而 Rust 就是這樣的好所在。

結論

其實還有滿多東西可以談的啦,像是 Rust 的官方套件管理工具 cargo,iterator,文件工具,測試之類的,不過我想這篇就到這裏吧。如果你看了上面的介紹有這麼一點,就算一點點也好,被推坑的感覺,趕快去官網下載來玩玩。(趁你還記得我說了啥之前)

學習 Rust 是一個相當有趣的過程,過去我在學習 C/C++ 以外的程式語言時都會陷入一個狀態:

"我好像只是在背另一種版本的語法而已"

而 Rust 打破了這個規則,Rust 有很多過去沒有用過的概念,學起來相當有趣新奇。而實際編寫程式後也能發現那些新想法帶來的好處。我自己很看好 Rust 的發展,因爲 Rust 企圖解決過去撰寫系統程式(雖然說,Rust 是相當通用的語言) 遇到的安全問題,而不是 另一種口味 的 C/C++ 替代品。像是提供了多線程程式的保護,參考生命週期的嚴格控管,很多在 C/C++ 要靠程式設計師腦袋注意的東西現在都搬到了編譯時期做保證,沒有包 Mutex 的東西就是不行在 thread 間共有,物件實體比參照早
死你就別想把參照存下來。

說了這麼多,總言之,這裏請

Comments

comments powered by Disqus