A First Look at Rust Language

文 Akisann@CNblogs / zhaihj@Github
本篇文章同時發佈在Github上:http://zhaihj.github.io/a-first-look-at-rust.htmlhtml

過去的一年半多,我一直沉迷與OOC,緣由卻是很簡單,OOC是目前爲止我所能見到的最容易理解和最容易書寫的語言。而且另一個極其重要的地方是,它能夠編譯成C代碼。編譯成C代碼,也就意味着優化能夠交給高度發展的C語言編譯器來作,聽起來彷佛適合十分高效的方法。git

最近幾年相似的語言愈來愈多,從好久好久以前就存在卻一直沒出名的Haxe,還有最近的Nim-lang,以及採用了相似ruby語法的Crystal,甚至包括編譯成C++的felix。這些語言都號稱本身考慮了速度(運行速度),至少從編譯成C/C++的層面上。程序員

惋惜的是,在改進OOC編譯器rock的過程當中,我遇到了愈來愈多的問題,這些問題讓喜歡速度的人泄氣。一個最明顯的事情是,這些語言幾乎都用了GC,不管是libGC仍是本身寫的,而且更重要的是,不少語言特性是基於GC設計的——好比閉包,好比iterator的unwrap,在有沒GC的狀況下,這些東西的設計要複雜的多。在OOC裏,因爲Generics不是Template,更多的東西開始依存GC,在用了它一年後,當我真正開始在工做裏使用的時候,這些問題開始出現,我開始打算關閉GC,但很顯然這是不可能的。編譯器會把一切搞不清楚的事情踢給GC。github

在這個時候,剛好Rust站了出來,靜態析構,沒有野指針…… 簡直就是爲有着Compile to C語言苦惱的人設計的。因而我打算在這篇文章裏瞄一眼Rust,來看看它是否是我想找的東西。ruby

首先從官方的例子開始,打開Rust的主頁就會看到。直接拷貝過來,就是這個樣子:多線程

fn main() {
    let program = "+ + * - /";
    let mut accumulator = 0;

    for token in program.chars() {
        match token {
            '+' => accumulator += 1,
            '-' => accumulator -= 1,
            '*' => accumulator *= 2,
            '/' => accumulator /= 2,
            _ => { /* ignore everything else */ }
        }
    }

    println!("The program \"{}\" calculates the value {}",
              program, accumulator);
}

看起來跟現代語言並無太大差異,至少這個例子還算比較容易閱讀,讓咱們來把這段代碼改爲相似函數式的寫法:閉包

fn main() {
    let program = "+ + * - /";
    
    let res = program.chars().fold(0, | x, x1 | 
        match x1 {
            '+' => x + 1,
            '-' => x - 1,
            '*' => x * 2,
            '/' => x / 2,
            _ => x
        }
    );

    println!("The program \"{}\" calculates the value {}",
              program, res);
}

這段代碼對OOC的用戶來講至關親切,它們實在有些類似,好比相同的lambda語法 | arguments | program ,幾乎相同的match語法match expr { case => expr }函數

不過若是僅僅是這樣,恐怕Rust不會這麼吸引人,下面讓咱們來看一個稍微複雜點的例子。優化

這個例子來自Computer Language Benchmark Game的Binary Tree,這也是我最喜歡的一個例子,幾乎在瞭解任何語言時我寫的第一個小代碼都是Binary Tree。它包含了一些基本的東西——構造體(或類),遞歸,循環。先來看看我寫的Binary Tree,後面會有詳細的解說。spa

use std::env;

struct Tree {
    left: Option<Box<Tree>>,
    right: Option<Box<Tree>>,
    item: i32,
}

impl Tree {
    pub fn new(depth: i32, i: i32) -> Tree {
        if depth <= 0 {
            Tree { item : i, left: None, right: None }
        } else {
            Tree { item : i, 
                left: Some(Box::new(Tree::new(depth - 1, 2 * i - 1))),
                right: Some(Box::new(Tree::new(depth - 1, 2 * i ))),
            }
        }
    }

    pub fn item_check(&self) -> i32 {
        self.item + 
            self.left.as_ref().map(| t | t.item_check()).unwrap_or(0) -
            self.right.as_ref().map(| t | t.item_check()).unwrap_or(0)
    }
}

const MINDEP : i32 = 4;

fn main() {
    let depth = env::args().nth(1).unwrap_or("10".to_string()).parse::<i32>().unwrap_or(10);
    println!("Running program with depth = {}", depth);
    let stretch = depth + 1;
    println!("stretch tree of depth {}\t check: {}", stretch, Tree::new(stretch, 0).item_check());
    let long_lived = Tree::new(depth, 0);
    let res = (MINDEP .. depth + 1).filter(| x | x % 2 == 0).map( | x | 
                    (1 << (depth - x + MINDEP + 1), x, (1 .. (1 << (depth - x + MINDEP)) + 1).fold(0, 
                        | xt , x1 | xt + Tree::new(x, x1).item_check() + Tree::new(x, -x1).item_check())));
    for (iters, i, check) in res { 
        println!("{}\t trees of depth {}\t check: {}", iters, i, check); 
    }

    println!("long lived tree of depth {}\t check: {}", depth, long_lived.item_check());
}

這段程序很短,算上空行也不過總共44行,讓咱們來看看每一部分都有什麼有趣的地方。

struct Tree {
    left: Option<Box<Tree>>,
    right: Option<Box<Tree>>,
    item: i32,
}

這是一個很容易理解的structure定義,讓人高興的是新語言愈來愈多的使用pascal式的variable : type而不是難於理解的type variable。下面就是具備Rust特色的東西了,跟C,D等語言不一樣,遞歸定義時並無用相似left: &Tree的形式,緣由很簡單——left和right有多是空的,而rust不容許這種空指針。爲了解決這個問題,Rust提供了一個叫作Option的特殊類型(enum),若是沒有內容,那麼OptionNone,不然就是Some(T),這樣作的好處是不用再考慮nil.item_check()這種可能引發Segmental fault的形式了。

接下來看到的是Box,固然Box也不過是儲存Heap上的一個指針而已,在C++等語言裏,Box&Tree彷佛並無太大差異。不過在Rust裏,&Tree並非Tree的指針,而是Tree的Borrow,或許你沒看明白,沒問題,讓咱們動手把Box修改爲&,看看會發生什麼:

struct Tree <'a> {
    left: Option<&'a Tree<'a>>,
    right: Option<&'a Tree<'a> >,
    item: i32,
}

impl <'a> Tree <'a> {
    pub fn new(depth: i32, i: i32) -> Tree<'a> {
        if depth <= 0 {
            Tree { item : i, left: None, right: None }
        } else {
            Tree { item : i, 
                left: Some(&Tree::new(depth - 1, 2 * i - 1)),
                right: Some(&Tree::new(depth - 1, 2 * i )),
            }
        }
    }

……………… 下略 ………………

修改完以後程序有了很大的變化,除了把Box改爲&以外,咱們還添加了lifetime標識。若是以前或多或少知道rust,那麼確定知道rust是如何管理內存的——每個變量都有一個生命期,超過生命期以後這個變量就會被銷燬。所以,對於struct裏的變量這種沒法推斷生命期的東西,須要在代碼裏指明這些變量到底能存在多長時間。不過很惋惜,縱使修改爲這個樣,這段代碼依然沒法編譯經過——會出現下面的錯誤:

15:29: 15:60 error: borrowed value does not live long enough
15                 left: Some(&Tree::new(depth - 1, 2 * i - 1)),
                                      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
10:48: 19:6 note: reference must be valid for the lifetime 'a as defined on the block at 10:47...

簡單來講,在new函數裏面,咱們定義的全部變量在new函數結束時就所有被析構了,所以咱們無法在其餘地方用它。爲了解決這個問題,咱們須要把Tree分配在Heap上,而且保證它能活得夠長。(固然,這並不表明這麼作是不可能的,但在這裏咱們不討論)

這個話題一旦展開就不得不附帶上冗長的解釋,畢竟lifetime是rust裏最獨特的東西,若是想更詳細理解lifetime,rust的官方文檔是一個好地方。總之,但願你能經過這個不夠詳細的解釋理解&Box的區別。

在說明了lifetime這個概念以後,下面的事情就變得簡單多了, pub fn new(depth: i32, i: i32) -> Tree是一個"構造函數",構造函數有引號是由於rust裏並無語言層級的構造函數,new僅僅是一個約定而已。函數的定義跟Ada有些相似,相信全部人第一眼都能看明白這個函數的意義。

下面讓咱們來看main函數,除去大量的println,重要的代碼只有一句:

let res = (MINDEP .. depth + 1).filter(| x | x % 2 == 0).map( | x | 
                    (1 << (depth - x + MINDEP + 1), x, (1 .. (1 << (depth - x + MINDEP)) + 1).fold(0, 
                        | xt , x1 | xt + Tree::new(x, x1).item_check() + Tree::new(x, -x1).item_check())));

這一句稍微有些函數式的感受,簡單解釋,咱們找出MINDEPdepth + 1之間的全部偶數,對於每個偶數,求從1到(1 << (depth - x + MINDEP)) + 1循環,並求對應Tree::item_check的和。相信熟悉函數式的人可以很快搞明白每一句的意思:map把沒一個偶數變成一個Tuple,而fold則對區間求和。若是用更普通一點的寫法,那麼是這樣:

let mut i = MINDEP;
    while i <= depth {
        let iterations = 1 << (depth - i + MINDEP);
        let mut check : i32 = 0;
        for j in 1 .. iterations+1 {
            check += Tree::new(i, j).itemCheck();
            check += Tree::new(i, -j).itemCheck();
        }
        println!("{}\ttrees of depth {}\t check: {}", iterations * 2, i, check);
        i += 2;
    }

能夠看到,三行代碼能夠展開成12行。就如同函數式宗教的信者們所一直在宣講的同樣,相比與循環,map和fold可能更加簡潔直觀。

不過這些並非重點,重點是咱們看到這些代碼裏壓根沒有出現free()這種東西,徹底就如同任何一個有GC的語言,定義,而後使用,沒必要擔憂哪些東西會吃掉內存。更重要的是Rust壓根沒有使用GC——也就是說不會有什麼東西會忽然停掉你的程序而後掃描內存,也不會有gc_malloc這種函數會在你使用的時候花費半個小時去掃描並釋放空間,全部的析構都是靜態的,也就是至關與自動在C代碼裏插入了free語句。

這種作法的好處顯而易見,不會有什麼不肯定的東西影響程序的運行,也不會有沒法釋放的內存。讓咱們繼續修改下這個程序,讓它變成多線程:

use std::{env, thread};

………… 中略 …………

    let res = (MINDEP .. depth + 1).filter(| x | x % 2 == 0).map( | x | 
                    (1 << (depth - x + MINDEP + 1), x, thread::spawn(move || (1 .. (1 << (depth - x + MINDEP)) + 1).fold(0, 
                        | xt , x1 | xt + Tree::new(x, x1).item_check() + Tree::new(x, -x1).item_check())))).collect::<Vec<_>>();
    for (iters, i, check) in res { 
        println!("{}\t trees of depth {}\t check: {}", iters, i, check.join().ok().unwrap_or(0)); 
    }

………… 後略 …………

能夠看到修改的地方不多,僅僅是在原先的1 .. (1 << (depth - x + MINDEP)) + 1循環外面套了一個thread::new而已,這也是函數式的另外一個好處,相比與用for循環來講,實現多線程很是簡單。同時,若是有一個C++程序員,那麼他頗有可能會對thread::new裏面的代碼表示擔憂,好比會不會有data race。回到Rust上,Rust有一個owner的概念,也就是說任何一個變量都有一個"全部者",而且只能有一個,雖然前面提到了Rust擁有borrow這個概念,但編譯器會限制同一時間只能有一個可修改內容的borrow。那麼很顯然,只要編譯事後沒有錯誤,那麼這個程序就不會出現問題——固然你可能須要面對不少的編譯錯誤。這一般是一個trade-off,不過對於多線程程序來講,很明顯面對編譯器的錯誤信息要簡單的多。

固然,也能夠用channel來傳遞消息:

use std::env;
use std::thread;
use std::sync::mpsc;

………… 中略 …………
    let long_lived = Tree::new(depth, 0);
    let (tx, rx) = mpsc::channel::<(i32, i32, i32)>();
    let res = (MINDEP .. depth + 1).filter(| x | x % 2 == 0).map( | x | {
        let tx = tx.clone();
        thread::spawn(move | | tx.send((1 << (depth - x + MINDEP + 1), x, (1 .. (1 << (depth - x + MINDEP)) + 1).fold(0, 
                        | xt , x1 | xt + Tree::new(x, x1).item_check() + Tree::new(x, -x1).item_check()))))}).collect::<Vec<_>>();
    for _ in res {
        let (iters, i, check) = rx.recv().unwrap();
        println!("{}\t trees of depth {}\t check: {}", iters, i, check); 
    }

………… 下略 …………

惟一須要注意的地方是因爲owner的限制,tx(sender)對於每一個線程都與要一個克隆。

好了,到這裏,這篇文章也算多少介紹了Rust的主要特性,是時候來回頭看看它到底怎麼樣了。對我來講,Rust有一個最大的特徵——安心。只要沒有編譯錯誤或者fn main() { main() }這種代碼,就能夠放心的認爲本身的程序是正確的,在寫了幾天Rust以後,能夠明顯感受到本身考慮的事情變少了,只要按照本身的想法寫出來,剩下的不足所有都由編譯器來指出。有一個提高生活質量的設計,我還能要求什麼呢?

以上例子的代碼能夠在Github下載。

相關文章
相關標籤/搜索