那些C語言缺失的,我在Rust裡找到了

來源:互聯網
上載者:User

那些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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.