rust为什么显示不了国服_Rust编程语言初探
靜態、強類型而又不帶垃圾收集的編程語言領域內,很久沒有新加入者參與競爭了,大概大部分開發者認為傳統的C/C++的思路已經不太適合新時代的編程需求,即便有Ken Tompson這樣的大神參與設計的golang也采用了GC的思路來設計其新一代的語言;一方面垃圾收集技術和即使編譯技術一直在發展和完善,另一方面是大量的未經過嚴格計算機科學基礎訓練的開發人員進入市場,似乎讓開發者永遠停留在邏輯層面而不是去直接操縱內存是個更為現代的選擇,Mozilla卻仍然堅信一門靜態而又高效低利用系統資源的“偏底層”的語言也依然會有巨大的生命力;于是站在現代成熟的軟件工程實踐上的Rustlang(以下簡稱Rust)為創造出來,其新版本的發布不時引起HackNews等極客圈的關注。
本文試圖通過其官方文檔對該語言(以及其相關的生態系統)做簡單的研習。
核心語言特性設計目標
按照其官方描述,Rust需要滿足其以下幾個核心目標
根據以上目標可以相對容易的理解一些核心的語言設計策略背后的決策依據。
基本語法特性
作為一門面向系統編程的偏底層的程序語言,其基本語法和傳統的C/C++/Java系列語言共享了很多共同之處,這里僅需要看看其不同之處。
類型系統
靜態語言的基本元素之一是變量和類型;不同的語言會選擇不同的類型定義和內置的開箱可用的基本類型;這些類型及內置類的設計往往反映了編程語言設計者的決策策略和權衡要素。
類型聲明和自動推斷
畢竟要面對的是偏嚴肅的系統編程領域,選擇靜態類型可以在編譯階段盡可能早地發現更多的程序錯誤是題中之義;同時作為一門比較現代的編程語言,每次讓程序員自己輸入每個變量的類型這類臃腫的做法也被廢棄,自動類型推斷必不可少 - 當編譯器可以”聰明地”推導出合適的類型的時候,變量類型指定可以忽略。
譬如需要聲明一個某種類型的變量,Rust用let x: someType = 來表示;當然對于編譯器可以推導出來類型的情況下,類型是可以省略的這樣可以少寫一些啰嗦的代碼,let x = 2就會定義一個整數類型的變量x;比較新的編程語言基本都是這么做的,沒有什么新意。
作為一門強類型的語言,任何變量或者表達式必須有唯一的類型,否則編譯的時候就會報錯。當然Rust支持一種特殊的變量隱藏機制(Shadow),即同一個名字的變量可以重新使用,并設置為一個完全不同的類型;這個時候原來的變量就不能被訪問了。如
let var = "something"; //string literallet var = 1; //changed to int這種機制從某種程度上來說,反而會使代碼變得不太容易理解,如果程序員習慣了C/C++的編程方式的話;同時也會給IDE等工具的解析帶來一些挑戰;當然這個是仁者見仁智者見智的事情。
類型可變性約束
Rust要求所有定義的變量必須指定是否是可變的;并且作為變量的基本特征強制程序員做合理的選擇。可變的變量用類似let mut varName:Type = value的語法來定義,顧名思義可以在聲明之后被重新賦值修改;而不可變的變量少了一個mut關鍵字,其定義的變量在初始化一次后續就不能再修改了。
Rust里邊同時支持常量類型,用const來聲明,像是從C++里借鑒來的。它和可變類型mutable有一些細微的不同: 對于常量類型我們必須使用類型注解,不能聲明可變的常量類型(不允許混合const和mut),而且常量類型只能被賦值為一個常量表達式, 不能用函數調用的結果或者是其他一些運行時計算出來的值來初始化。綜合來看,Rust的常量類型和C++11中新引入的constexpr行為比較接近。
內置類型
內置類型一般用于提供大部分程序員都要用到的基本數據結構。除了一些其他語言都常見的基本類型(Rust稱之為標量類型),Rust也提供了一些相對比較復雜的類型。
基本標量類型包含以下這些基本的類型
- 整型類型,包括定長的8/16/32/64位的有符號和無符號類型(如u16是無符號16位整型,i32是有符號32位類型), 還支持平臺相關的有符號/無符號類型,分別用isize和usize表示
- 浮點類型,支持單精度(f32)和雙精度(f64)類型,這些在數值計算的時候比較關鍵
- 布爾類型,和C++中的比較類似,有true和false兩種可能的取值,當然沒有C/C++中的那些隱式轉換的麻煩
- 字符類型,支持Unicode
復合類型`
比較新一點的語言都支持一些復雜一點的基本組合類型。
tuple和其它語言的比較類似,用括號語法來聲明,基本用法可以看下邊這個簡單的例子
let tup = (1, 2.2, "something")let (a, b, c) = tuplet secondElem = tup.1第一行代碼聲明一個含有三個不同類型的元素的元組;第二行代碼則將元組中的元素逐一取出,和Python的用法比較類似。除了這種提取方式,元組元素也可以用點語法來訪問元素,如上邊的第三行代碼則用tup.1則取出第二個元素;比C++11的模板元語法簡單多了。
數組則用于表示具有相同類型的元素的集合,如let arr = [1, 2, 3, 4, 5],如果類型不一致則會有編譯錯誤報出。和C/C++這中的類似,數組元素一般是分配在棧上的,其大小在編譯器應該是預先確定的;如果需要可變長的容器,則需要Vector類型。數組越界的檢查默認也包含在語言中了,如果訪問越界的下標,默認程序就會崩潰;當然Rust的錯誤處理機制也有些特殊,容后探討。
容器類型
Rust支持以下基本的容器類型
- Vector 該類型用于存儲邏輯上的列表類型,其正式名字是Vec,用Vec::new()創建空的向量,因為其是用泛型實現的,我們必須指定類型注解;即使用 let v: Vec = Vec::new() 來生成一個新的向量v
- String 是作為一個庫提供的而不是基本的語言機制;其實現和C++的比較類似,內部也使用一個Vec來存儲數據的,因此考慮到國際化的原因,其操作可能比其它語言中的要復雜一些;幸運的是,這些細節以及被標準庫所封裝。
- Hashmap 用于表述邏輯上的哈希關聯容器;其提供的API和C++/Java的比較類似,功能上比C++的復雜一些但比Java的更精簡一點
函數
作為基本編程要素的函數在Rust中的定義沒有什么特別特殊的地方,除了其類型聲明是后置風格之外,其返回類型(如果不能被自動推斷)用->來聲明,比如
//一個返回int類型的函數fn thisIsAFunction(parA: i32, parB: string) -> int { //some implementation}函數的實現體本質上是一個block,由一系列的表達式組成(當然表達式也是用分號分隔的),同時它還支持Ruby風格的自動返回最后一個表達式的寫法, 僅僅需要最后一個表達式省略分號即可;比如這個簡單的函數
fn five() -> i32 { 5}懶惰是偉大程序員的優良品質嘛。由于我們有內置的tuple類型,因此Rust是可以允許有多個返回值的;比較典型的一個場景是用戶錯誤處理的情況,可以返回一個Result,同時攜帶錯誤碼和可能的原因;稍后會仔細看一下異常處理的部分。
函數和宏
Rust本身支持語法層面的宏,并且其標準庫提供了很多各種各樣的宏,譬如最常用的打印函數其實就是一個宏;所有的宏使用!后綴來區分。 println!("The value of x is {}, y is {}", x, y)用于打印出x和y的值;其語法形式非常像一些常見的Java庫所支持的格式,可以用大括號來打印對象。
宏是在編譯的早期階段被展開的,和C中的宏原理類似,雖然Rust的語法看起來更簡潔一些;但是依然有很多新的語法構造,簡單來說可以認為Rust的宏是用macro_rules和模式匹配來實現的。
從可維護的角度來說,應該做好這種因為宏代碼往往意味著更難理解和調試。很多時候,需要將宏作為最后一種不得已而為之的措施。比C中的宏好一點的是,Rust提供了對宏進行調試的方式,可以在其編譯器的命令行中加入--pretty expand選項來查看展開的代碼。
錯誤檢查機制
現實生活中的軟件總是有各種各樣的錯誤需要被正確處理但沒有被及早處理就泄漏到了客戶現場。Rust采用的設計思路是,盡早強迫程序員去顯示處理并以編譯器錯誤的方式提示程序員。
和Java的關于錯誤分類的思路類似,Rust也區分可恢復的錯誤和不可恢復的錯誤,并提供了相應的語言機制上的支持。可恢復的錯誤一般是一些環境的錯誤,譬如文件找不到或者網絡連接失敗等情況,實現上可以用重試等策略來嘗試自動恢復。不可恢復的錯誤往往意味著編程錯誤或低級bug,這種情況下最好的思路是直接讓程序崩潰,并修復代碼。
和Java不同的是,Rust里沒有異常支持!對于可恢復異常,Rust使用Result類型來封裝處理結果,而不可恢復異常則提供panic!未來終止程序繼續執行。
不可恢復異常的支持
遇到不可恢復異常的時候,panic!宏會打印錯誤消息(程序員指定),展開線程棧幀,打印出實際出錯的源代碼位置。如果需要打印backtrace信息,則可以在程序運行前設置環境變量RUST_BACKTRACE。如果忘記設置的話,默認的打印輸出會給出溫馨的提示。
如果不希望展開棧幀而直接暴力終止程序,可以在Cargo.toml中指定
[profile.release]panic='abort'可恢復異常
可恢復異常用一個泛型類Result來傳遞結果,其定義是
enum Result { Ok(T), Err(E)}可以使用枚舉類型的模式匹配(見后述) 來優雅的解決,譬如這個操作文件的例子
use std::fs::File;fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => { panic!("There was a problem opening the file: {:?}", error) }, };}Rust支持一種更簡潔的方法來簡化上述的樣板代碼let f = File::open("hello.txt").unwrap()則返回正常情況下的返回值,如果有異常則直接調用panic!來終止程序。還有一種更”偷懶/簡潔”的做法是,加上額外的描述字符串 - 大部分情況下出錯了我們總想額外打印一些信息,可以用
let f = File::open("hello.text").expect("Unable to open file...")異常的傳遞和擴散
這是一個常見的場景,某個API的使用者不想自己去處理異常場景,僅僅想將其傳遞給自己的調用者去處理,或者程序中有個統一的地方處理異常(通常來說可能不是一個好的主意!)。最基本的思路是,直接將異常返回的類型簽名寫出來,顯示讓調用者處理。
下邊這段代碼實現讀入一個文件,從里邊讀取某個字符串,如果成功則返回該字符串,期間有任何錯誤,則傳遞給調用者。
fn read_username_from_file() -> Result { let f = File::open("hello.txt"); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(e) => Err(e), }}Rust提供了一種更簡潔的方式(慣用法) - 用"?"操作符來傳遞錯誤,類似的代碼可以重寫為
fn read_username_from_file() -> Result { let mut f = File::open("hello.txt")?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s)}需要注意到上邊的代碼使用了block的寫法省略return關鍵字。
如果追求更精簡的代碼,我們甚至可以用一行代碼來完成上述的函數體
let mut s = String::new();File::open("hello.txt")?.read_to_string(&mut s)?;Ok(s)是否有種熟悉的函數式編程的鏈式寫法的味道?
內存訪問模型和并發
作為一門面向系統編程的語言,Rust決定了不使用GC,同時基于工程上的原因,讓工程師自己來管理內存又顯得不符合時代潮流。Rust采用的策略是讓程序員提供一定的指示給編譯器,然后由編譯器來確保內存的分配和訪問總是安全的。
對于Rust程序用而言,理解堆和棧以及對象的生存期/作用域是必須的,雖然編譯器在后臺做了很多工作。為了支持其內存安全和高效約束的目標,Rust提供了一些特殊的語言機制,包括其獨特的對象唯一所有權的概念和引用語法,其智能指針的概念也比較有特色。
從語法的角度來看,Rust取消了->操作符,因此所有的方法調用都是采用obj.doSth()的方式;這點沒什么驚喜,沒有了C的后向兼容負擔,基本上新的語言都是這么干的。在語言層面上,Rust仍然有引用類型的概念;由于要借助編譯器來管理內存,Rust的對象作用域規則有些特殊。
對象的唯一Ownership
默認每個對象都是有唯一的所有權的,這個貫穿在Rust的基本設計規則中
舉個簡單的例子,當我們聲明let s = "hello world"的時候,字面量"hello world"的Owner就是s本身;當s離開作用域的時候,對應的字面量空間就會被釋放。作用域的概念和傳統的C/C++/Java中的很類似,大部分情況下,是通過大括號來限定作用域的。
比較特殊一點的情況和變量的shadow有關,當一個變量通過shadow的方式重新指向另外一個對象的時候,原來的值因為失去了Owner也應該被編譯器悄悄釋放了;當然這里行為仍然是安全的,因為程序沒有通過其它辦法再訪問原來的值。編譯器也可以選擇在真正碰到作用域結束的時候再釋放,然而這些已經屬于編譯器的實現細節了,應用程序無需關心。非常優雅的關注點分離設計!
函數調用中的所有權轉移
和C/C++中不一樣的是,函數調用的時候,參數傳遞會造成所有權轉移即調用者失去了對原來參數的所有權!考慮下邊的例子
fn main() { let s = String::from("hello"); do_something(s); //s失去對字符串的所有權! let x = 5; do_somethingElse(x); //內置類型被拷貝!}fn do_something(par: String) { //par 擁有外部傳入參數的所有權} //作用域結束的時候,par對應的對象會被釋放fn do_somethingElse(par: i32) { // play with par}上述例子中,當調用了do_something(s)之后,雖然s還可以訪問但已經失去了對應對象的所有權,其行為和C++11/14中的Move很像。第二個例子中x對象卻依然可以訪問,這里的不同是,Rust對象對分配在棧上的對象默認采用copy方式處理, 所以僅分配在內存堆上的對象被Move,棧上的對象(編譯器必須知道大小)默認是被復制過去的。
對于分配于堆上的(大小運行期才知道)對象,Rust也提供了clone方法來(其實是泛型的annotation)執行深度拷貝。
函數返回的時候,默認也會轉移所有權,這點和函數調用的參數傳遞情況類似,只不過是傳遞/接收參數的順序反了過來,不再詳述。
引用類型
如果默認的轉移所有權的方式不符合實際的場景,Rust還提供了引用類型來指示傳遞過程中,僅僅保留對原來參數的引用而不轉移所有權;概念上和C的指針很相像,只是有很多額外的措施避免濫用指針可能出現的空指針、懸掛指針等復雜問題。
引用類型在語法上用&符號來表示,可以用于修飾標志符,熟悉C/C++的應該不陌生;唯一有點麻煩的是,調用者和函數聲明都必須顯示聲明引用類型, 如下邊的例子
fn calculate_lenght(s: &String) -> usize { s.len()}let s1 = String::from("hello");let len = calculate_length(&s);println!("The length of '{}' is {}", s1, len);默認的引用類型是只讀的,因為這個對象是借來的,被調用函數沒有所有權;嘗試去修改的話,則會被編譯器報錯攔住。又是一個精妙的設計,多少粗心的錯誤可以被精明的編譯器攔住。
可修改的引用和安全性
如果實在需要在被調用函數中修改傳入的引用參數,那么也是可以聲明類型為 &mut SomeType的,只是出于數據安全性的考慮(避免可能的運行期錯誤), Rust定義了如下規則來保證對象的訪問總是安全的;任何可能引起Race Condition的訪問模式都盡量被編譯器攔截住,這樣成功編譯的代碼,出現運行期錯誤的可能性被大大降低了。
上述最后一條規則其實意味著我們可以有意利用它,通過大括號來創建不同的作用域,寫出更簡潔的代碼,比如
let mut aStr = String::from("hello"){ let r1 = &mut s; //do sth with r1} //r1 離開作用域let r2 = &mut s;//基于r2的修改操作另外一種常見的指針錯誤是”懸掛指針”,在傳統的C++程序中,當一個指針指向一個不存在的對象的時候,緊接著所有對指針的操作會導致未定義的行為;由于實際出現錯誤的地方和真正“制造出懸掛指針”的地方可能相距萬里,這類運行期的錯誤往往會耗費程序員大量寶貴的時間。考慮下邊的例子
fn main() { let reference_to_nothing = dangle();}fn dangle() -> &String { let s = String::from("hello"); &s}如果嘗試編譯上述代碼,rust編譯器會清晰的報告一個對象生存期錯誤
error[E0106]: missing lifetime specifier --> dangle.rs:5:16 | 5 | fn dangle() -> &String { | ^^^^^^^ | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from = help: consider giving it a 'static lifetimeerror: aborting due to previous error對象生存期
在Rust的內部實現中,一個隱含的邏輯是,任何一個引用都關聯著一個對于的生存期,大部分情況下生存期都可以由編譯器自動推導得到而不需要使用者格外留意。當具體的實現中期望引用的生存期可以根據某些條件呈現不同的行為的時候,程序員必須提供一些輔助措施告訴編譯器這些額外的判斷信息。
Rust編譯器內部有一個成為BorrowChecker的工具,它在程序編譯的過程中會檢查是否所有的引用是合法的。當它無法判斷引用的生存期的時候,程序員需要在定義的地方傳入一些類似于檢查點的生存期指示幫助編譯器正常檢查。
考慮一個取2個字符串slice長度最大者并將其返回的一個函數
fn longest(x: &str, y:&str) -> &str { if x.len() > y.len() { x } else { y }}編譯這段程序的時候,編譯器就會報錯說,不知道如何決定返回的引用的生存期,因為它要么是x,要么是y, 卻是由程序的運行期的行為來動態決定的,編譯器沒有辦法在編譯的過程中做決定。修補這個錯誤則需要在函數簽名中加入生存期標記
fn longest(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}這樣編譯器就可以知道其實參數和返回值的生存期是一致的,不會產生意外的非法訪問或者Race Condition。這里采用的語法是泛型的語法,后邊會詳細考察一下Rust的泛型支持。
生存期檢查的概念是Rust獨有的,其采用的類泛型的語法學習起來也顯得不是很清晰易懂;這也許是最迷人也最晦澀的特性,從設計的角度來說, 犧牲一定的簡單性來達到安全編程又不損失性能的目標也許是個不錯的折中; 既想要高層的抽象,又想要極致的性能,還不想有太多意外的錯誤是個刀劍上跳舞的極致挑戰,這方面Rust做的很不錯。
智能指針
默認的引用方式支持生存期檢查和對象借用,實質上采用的仍然是所有者唯一的模型;實際應用場景中,程序員可能需要選擇一個可以被多個所有者共享的對象生存期模型, 一如C++中很常用的基于自動引用計數的shared_ptr的樣子。
Rust通過標準庫的方式提供了額外的對象生存期管理模型,包括
- Box類型用于表示一個指向單個堆上分配的對象的指針,該指針的大小在編譯期間是可知的從而我們可以用它來定義遞歸的數據結構
- Deref Trait用于表示一個允許通過解引用來訪問其封裝的數據的智能指針
- RefCell 用來支持可以修改某個不可變參數內部隱藏的數據的模式;默認情況下,引用規則不允許這樣的操作。這種情況下會產生不安全的代碼,需要程序員做一些額外的處理
- Rc和RefCell用于支持環形引用而不引入內存泄露,這個在GC的算法中很常見
細節不一一展開探討,總體上而言智能指針其實是接管了對象的所有權,并且在智能指針內部做自動的控制;這一思路現代C++的實踐是英雄所見略同。
更簡潔的并發支持
支持安全而又高效的并發編程是Rust另外一個雄心勃勃的目標。同時Rust又力圖做到盡可能的簡潔。從語言實現上來說,Rust采用了和控制內存安全訪問以及對象所有權/生存期以及類型系統完全相同的工具來解決并發的問題, 盡管這些機制看起來和并發安全相差甚遠。
經由大量的類型系統檢查、對象生存期檢查;大量的并發編程問題都可以在編譯器被捕獲,從而編譯通過的代碼往往就意味著沒有并發安全性的問題找上門;程序員可以放心的重構其代碼而不用太擔心重構后的代碼會破壞并發安全性;因此Rust稱之為“無所畏懼的并發”。
Rust的并發編程支持一些流行的并發編程模型
- 基于消息傳遞的CSP模型,這也是Golang所采用的并發方式
- 傳統的基于Mutex和對象的所有權來控制共享數據訪問的方式 - Rust的類型系統和所有權控制使得其中的挑戰降低了不少
從設計上來說,并發支持不是Rust的核心語言的部分,所有的并發機制都是通過標準庫來提供的,這也意味著更多擴展的可能;有新的并發訪問方式,那就寫新的庫唄。
模塊系統
編程范式和高級特性
從編程范式的角度來看,Rust本身其實支持多種編程范式因為其某種程度上對標的是現代的C++或者Golang這樣的競爭對手。
過程式編程
傳統的過程式編程風格和基本的C模型比較接近;其定義結構體的方式和C比較類似,依然是采用struct來組織數據,所不同的是Rust支持“方法”和實現分開定義,通過新的關鍵字impl來添加新的方法實現。
考慮一個簡答的例子,定義個矩形以及對應的area方法來計算其面積
struct Rectangle { length: u32, width: u32,}impl Rectangle { fn area(&self) -> u32 { self.length * self.width }}這里的area方法綁定于該Rectangle上,第一個參數總是&self這樣編譯器可以自動推導出其類型是所綁定的struct對象;因為這里的參數仍然是一個引用, 默認是不能修改結構體的參數,當需要修改時候,可以指定&mut self從而獲取一個可修改的引用。這里引用的生存周期模型仍然是適用的。
調用的時候,只需要構造一個結構然后,采用structObj.callMethod(...)語法即可;大概是出于簡化語言的考慮,Rust只支持簡單的.語法而丟棄了古老的->操作符; ->的使用僅僅限于指定函數的返回類型上,干凈清爽了許多。
let rect = Rectangle { length: 50, width: 30 };println!("The area of rectangle is {}", rect.area())Rust也支持類似C++中的靜態函數的概念,對應的機制Rust稱為關聯函數,這樣的機制對大型代碼的組織是很有意義的,可以方便地解決名字沖突的問題。當定義在impl塊里的函數其參數中沒有self的時候,Rust會認為其是一個和某個具體的數據結構無關的函數,它和該結構體類在同一個命名空間中。比如我們前邊已經看到的String::from("hello")這樣的調用就是將構造方法放置在String的impl塊里,但是完全沒有使用self參數。
只是現代的C++社區因為有更完善的語言層面的命名空間隔離機制,其實已不太推薦這種古老的靜態函數組織方式。
面向對象和泛型編程
從形式上來說,Rust不提供對傳統的面向對象編程的直接支持,但提供了一些更復雜的面向接口編程的語言級別機制。這一核心武器就是Rust的Traits。某種程度上說,面向接口編程是面向對象編程最核心的精髓之一;繼承、封裝和多態這些基本的武器都可以用面向接口編程的方式來達到。
Rust的泛型編程實現上有很明顯的C++的影子,不同的是它通過Traits機制巧妙的將編譯器多態和運行器多態統一為一體了。
Traits
Traits從概念上來說就是接口,它是Rust支持可擴展程序的基礎;它既可以支持編譯器多態(類似于C++的模板元但是比模板元更為簡單一些),也可以支持基于動態分發技術的運行器多態。從設計的角度來看,Traits機制受C++的設計哲學影響比較深,同樣希望達到零成本的抽象這一至高目標
C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for [Stroustrup, 1994]. And further: What you do use, you couldn’t hand code any better.
Stroustroup
一個描述Hash函數的Traits定義如下
trait Hash { fn hash(&self) -> u64; //can have more functions}兩個實現了該Traits的結構可以定于如下(不一定必須放在同一個源代碼文件中)
impl Hash for bool { fn hash(&self) -> u64 { if *self { 0 } else { 1 } }}impl Hash for i64 { fn hash(&self) -> u64 { self as u64 }}和傳統的C++中的抽象類或Java中的接口不同的時候,Traits是半開放的,這意味著我們可以打開某個定義好的結構,為其添加新的實現;有點類似Ruby的模塊擴展方式。當然Rust 仍然是靜態語言并且是強類型的。C++的模板元雖然可以達到類似的效果,但只支持編譯器多態,并且其Concept的支持雖然千呼萬喚卻一直沒有進入語言標準。
基于泛型的編譯器多態
考慮一個適用上述Traits的例子
fn print_hash(t: &T) { println!("The hash is {}", t.hash())}print_hash(&true); // calls with T=boolprint_hash(&12_i64); //calls with T=i64這里定義了一個打印Hash的泛型函數print_hash,要求對應的類型必須實現了Hash;實際調用的時候,編譯器可以做類型檢查來判斷對應的實際類型是否滿足Traits約束;和C++的Concept非常相像。
此外這種類型約束方式還是可以組合的,當期望泛型類滿足多個Traits約束的時候,可以用+將其串起來, 比如 則要求泛型類T必須同時實現Hash和Eq才能編譯通過。
動態分發的運行期多態
當多態行為依賴于具體運行期才精確得知的條件的時候,泛型就無能為力了。Rust的解決方式是,采用額外的中間層-指針來達到。比如在GUI編程中,我們經常需要處理界面元素的點擊事件,傳統的面向對象思路是定義一個Traits,然后在具體的界面元素上添加一個事件監聽者列表
trait ClickCallback { fn on_click(&self, x: i64, y: i64);}struct Button { listeners: Vec>;}由于結構體的大小必須在編譯期確定,因而直接放一個大小不確定的ClickCallback就不能編譯通過了;標準庫中提供了智能指針來幫我們很優雅地解決了這個問題;因為指針的大小總是確定的。具體到實現上,其原理和C++中的虛函數表非常類似,一個封裝了Traits的智能指針(這里是Box)內部結構上類似于一個vtable,其指向一個在運行期動態構造的函數表。在調用的地方,編譯器可以自動查找具體實現了對應Traits的結構的函數表,轉到正確的調用地址。
函數式編程
函數式編程風格具有更高的抽象層次和更豐富的表達能力,更有利于寫出聲明式風格的代碼。較新的編程語言無一例外都或多或少對函數式編程風格提供支持。Rust的函數式編程具有明顯的Haskell痕跡。
枚舉類型Enum
Rust的枚舉類型和傳統的C++/Java中的枚舉的概念類似,都可以用來表示取值有固定可能性的數據類型;通過與泛型的結合,Enum還擁有和Haskell的抽象數據類型ADT相同的擴展能力。
最簡單的枚舉類型定義可以是如下的樣子
enum IpAddrKind { V4, V6}這里每個具體的枚舉值都是一個不同的具體值,同時他們的類型是一樣的。更復雜一點的情況是,Enum支持每個枚舉的值可以有不同的類型構造,如
enum IpAddr { V4(u8, u8, u8, u8), V6(String),}let home = IpAddr::V4(127, 0, 0, 1)let lo = IpAddr::V6(String::from("::1"))更一般地,具體的枚舉值可以用不同的類型來構造出來;從而我們由此將不同類型的數據聚合在一起形成一個抽象的定義。
struct IpAddr4 { // 細節省略}struct IpAddr6 { // 細節省略}enum IpAddr { V4(IpAddr4), V6(IpAddr6)}模式匹配
一個Enum中可能封裝了不同的數據,當需要對不同的可能的數據做不同的處理的時候,Rust采用模式匹配的方式來提高代碼的可讀性。模式匹配是一種特殊的表達式,采用match關鍵字和一個包含枚舉了所有可能的取值以及其處理代碼的代碼塊組成。譬如考慮上面的地址定義,如果需要對不同的地址類型有不同的處理,可以用模式匹配的方式寫為
fn handle_address(addr : IpAddr) -> i32 { match addr { IpAddr::V4 => 1, IpAddr::V6 => 2, }}這里每一個=>對應,分隔開,其左邊的部分是某個具體的枚舉變量值,右邊是對應的處理表達式。當表達式不止一條語句的時候,可以用大括號隔開。
模式匹配必須保證所有的枚舉值都必須被處理過;并且處理表達式的類型必須是一樣的;否則編譯器會報錯。當枚舉的可能取值有很多個而處理代碼只對其中部分可能值感興趣,可以用_來表示可以匹配所有之前未匹配到的值。
另外一種特殊的情況是,我們僅僅關心某個枚舉值中的一個的時候,match語法依然顯得比較啰嗦;Rust提供了特殊的語法來簡化代碼,如
let some_u8_value = Some(0u8);match some_u8_value { Some(3) => println!("three!") _ => (),}可以改寫為
if let Some(3) = some_u8_value { println!("three!")}類似的我們也可以像常規的處理一樣加上一個else分支來處理其它不匹配的情況。
Option類型
Option是一個封裝類型,其概念和Haskell中的Monad或Java8中的Optional的作用比較類似;都是用于表示一種要么存在一個值要么沒有值的容器。它比空指針有優勢的地方在于它是一種應用邏輯層的抽象;是用于替代空指針的一個很好的工具。
I call it my billion-dollar mistake. At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
Tony Honare, the inventor of null
Rust中的Option是一種構建于泛型技術上的特殊的enum類型
pub enum Option { None, Some(T),}標準庫提供了一些成員函數來實現常見的綁定/鏈式操作范式
- fn is_some(&self) -> bool 判斷是否有值
- fn is_none(&self) -> bool 判斷是否為空
- fn unwrap(self, msg: &str) -> T 用于提取內部存儲的值,如果不存在則用給定的消息panic
- fn unwrap(self) -> T 移動內部存儲的值如果其存在的話;不存在則panic
- fn unwrap_or(self, def: T) -> T 存在的話返回其存儲的值,否則返回提供的默認值
- fn unwrap_or_else(self, f: F) -> T where F: FnOnce() -> T 嘗試提取值,如果不存在調用給定的函數生成一個值
- fn map(self, f: F) -> Option where F: FnOnce(T) -> U 經典的map操作,將值通過給定的函數轉換成另外一個值并封裝成新的Option,如果不存在,則也重新封裝成目標類型的空值
- fn map_or(self, default: U, f:F) -> U where F: FnOnce(T) -> U 類似于map操作,但返回轉換后的類型;如果空則返回給定的默認值
- fn as_ref(&self) -> Option 返回引用類型
- fn as_mut(&mut self) -> Option返回可修改的類型
- fn iter(&self) -> Iter 返回迭代器類型,可以遍歷其值,這里的迭代器總是只能返回一個值
- fn and(self, optB: Option) -> Option 如果沒有值,則返回空,否則返回給定的新的optB,便于鏈式操作減少邏輯判斷
- …
閉包Closure
閉包是另外一個重要的函數式編程工具;Rust采用的語法是比較類似于Ruby,其內部實現上則采用C++的匿名函數模型;即閉包對象其實生成的是匿名的函數對象。一個最簡單的例子
let calculate = |a, b| { let mut result = a * 2; result += b; result};assert_eq!(7, calculate(2, 3)); // 2 * 2 + 3 == 7assert_eq!(13, calculate(4, 5)); // 4 * 2 + 5 == 13閉包的類型注解約束要比函數定義的要求寬松一些,即不需要指定返回類型也可以;和現代C++的generic lambda特性比較類似;都是為了方便程序員寫出更簡潔、干凈的代碼。如下的代碼是完全等價的
fn add_one_v1 (x: i32) -> i32 { x + 1 } // a functionlet add_one_v2 = |x: i32| -> i32 { x + 1 }; // the full syntax for a closurelet add_one_v3 = |x| { x + 1 }; // a closure eliding typeslet add_one_v4 = |x| x + 1 ; // without braces從代碼可讀性和可維護性的角度來看,最好不用閉包來寫太長/太復雜的代碼塊, 因為隨著匿名代碼塊中邏輯的增加,上下文邏輯變得更加模糊;這個時候,用一個命名良好的子函數反而更清晰便于維護。
軟件工程支持 - 工具和方法
Rust提供了成熟的軟件工程實踐支持;有相對完善的模塊文檔和官方的gitboook。
Creates && Cargo系統
作為一門站在巨人肩上的語言,Rust吸收了已有的一些成熟的包管理系統的經驗,并提供了類似的機制來支持更好的協作
- Creates和其包分發系統有點Hackage的影子,又有點NPM的味道
- 版本依賴管理上,和Ruby Gems的處理方式也有些像,雖然toml的格式沒有Ruby的DSL那么靈活強大
Cargo是一個類似于C++中的CMake的系統,同時還提供了一些創建項目模板的快捷方式,幫助程序員快速創建項目骨架,更快專注于具體的實現而不是構建細節。可以用它的子命令來
- 檢查依賴,自動升級依賴
- 增量編譯代碼并報告錯誤
- 根據特定的開關選項執行對應的測試
- 生成文檔
- 運行項目生成的可執行文件(如果不是編譯一個庫)
- 運行benchmark
- 安裝編譯好的二進制構建結果
- 搜索crates中注冊的模塊
具體功能可以查看其命令行幫助。
IDE和編輯器插件支持
某些程序員更喜歡IDE,另外一些人則更熟悉命令行的Vim/Emacs或者其它輕量級的編輯器。社區目前提供了比較豐富的支持,包括對Eclipse/IntelliJ/Visual Studio的IDE插件, 以及對Atom/Visual Studio Code/Sublime/Vim/Emacs的插件支持;基本上比較主流的編程環境的支持都有了;具體支持程度如何,有待進一步驗證;官方的文檔看起來非常值得一試。
測試
Rust支持在包中提供單元測試和功能測試。默認的工具會搜索源碼目錄中的所有單元測試,并自動組織起來運行,同時也提供了一些高級的測試支持。Rust希望程序員明確的區分這兩種測試,并采用不同的約定
- 所有的單元測試都和被測試的源代碼放在一起,并且支持對private方法的測試(當然這個很有爭議,個人建議不要測試private)
- 集成測試被放在專門的test文件夾下邊,可以放在多個文件中,Cargo將會為每個文件生成一個crates
cargo test命令可用來執行所有的測試,并且默認是并發執行的;這樣開發的反饋周期會更短;也可以用命令來顯示要求線性執行 - 傳入 --test-threads=1即可。一些更復雜的特性,如指定某個case的執行,跳過某些特定的case,以及按照某個過濾條件來選擇特定的case,忽略case運行過程中的打印輸出等特性也被貼心的支持了。
總結
在注重極致性能又強調工程協作和擴展性的系統編程領域,Rust做了比較大膽的嘗試,在不引入垃圾收集并保持強類型檢查的前提下, 它期望能將C++的零成本抽象推向一個新的高度而又能避免陷入傳統C/C++語言指針訪問安全性以及復雜的模板元編程等復雜性的泥潭。
它的泛型編程支持和強調值對象唯一所有權的概念和對象生存周期的強制檢查使得多線程并發編程變得輕松簡單;加上強類型檢查的約束,編譯通過的程序往往運行期錯誤也變得很少,這一來自于Haskell的設計哲學深深地影響著Rust。
從一開始就加入的包管理器機制和對豐富的軟件工程工具支持以及對開源社區的熱情擁抱,使得Rust一開始就汲取了傳統C/C++語言工程化支持不足的一些教訓。中心化的軟件倉庫以及對流行IDE、編輯器環境的支持使得它可以更好地贏得社區的支持。
與此同時隨著更新節奏的加快,基于ISO標準化的C++語言也在通過更快的迭代速度和更短的更新周期對這些新加入的競爭者予以反擊;期望Rust能在系統編程領域掀起新的波瀾。
總結
以上是生活随笔為你收集整理的rust为什么显示不了国服_Rust编程语言初探的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 老鹰多少钱啊?
- 下一篇: python高级语法装饰器_Python