# Rust:令人沉迷於自虐的程式語言
開個新坑吧。
這系列來分享一個最近幾年變得特別熱門的程式語言:Rust。
其實 Rust 語言早在 2010 年就已經問世了,最早是基於「作為著重記憶體安全性和性能的系統級語言」而生,由 Mozilla 社群(就是開發、維護 Firefox 瀏覽器的那個 Mozilla)所主導。
多年來,許多程式的安全性漏洞都是基於記憶體管理不當而發生,這樣的議題隨著資訊技術的發達而越來越受到開發者重視,再加上近年 Rust 的生態日趨完善,它的優勢也就逐漸被開發者注意並走入大眾視野。直到 2020 年代,Android、Windows、Linux 的開發人員甚至都開始嘗試使用 Rust 撰寫這些專案的部分程式碼,雖然要完全替換是不太可能,但若要說 Rust 是 C++ 的下一代接班人,肯定也不為過。
<br/>
## ► 為什麼學習 Rust?
若要問起為什麼我會想學 Rust,原因很單純:聽說它很難。
本來我只是想要看看一門程式語言可以自虐到什麼地步,等到實際用它投入開發的時候,才真正體悟到為何它如此龜毛,卻又能夠如此受到歡迎──若要用簡單的描述概括,它無論是語言本身的設計還是編譯器的規則,都可以說是卯足全力阻止你寫出爛程式碼,迫使你寫出考慮周密而穩定的程式。在接觸 Rust 幾個月以後,身為完美主義者的我也成為那眾多愛上 Rust 的開發者之一了。
我曾接觸過各種不同類型的語言,其中 Rust 語言對我既有的觀念帶來的顛覆是以前未曾感受過的。即便你沒有使用 Rust 作為主要語言的打算,我也會強烈推薦你投入時間學習它、瞭解它。僅僅是試圖用它重寫一些簡單的專案,你也會因此開始注意許多以前你從來不在乎的細節,學習 Rust 所帶來的收益會很容易反映在你所有的專案上。
為了不讓篇幅過長,關於「Rust 有多嚴格」這件事情,以及這些設計帶來的好處,就等之後的章節讓我一點一點透過實際的例子來體現吧。
如果你還沒學過 Rust 也不用因為聽說很困難而卻步。得益於 ChatGPT 等 AI 模型的技術發展,現在我們可以利用這些語言模型作為學習 Rust 時的輔助工具,這語言也就沒有想像中那麼變態了。
<br/>
## ► 準備你的開發環境和專案
不免俗的,我們總要從最簡單的部分開始,就像我們學習其他任何的程式語言一樣。
首先到 Rust-lang 官方網站安裝 rustup-init,你會因此得到兩樣東西:
* Rustup:用來管理你的 Rust 工具鏈(像是 Rust 編譯器)版本
* Cargo:用來管理你的專案和依賴項目
對於大部分的使用者來說,rustup 除了查看或升級 Rust 版本以外,沒有其他作用了。在使用 Rust 開發專案的時候,使用最多的應該會是 cargo 工具。
安裝好以後,首先打開任一 terminal(例如 CMD 或 PowerShell),用 cd 指令到達你想要建立專案的地方,接著執行命令:
```powershell
cargo init test_project
```
這樣你就在資料夾底下建立了一個名字叫做「test_project」的專案。如果系統沒有辦法找到 cargo,那有可能是你的 terminal 沒有讀取到安裝檔剛更新上去的環境變數,通常只要把帶有 terminal 的程式(例如 Windows Terminal 或 VS Code)整個重開就能解決了,再不行的話可以試試重開機。
建立好專案以後,使用 VS Code 的「Open Folder(開啟資料夾)」把剛才創立的 test_project 資料夾打開,這時候使用 [Ctrl] + [~] 快捷鍵打開 terminal,你應該會看見 terminal 的當前資料夾是落在「x:/xxxx/test_project」這個目錄底下,而不是「x:/xxxx」或「x:/xxxx/test_project/src」。
接著,使用快捷鍵 Ctrl+Shift+X 或是從左邊的選項找到 extensions 頁籤,在搜尋欄搜尋並安裝這兩樣東西:
* Even Better TOML
* rust-analyzer
前者能夠給 cargo.toml 依照格式上色,提高可讀性,後者能夠即時對程式碼靜態分析,找出編譯錯誤或警告,對 Rust 專案來說是不可或缺的工具。安裝好這些東西以後,你基本上已經把環境都設定好了。
在你的專案資料夾底下,預設會有這些東西:
* src:你的程式碼
* target:編譯出來的執行檔或過程遺留下來的暫存檔,這些檔案留著可以改善下次該專案的編譯速度,不需要的時候可以刪除
* .gitignore:給 git 工具使用的忽略清單,預設忽略 target 資料夾
* Cargo.lock:用來給 Cargo 工具檢查依賴函式庫的校驗資訊,因為這是給自動化工具而不是人類用的,切勿更動
* Cargo.toml:專案的附加資訊,包括專案依賴的函式庫名稱、版本、啟用的功能等
根據我的經驗和理解,因為 Rust 的專案是基於 LLVM 來編譯,過程會採用很多複雜的最佳化策略,所以多次編譯下來產生出來的暫存檔很大,編譯速度也普遍比較慢。因此,如果可以的話,我會建議盡量把專案放在 SSD 上面,且不要把它和 Dropbox 之類的雲端硬碟同步,不然你的雲端空間大概很快就爆了。假如是想要創建一個能推到 GitHub repo 的專案,你可以先在 GitHub 上面創設一個 repo,用 git 工具把它 clone 下來,然後再從 terminal 進入剛剛 clone 下來的 repo 資料夾,執行「cargo init」來創建專案。
<br/>
## ► Hello, world!
接下來就可以測試你的專案了。沒有意外的話,Cargo 會自動幫你產生一個 Hello, world! 的專案:
```rust
// src/main.rs
fn main() {
println!("Hello, world!");
}
```
如果想要編譯並執行你的程式,在 terminal 執行:
```powershell
cargo run
```
如果想要編譯成執行檔,則用 build 命令:
```powershell
cargo build
```
你的執行檔就會出現在 target/debug/ 底下。
不過,單單呼叫 cargo build 的話,編譯器是不會套用所有優化策略的。如果程式已經確定完成了、需要實際發行或投入使用,你可以加上 release tag,讓編譯器知道你需要把成品最佳化:
```powershell
cargo build --release
```
經過最佳化編譯的執行檔就會出現在 target/release/ 底下了。
<br/>
## ► Rust 語言的巨集
既然基本的流程我們已經搞懂,就可以回頭來專注在程式碼上面。這個時候遇到的第一個問題是:「為什麼 `println` 後面要加上驚嘆號?」
這就要說到 Rust 本身的特性和它的巨集功能。
(註:macro,簡體中文圈一般習慣稱之為「宏」而不是「巨集」。由於簡體中文的資料比較多,你也許會時常看見「宏」這個稱呼)
如果你曾寫過 C 語言的話,應該對於 #define 有些印象:
```C
#define MAX(x, y) (x)>(y) ? (x):(y)
```
我們可以把某些簡單的行為寫成巨集而不是函式,編譯器會在編譯時自動地把這些巨集和其內容視為等價的語法。
也就是說,當我們在 C 語言當中這麼寫:
```C
#include <stdio.h>
#define MAX(x, y) (x)>(y) ? (x):(y)
int main() {
int a = 3;
int b = 5;
printf("The bigger one has value: %d\n", MAX(a, b));
return 0;
}
```
編譯器會先暗自把 main 的內容轉換為:
```C
int main() {
int a = 3;
int b = 5;
printf("The bigger one has value: %d\n", (a)>(b) ? (a):(b));
return 0;
}
```
然後才開始編譯流程。實際上這個行為比較接近文本的替換而不是函數的呼叫,在某些情況下具有性能優勢。
通常情況下,print 函數裡可能會有好幾個不同的參數,且參數的數量多寡並不一定。然而,Rust 的原則正是希望「凡事都能在編譯期確定」,所以 Rust 的函數在設計之初,就不能像其他語言一樣把函數直接寫成可變數量參數的形式。若是單單為了一個 print 而動用 array、vector 之類的複雜型態來實現這個功能,則更是本末倒置。
為了讓程式碼具備強大的擴充性,Rust 引入了強大的巨集功能,這所謂的巨集就像剛才提到 C 語言的 #define 一樣,而 Rust 正是使用巨集功能來實現基本的 println。在 Rust 語法當中,名稱尾端的驚嘆號表示這是一個巨集的名稱,也就是說,你呼叫的是一個名為 println 的巨集,而不是名為 println 的函數。
比方說,我們想自行定義一個 add!(x, y) 的巨集,使用起來就會像這樣:
```Rust
macro_rules! add {
($x:expr, $y:expr) => {
$x + $y
};
}
fn main() {
let result = add!(123, 456);
println!("Result: {result}");
}
```
在範例程式碼當中,編譯器就會在編譯時自動把 add!(123, 456) 展開,變成 123+456,然後才繼續編譯。實際上 macro 可以做到的事情遠遠比這個更多(包括定義結構體、函數,甚至是更複雜的型態轉換),而且也同樣可以透過 rust-analyzer 即時找出會引起編譯錯誤的問題。關於這部分,就等之後的章節再獨立拿出來談吧。
至此,你已經準備好了開發環境,也學會了編譯 Rust 的 Hello, world! 範例程式。
<br/>
###### tags: `Rust` `程式設計`