本篇文章有不少程式碼,如果這裡的排版讓你感到閱讀困難,請服用 HackMD 好讀版。
開個新坑吧。
這系列來分享一個最近幾年變得特別熱門的程式語言:Rust。
其實 Rust 語言早在 2010 年就已經問世了,最早是基於「作為著重記憶體安全性和性能的系統級語言」而生,由 Mozilla 社群(就是開發、維護 Firefox 瀏覽器的那個 Mozilla)所主導。
多年來,許多程式的安全性漏洞都是基於記憶體管理不當而發生,這樣的議題隨著資訊技術的發達而越來越受到開發者重視,再加上近年 Rust 的生態日趨完善,它的優勢也就逐漸被開發者注意並走入大眾視野。直到 2020 年代,Android、Windows、Linux 的開發人員甚至都開始嘗試使用 Rust 撰寫這些專案的部分程式碼,雖然要完全替換是不太可能,但若要說 Rust 是 C++ 的下一代接班人,肯定也不為過。
? 為什麼學習 Rust?
若要問起為什麼我會想學 Rust,原因很單純:聽說它很難。
本來我只是想要看看一門程式語言可以自虐到什麼地步,等到實際用它投入開發的時候,才真正體悟到為何它如此龜毛,卻又能夠如此受到歡迎──若要用簡單的描述概括,它無論是語言本身的設計還是編譯器的規則,都可以說是卯足全力阻止你寫出爛程式碼,迫使你寫出考慮周密而穩定的程式。在接觸 Rust 幾個月以後,身為完美主義者的我也成為那眾多愛上 Rust 的開發者之一了。
我曾接觸過各種不同類型的語言,其中 Rust 語言對我既有的觀念帶來的顛覆是以前未曾感受過的。即便你沒有使用 Rust 作為主要語言的打算,我也會強烈推薦你投入時間學習它、瞭解它。僅僅是試圖用它重寫一些簡單的專案,你也會因此開始注意許多以前你從來不在乎的細節,學習 Rust 所帶來的收益會很容易反映在你所有的專案上。
為了不讓篇幅過長,關於「Rust 有多嚴格」這件事情,以及這些設計帶來的好處,就等之後的章節讓我一點一點透過實際的例子來體現吧。
如果你還沒學過 Rust 也不用因為聽說很困難而卻步。得益於 ChatGPT 等 AI 模型的技術發展,現在我們可以利用這些語言模型作為學習 Rust 時的輔助工具,這語言也就沒有想像中那麼變態了。
? 準備你的開發環境和專案
不免俗的,我們總要從最簡單的部分開始,就像我們學習其他任何的程式語言一樣。
首先到 Rust-lang 官方網站安裝 rustup-init,你會因此得到兩樣東西:
對於大部分的使用者來說,rustup 除了查看或升級 Rust 版本以外,沒有其他作用了。在使用 Rust 開發專案的時候,使用最多的應該會是 cargo 工具。
安裝好以後,首先打開任一 terminal(例如 CMD 或 PowerShell),用 cd 指令到達你想要建立專案的地方,接著執行命令:
這系列來分享一個最近幾年變得特別熱門的程式語言:Rust。
其實 Rust 語言早在 2010 年就已經問世了,最早是基於「作為著重記憶體安全性和性能的系統級語言」而生,由 Mozilla 社群(就是開發、維護 Firefox 瀏覽器的那個 Mozilla)所主導。
多年來,許多程式的安全性漏洞都是基於記憶體管理不當而發生,這樣的議題隨著資訊技術的發達而越來越受到開發者重視,再加上近年 Rust 的生態日趨完善,它的優勢也就逐漸被開發者注意並走入大眾視野。直到 2020 年代,Android、Windows、Linux 的開發人員甚至都開始嘗試使用 Rust 撰寫這些專案的部分程式碼,雖然要完全替換是不太可能,但若要說 Rust 是 C++ 的下一代接班人,肯定也不為過。
? 為什麼學習 Rust?
若要問起為什麼我會想學 Rust,原因很單純:聽說它很難。
本來我只是想要看看一門程式語言可以自虐到什麼地步,等到實際用它投入開發的時候,才真正體悟到為何它如此龜毛,卻又能夠如此受到歡迎──若要用簡單的描述概括,它無論是語言本身的設計還是編譯器的規則,都可以說是卯足全力阻止你寫出爛程式碼,迫使你寫出考慮周密而穩定的程式。在接觸 Rust 幾個月以後,身為完美主義者的我也成為那眾多愛上 Rust 的開發者之一了。
我曾接觸過各種不同類型的語言,其中 Rust 語言對我既有的觀念帶來的顛覆是以前未曾感受過的。即便你沒有使用 Rust 作為主要語言的打算,我也會強烈推薦你投入時間學習它、瞭解它。僅僅是試圖用它重寫一些簡單的專案,你也會因此開始注意許多以前你從來不在乎的細節,學習 Rust 所帶來的收益會很容易反映在你所有的專案上。
為了不讓篇幅過長,關於「Rust 有多嚴格」這件事情,以及這些設計帶來的好處,就等之後的章節讓我一點一點透過實際的例子來體現吧。
如果你還沒學過 Rust 也不用因為聽說很困難而卻步。得益於 ChatGPT 等 AI 模型的技術發展,現在我們可以利用這些語言模型作為學習 Rust 時的輔助工具,這語言也就沒有想像中那麼變態了。
? 準備你的開發環境和專案
不免俗的,我們總要從最簡單的部分開始,就像我們學習其他任何的程式語言一樣。
首先到 Rust-lang 官方網站安裝 rustup-init,你會因此得到兩樣東西:
- Rustup:用來管理你的 Rust 工具鏈(像是 Rust 編譯器)版本
- Cargo:用來管理你的專案和依賴項目
對於大部分的使用者來說,rustup 除了查看或升級 Rust 版本以外,沒有其他作用了。在使用 Rust 開發專案的時候,使用最多的應該會是 cargo 工具。
安裝好以後,首先打開任一 terminal(例如 CMD 或 PowerShell),用 cd 指令到達你想要建立專案的地方,接著執行命令:
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」來創建專案。
? Hello, world!
接下來就可以測試你的專案了。沒有意外的話,Cargo 會自動幫你產生一個 Hello, world! 的專案:
// src/main.rs fn main() { println!("Hello, world!"); } |
如果想要編譯並執行你的程式,在 terminal 執行:
cargo run |
如果想要編譯成執行檔,則用 build 命令:
cargo build |
你的執行檔就會出現在 target/debug/ 底下。
不過,單單呼叫 cargo build 的話,編譯器是不會套用所有優化策略的。如果程式已經確定完成了、需要實際發行或投入使用,你可以加上 release tag,讓編譯器知道你需要把成品最佳化:
cargo build --release |
經過最佳化編譯的執行檔就會出現在 target/release/ 底下了。
? Rust 語言的巨集
既然基本的流程我們已經搞懂,就可以回頭來專注在程式碼上面。這個時候遇到的第一個問題是:「為什麼 println 後面要加上驚嘆號?」
這就要說到 Rust 本身的特性和它的巨集功能。
(註:macro,簡體中文圈一般習慣稱之為「宏」而不是「巨集」。由於簡體中文的資料比較多,你也許會時常看見「宏」這個稱呼)
如果你曾寫過 C 語言的話,應該對於 #define 有些印象:
#define MAX(x, y) (x)>(y) ? (x):(y) |
我們可以把某些簡單的行為寫成巨集而不是函式,編譯器會在編譯時自動地把這些巨集和其內容視為等價的語法。
也就是說,當我們在 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 的內容轉換為:
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) 的巨集,使用起來就會像這樣:
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! 範例程式。
HackMD 好讀版:https://hackmd.io/@upk1997/rust-hello-world
縮圖素材原作者:Karen Rustad T?lva(CC0 1.0)