Rust 程式設計:基礎概念

在本章,我們對程式 (program) 及程式設計 (programming) 進行一些概念上的說明;接著,會介紹 Rust 的一些特性以及和其他程式語言的比較。在這裡,讀者不需要實際撰寫程式。

程式

電腦程式內預先寫好了特定的步驟,在執行該程式時完成特定的任務。程式有很多種,小到某個終端機的指令 (command),大到整個作業系統 (operating system),都是不同種類的程式。除了使用現有的程式,電腦使用者也可自行撰寫新的電腦程式。透過撰寫電腦程式,使用者不再受限於現有的程式所提供的功能,而可以將某個特定的任務,以自己期待的方式,利用電腦來解決。

將某個任務轉為電腦程式的過程,就是程式設計 (programming),在後文中,會探討這個議題。在實作程式的過程中,我們會撰寫程式碼 (code),程式碼經特定軟體的處理後,會轉化成程式,這個轉化的過程,可透過編譯 (compilation) 或是直譯 (interpretation) 來完成,而這個特定的軟體,就是編譯器 (compiler) 和直譯器 (interpreter)。

程式碼會以某種程式語言 (programming language) 來呈現。程式語言和人類平日使用的自然語言 (natural language) 不同,前者需遵循正規的語法規範。學習特定的程式語言的語法也是學習程式設計的一部分。如果以上述的程式碼轉化成程式的方式來區分,程式語言大略分為編譯語言 (compiled language) 和直譯語言 (interpreted language),如果將同樣的步驟以不同程式語言撰寫,通常編譯語言所生成的程式,其效能較佳。本書所要介紹的 Rust 也是一種編譯語言。

程式設計

程式設計是將某個問題轉換成可由電腦執行的程式,以利用電腦解決該問題。程式設計分為以下三個步驟:

  1. 分析問題
  2. 設計相關步驟及設計相關的演算法 (algorithm)
  3. 實作 (implementation),就是撰寫程式碼

分析問題需要該問題相關的領域知識 (domain knowledge),例如,如果我們想寫一個預測基因體中基因可能存在的位置,我們必需學習相關的分子生物學的知識。程式設計的書,無法對領域知識提供太多幫助,而需要讀者另行找尋相關資訊。

演算法是針對某個問題的解決步驟。學習演算法的目的,不僅在於設計正確的操作步驟,更要設計有效率的步驟。相較於程式設計,演算法是更為抽象的主題。建議程式設計初學者先至少熟悉一種程式語言後,再回頭學習演算法。本書不會用到深入的演算法,而以初階的程式設計為主。

實作演算法需要使用某種程式語言,要選用那一種語言,有許多不同的考量,像是發布平台、執行效率、開發時間、問題領域、個人偏好等。演算法和程式語言是無關的,但是由於實務上的考量,有時候某些程式語言會更適合某些特定的任務,有些則僅是個人喜好而已。本書的目標是讓讀者學會 Rust 程式設計,日後應用在各種不同的任務中。

程式的基本組成

程式的運行過程可能很複雜,但大抵上包含以下數種核心概念:

  • Sequencing
  • Selection
  • Iteration
  • Procedural abstraction, i.e. function, procedure, subroutine
  • Recursion
  • Data abstraction, i.e. class and/or object
  • Concurrency
  • Exceptional handling
  • Nondeterminacy

在學第一個程式語言時,要同時學習這些概念和程式語言的語法,所以學習過程會較長。第二、三個以後的語言,只要專注在語法上即可,學起來就會快得多。

撰碼風格

每個程式語言都有自己的撰碼風格 (coding style),雖然不造撰碼風格寫程式也不一定會造成程式的錯誤,維持良好且一致的撰碼風格可以使得程式碼更容易維護。程式碼不僅是要傳給編譯器或直譯器執行,也是要給程式設計者看的,包括未來的自己。如果曾經接手過維護不良的程式碼,還要浪費時間去猜這些程式碼的意圖,就會知道撰寫良好的程式碼的重要性。從學習程式的早期就開始習慣良好的撰碼風格,對日後會有相當的幫助。

如果讀者想學習 Rust 的撰碼風格,可參考這裡

Rust

了解一個程式語言適用的層面,對於是否要在自己的專案中使用這個語言,是有相當幫助的 ─ 畢竟,將專案整個重寫的代價實在太大了。接下來,我們會簡介 Rust 這個語言,讓讀者知道為什麼 Rust 值得學習。

根據 Rust 官方網站的說明,Rust 是「安全、快速、共時的系統程式語言」,我們將會逐一討論。

在 C 或 C++ 中,使用者有較大的自由,像是可以自由地操作指標和記憶體,然而,C 或 C++ 不是一個安全的語言,有許多的動作是未定義且危險的,程式設計者需維持高度自律,以避開這些問題。在許多高階語言中,程式是安全的,但高階語言的使用者無法像在 C 或 C++ 中那麼自由。而 Rust 中,透過嚴格的編譯器,將許多 C/C++ 中相對危險的操作視為程式的錯誤,但仍保有一些操作指標的自由。嚴格的編譯器使得 Rust 較難上手,以前在程式執行時發生的錯誤,現在提早到編譯時就發現。往好處想,當程式能順利編譯時,該程式的錯誤也比較少。

一些評效 (benchmark) 顯示,在相同演算法的前提下,Rust 程式已有接近等效 C++ 程式的執行速度。在實務中,已經有人用 Rust 撰寫遊戲引擎 (game engine) (如 Piston) 和作業系統 (operating system) (如 Redox),而這些項目都對效能有較高需求,可以知道 Rust 已有足夠的效能和低階操作,應對不同層面的任務。其中的原因之一,在於 Rust 不依賴垃圾回收 (garbage collection) 來管理記憶體,而是使用類似 C++ 中的 resource acquisition is initialization (RAII) 機制,此外,Rust 也不需要透過虛擬機器 (virtual machine) 來運行,其程式碼會轉為原生的機械碼。

由於物理條件的限制,現在的 CPU 不再以衝高時脈為目標,而朝向多核心發展,能夠支援共時性 (concurrency) 的程式語言,對於程式的效率,會有一定的提升。然而,比起一般的程式,共時性程式較難撰寫,程式中也會有更多難以發現的臭蟲 (bug)。Rust 同樣將安全的特性放入共時性程式中,讓程式設計者在安全的前提下,撰寫共時運算的程式。

系統程式語言 (system programming language) 指的是用來撰寫系統軟體 (system software) 的程式語言。系統軟體和應用軟體 (application software) 是相對的概念,應用軟體直接和使用者互動,像是網頁瀏覽器 (web browser) 或是文字處理器 (word processor) 等;系統軟體則是和電腦硬體互動,或是提供執行應用軟體的平台,像是作業系統、工具軟體 (utility software)、驅動程式 (device driver)、編譯器 (compiler) 等。近年來,系統程式語言的主流是 C 或 C++,而 Rust 提供相似的角色。然而,這不代表 Rust 不能用來寫應用軟體,就像是 C++ 也可以用來寫應用軟體一般。

雖然 C/C++ 讓我們不再需要為每個特定的平台撰寫組合語言,然而,使用 C/C++ 仍然要去面對平台間的差異,所以才需要撰寫條件式編譯相關程式碼。撰寫 Rust 程式碼時,不用再撰寫條件式編譯的程式碼,也就是說,Rust 程式碼是跨平台的。跨平台不是什麼新聞,在 Java 之後的程式語言若不能跨平台很難有立足之地,然而,大部分的高階程式語言都依賴某個特定的執行環境 (runtime environment),而 Rust 程式碼在跨平台的前提下,可產生原生機械碼。

另外,值得一提的是,Rust 和其他語言間的合作。雖然 Rust 的角色類似 C 和 C++,但是,許許多多以 C 和 C++ 撰寫的函式庫,已經相當成熟穩健,重新以 Rust 實作這些函式庫不是明智的選擇,而 Rust 提供介面讓我們很容易地再利用這些函式庫,減少重造輪子所浪費的心力。另一方面,某些任務,使用其他高階語言即可完成,也不需要用 Rust 全部重寫,這時候,Rust 的角色就如同 C 或 C++ 一般,為這些高階語言提供延伸模組,在關鍵步驟為程式加速。

由於 Rust 在演進的過程中,嘗試了一些語法特性,但後來又廢棄不用,使得網路上一些關於 Rust 的文章變成錯誤的資訊,學習者透過網路學習 Rust 時,需多方嘗試。隨著 1.0 版發布,Rust 的核心特性大抵上穩定下來,這個問題逐漸減少。

由於 Rust 還是一個年輕的語言,目前的函式庫 (library) 和框架 (framework) 沒有那麼豐富。使用者不能期待像 Java 或是 Python 般完整的社群資源。在將 Rust 引入自己的專案前,需要評估是否有充足的外部資源,還是需要自行實作。隨著時間,Rust 社群成長,這一點應該會逐漸改善。

至於其他的特性,在學習 Rust 的過程中就會逐漸體會,這裡不再贅述。

Rust vs. 其他語言

程式語言間不存在絕對的優劣,這類討論往往最後流於不同意識型態間的爭執。此外,演算法是獨立於程式語言之外的,不會有某個演算法只能用某個語言來實作的情形,即使某個語言缺乏某些特性,通常可以用其他的方法代替。語言的選擇,往往是許多因素綜合考量後的結果。這裡的比較,基於筆者過去的經驗,而帶有主觀的色彩,不是絕對的標準。讀者可再多方收集相關資訊,但不需執著於誰好誰壞。

vs. C++

C++ 傾向於在語言層次提供各種豐富的機制,讓使用者從中自由組合出期待的效果,這也使得 C++ 成為一個複雜的語言;Rust 也有豐富的語法機制,不過,Rust 吸收了許多函數式語言的特色,寫起來和 C/C++ 風格的語言有所不同,需要一段時間來適應。C++ 將記憶體管理的責任留給使用者,Rust 則自動處理大部份的記憶體操作。比起 C++,Rust 的編譯器較為嚴格,將許多常見的錯誤提前到編譯時期,使得程式更為安全。

vs. Java

Java 從一開始就強調物件導向程式設計,將物件導向融入 Java 的語法;Rust 也支援物件導向,但並不特別強調。比起 Java,Rust 的物件導向略為不同,傾向用組合 (composition) 代替繼承 (inheritance) 等。出於安全性的考量,Java 將指標操作取消,而 Rust 仍然保有指標操作。Java 程式需要在虛擬機器下運行,而 Rust 程式為原生機械碼。

vs. Python

雖然 Rust 有著部分高階語言的特性,比起 Python 這類高階直譯語言,仍需關注更多的細節,而需花費更多時間實作程式。和 Rust 相比,Python 支援更多高階特性,使用者可以用更短的時間撰寫程式。然而,純 Python 實作的程式效能較差,隨著程式規模增加,這個差距會更加明顯。常見的開發模式是使用一些方式將程式加速,目前已有許多方案,像是 Cython 或 PyPy 等,Rust 可視為另一個新的作法。

vs. Go

Rust 和 Go 時常會拿來相比,由於 (1) 發布時間接近,(2) 同為可自動管理記憶體的編譯語言,(3) 兩者分別用不同的特性改善傳統編譯語言的缺點。Go 使用一套簡單易學的語法機制,使用者可以很快熟悉大部分 Go 的特性,將其使用在自己的專案,但是,Go 缺乏部分重要的語法特性,為了向下相容,短期內這些問題不會改變;Rust 的語法機制則較完整,但語法不穩定的問題,使得 Rust 函式庫相對不穩定。另外,Go 依賴垃圾回收,使得 Go 較不適合即時運算 (real-time computing) 方面的應用,而 Rust 沒有這個問題。