那些C語言缺失的,我在Rust裡找到了
Librsvg似乎已經到了這樣的一個地步:直接將C語言開發的部分改用Rust要比繼續使用C語言來得更加容易。更何況,它越來越多的代碼已經使用了Rust。
近來,我在C語言和Rust之間來回切換。在我看來,C語言似乎變得更像老古董。
C語言輓歌
我大概在24年前就愛上了C語言。當時,我通過一本西班牙語版的“The C Programming Language”(第二版,作者是Brian Kernighan和Dennis Ritchie,所以有時候也用K&R來稱呼這本書)來學習C語言。在這之前,我用過Turbo Pascal,它也有指標,也需要手動管理記憶體,而C語言在當時是新生事物,但十分強大。
K&R因其獨特的文風和簡潔明了的代碼風格而聞名。它甚至還教你如何自己實現簡單的malloc()和free()函數,這實在太有意思了。而且,這門語言本身的一些特性也可以通過自身來實現。
在接下來的幾年,我一直使用C語言。它是一門輕巧的程式設計語言,使用差不多2萬行代碼實現了Unix核心。
GIMP和GTK+讓我學會了如何使用C語言來實現物件導向編程,GNOME讓我學會了如何使用C語言維護大型的軟體項目。一個2萬行代碼的項目,一個人花上幾周就可以完全讀懂。
但現在的程式碼程式庫規模已經不可同日而語,我們的軟體對程式設計語言的標準庫有了更高的期望。
C語言的一些好的體驗
第一次通過閱讀POV-Ray原始碼學會如何在C語言中實現物件導向編程。
通過閱讀GTK+原始碼瞭解C語言代碼的清晰、乾淨和可維護性。
通過閱讀SIOD和Guile的原始碼,知道如何使用C語言實現Scheme解析器。
使用C語言寫出GNOME Eye的初始版本,並對MicroTile渲染進行調優。
C語言的一些不好的體驗
在Evolution團隊時,很多東西老是崩潰。那個時候還沒有Valgrind,為了得到Purify這個軟體,需要購買一台Solaris機器。
調試gnome-vfs線程死結問題。
調試Mesa,卻無果。
接手Nautilus-share的初始版本,卻發現代碼裡面居然沒有使用free()。
想要重構代碼,卻不知道該如何管理好記憶體。
想要打包代碼,卻發現到處是全域變數,而且沒有靜態函數。
但不管怎樣,還是來說說那些Rust裡有但C語言裡沒有的東西吧。
自動資源管理
我讀過的第一篇關於Rust的文章是“Rust means never having to close a socket”(http://blog.skylight.io/rust-means-never-having-to-close-a-socket/)。Rust從C++那裡借鑒了一些想法,如RAII(Resource Acquisition Is Initialization,資源擷取即初始化)和智能指標,並加入了值的單一所有權原則,還提供了自動化的決策性資源管理機制。
- 自動化:不需要手動調用free()。記憶體使用量完後會自動釋放,檔案使用完後會自動關閉,互斥鎖在範圍之外會自動釋放。如果要封裝外部資源,基本上只要實現Drop這個trait就可以了。封裝過的資源就像是程式設計語言的一部分,因為你不需要去管理它的生命週期。
- 決策性:資源被建立(記憶體配置、初始化、開啟檔案等),然後在範圍之外被銷毀。根本不存在垃圾收集這回事:代碼執行完就都結束了。程式資料的生命週期看起來就像是函數調用樹。
如果在寫代碼時老是忘記調用這些方法(free/close/destroy),或者發現以前寫的代碼已經忘記調用,甚至錯誤地調用,那麼以後我再也不想使用這些方法了。
泛型
Vec<T>真的就是元素T的vector,而不只是對象指標的數組。在經過編譯之後,它只能用來存放類型T的對象。
在C語言裡需要些很多代碼才能實作類別似的功能,所以我不想再這麼幹了。
trait不只是interface
Rust並不是一門類似Java那樣的物件導向程式設計語言,它有trait,看起來就像是Java裡的interface——可以用來實現動態綁定。如果一個對象實現了Drawable,那麼就可以肯定該對象帶有draw()方法。
不過不管怎樣,trait的威力可不止這些。
關聯類別型
trait裡可以包含關聯類別型,以Rust的Iterator這個trait為例:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
也就是說,在實現Iterator時,必須同時指定一個Item類型。在調用next()方法時,如果還有更多元素,會得到一個Some(使用者定義的元素類型)。如果元素迭代完畢,會返回None。
關聯類別型可以引用其他trait。
例如,在Rust裡,for迴圈可以用於遍曆任何一個實現了IntoIterator的對象。
pub trait IntoIterator {
/// 被遍曆元素的類型
type Item;
type IntoIter: Iterator<Item=Self::Item>;
fn into_iter(self) -> Self::IntoIter;
}
在實現這個trait時,必須同時提供Item類型和IntoIter類型,IntoIter必須實現Iterator,用於維護迭代器狀態。
通過這種方式就可以建立起類型網路,類型之間相互引用。
字串切割
我之前發表了一篇有關C語言缺少字串切割特性的文章(https://people.gnome.org/~federico/blog/rant-on-string-slices.html),解釋了C語言的這個痛點。
依賴管理
以前實現依賴管理需要:
- 手動調用或通過自動化工具宏來調用pkg-config。
- 指定標頭檔和庫檔案路徑。
- 基本上需要人為確保安裝了正確版本的庫檔案。
而在Rust裡,只需要編寫一個Cargo.toml檔案,然後在檔案裡指明依賴庫的版本。這些依賴庫會被自動下載下來,或者從某個指定的地方擷取。
測試
C語言的單元測試非常困難,原因如下:
- 內建函式通常都是靜態。也就是說,它們無法被外部檔案調用。測試程式需要使用#include指令把源檔案包含進來,或者使用#ifdefs在測試過程中移除這些靜態函數。
- 需要編寫Makefile檔案將測試程式連結到其中的部分依賴庫或部分代碼。
- 需要使用測試架構,並把測試案例註冊到架構上,還要學會如何使用這些架構。
而在Rust裡,可以在任何地方寫這樣的代碼:
#[test]
fn test_that_foo_works() {
assert!(foo() == expected_result);
}
然後運行cargo test運行單元測試。這些代碼只會被連結到測試檔案中,不需要手動編譯任何東西,不需要編寫Makefile檔案或抽取內建函式用於測試。
對我來說,這個功能簡直就是殺手鐧。
包含測試的文檔
在Rust中,可以將使用Markdown文法編寫的注釋產生文檔。注釋裡的測試代碼會被作為測試案例執行。也就是說,你可以在解釋如何使用一個函數的同時對它進行單元測試:
/// Multiples the specified number by two
///
/// ```
/// assert_eq!(multiply_by_two(5), 10);
/// ```
fn multiply_by_two(x: i32) -> i32 {
x * 2
}
注釋中的範例程式碼被作為測試案例執行,以確保文檔與實際代碼保持同步。
衛生宏(Hygienic Macro)
Rust的衛生宏避免了C語言宏可能存在的問題,比如宏中的一些東西會掩蓋掉代碼裡的標識符。Rust並不要求宏中所有的符號都必須使用括弧,比如max(5 + 3, 4)。
沒有自動轉型
在C語言裡,很多bug都是因為在無意中將int轉成short或char而導致,而在Rust裡就不會出現這種情況,因為它要求顯示轉型。
不會出現整型溢出
這個就不用再多作解釋了。
在安全模式下,Rust裡幾乎不存在未定義的行為
在Rust的“安全”模式下編寫的代碼(unsafe{}代碼塊之外的代碼)如果出現了未定義行為,可以直接把它當成是一個bug來處理。比如,將一個負整數右移,這樣做是完全可以的。
模式比對
在對一個枚舉類型進行switch操作時,如果沒有處理所有的值,gcc編譯器就會給出警告。
Rust提供了模式比對,可以在match運算式裡處理枚舉類型,並從單個函數返回多個值。
impl f64 {
pub fn sin_cos(self) -> (f64, f64);
}
let angle: f64 = 42.0;
let (sin_angle, cos_angle) = angle.sin_cos();
match運算式也可以用在字串上。是的,字串。
let color = "green";
match color {
"red" => println!("it's red"),
"green" => println!("it's green"),
_ => println!("it's something else"),
}
你是不是很難猜出下面這個函數是幹什麼用的?
my_func(true, false, false)
但如果在函數的參數上使用模式比對,那麼事情就會變得不一樣:
pub struct Fubarize(pub bool);
pub struct Frobnify(pub bool);
pub struct Bazificate(pub bool);
fn my_func(Fubarize(fub): Fubarize,
Frobnify(frob): Frobnify,
Bazificate(baz): Bazificate) {
if fub {
...;
}
if frob && baz {
...;
}
}
...
my_func(Fubarize(true), Frobnify(false), Bazificate(true));
標準的錯誤處理
在Rust裡,不再只是簡單地返回一個布爾值表示出錯與否,也不再簡單粗暴地忽略錯誤,也不再通過非本地跳轉來處理異常。
#[derive(Debug)]
在建立新類型時(比如建立一個包含大量欄位的struct),可以使用#[derive(Debug)],Rust會自動列印該類型的內容用於調試,不需要再手動編寫函數去擷取類型的資訊。
閉包
不再需要使用函數指標了。
結論
在多線程環境裡,Rust的並發控制機制可以防止出現資料竟態條件。我想,對於那些經常寫多線程並發代碼的人來說,這會是個好訊息。
C語言是一門古老的語言,用它來編寫單一處理器的Unix核心或許是個不錯的選擇,但對於現今的軟體來說,它算不上好語言。
Rust有一定的學習曲線,但我覺得完全值得一學。它之所以不好學,是因為它要求開發人員對��己所寫的代碼必須有充分的瞭解。我想,Rust是一門這樣的語言:它可以讓你變成更好的開發人員,而且它會成為你解決問題的利器。
查看英文原文:Rust things I miss in C
本文永久更新連結地址:https://www.bkjia.com/Linux/2018-03/151270.htm