文 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),若是沒有內容,那麼Option
是None
,不然就是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())));
這一句稍微有些函數式的感受,簡單解釋,咱們找出MINDEP
到depth + 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下載。