編碼 · 程式語言
Rust 入門教學
簡答
Rust 是編譯式、靜態型別的系統程式語言,速度與 C、C++ 相當,卻能在編譯期防止記憶體錯誤,而且不需要垃圾回收。用 rustup 安裝後,執行 cargo new hello 與 cargo run。核心概念包括所有權與借用(由借用檢查器負責檢查)、搭配 match 的 struct 與 enum、Vec 和 HashMap 等集合,以及使用 Result 與 ? 運算子的錯誤處理。
Rust 是什麼,用來做什麼
Rust 起初是 Graydon Hoare 的個人專案,後來由 Mozilla 贊助,並於 2015 年發布 1.0 版,現在由 Rust Foundation 維護。Rust 用於速度與可靠性都重要的地方:命令列工具(ripgrep)、網路服務、Firefox 的瀏覽器元件、Linux 核心與 Windows 的部分模組、嵌入式裝置與 WebAssembly。它的編譯器很嚴格,但換來的是:在安全的 Rust 程式碼中,use-after-free 與資料競爭等整類錯誤都不可能發生。
安裝 Rust 並執行 Hello, world
從 rustup.rs 使用 rustup 安裝工具鏈。它會提供 rustc(編譯器)與 Cargo(建置工具和套件管理工具)。建立專案:
cargo new hello
cd hello
cargo run
# Hello, world!Cargo 會產生 src/main.rs:
fn main() {
println!("Hello, world!");
}println! 以 ! 結尾,因為它是巨集(macro)而不是函式。巨集在編譯期產生程式碼;這裡的格式字串會在編譯時被檢查。
變數與基本型別
變數預設是不可變的;加上 mut 才允許修改。型別通常可以推論,但每個值都只有一個靜態型別,數值轉換則要用 as 明確指定。
| 型別 | 範例 | 說明 |
|---|---|---|
i32, i64, u8, usize | let n: i32 = 42; | 有號(i)與無號(u);索引使用 usize |
f64 | let x = 3.14; | 預設的浮點數型別 |
bool | let ok = true; | true 或 false |
char | let c = 'é'; | 一個 Unicode 純量值,4 位元組 |
&str | let s = "hi"; | 借用的字串切片 |
String | String::from("hi") | 擁有所有權、可增長的 UTF-8 文字 |
| Tuple、陣列 | (1, "a"), [0; 3] | 固定大小 |
fn main() {
let name = "Ada";
let mut count = 7;
count += 1;
let avg = count as f64 / 2.0;
println!("{name} has {count} items, avg {avg}"); // Ada has 8 items, avg 4
let parsed: i32 = "12".parse().unwrap();
println!("{}", parsed + 1); // 13
}流程控制與函式
if、match 與區塊都是運算式:它們會產生值。函式最後一個沒有分號的運算式就是它的回傳值。
fn square(x: i32) -> i32 {
x * x
}
fn main() {
let score = 72;
let grade = if score >= 90 { "A" } else if score >= 70 { "B" } else { "C" };
println!("{grade}"); // B
for i in 0..3 {
print!("{} ", square(i)); // 0 1 4
}
println!();
let mut n = 10;
while n > 1 {
n /= 2;
}
let label = loop {
break "done"; // loop can return a value with break
};
println!("{n} {label}"); // 1 done
}招牌特色:所有權與借用
每個值都恰好有一個擁有者。擁有者離開作用域時,值就會被釋放(drop)。把 String 這類堆積上的值指派給另一個變數,會將它移動(move),舊的名稱就不能再使用。若要在不取得所有權的情況下使用某個值,就用參考來借用它:同一時間可以有任意數量的共享參考 &T,或恰好一個可變參考 &mut T。
fn length(s: &str) -> usize {
s.len() // borrows, does not take ownership
}
fn add_exclamation(s: &mut String) {
s.push('!');
}
fn main() {
let a = String::from("hello");
let b = a; // ownership moves to b
// println!("{a}"); // error: borrow of moved value: `a`
let mut c = b.clone(); // explicit deep copy
add_exclamation(&mut c);
println!("{} {} {}", b, c, length(&c)); // hello hello! 6
}這些規則由借用檢查器在編譯期強制執行,這就是 Rust 不靠垃圾回收也能達成記憶體安全的方式。整數等簡單型別實作了 Copy,所以是被複製而不是被移動。
Struct、enum 與模式比對
struct 把欄位組合起來,impl 區塊則加入方法。enum 的每個變體都可以攜帶資料,而 match 必須處理每個變體,這由編譯器檢查。標準的 Option<T> enum(Some(value) 或 None)取代了 null。
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}
impl Point {
fn distance(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
enum Shape {
Circle(f64),
Rect { w: f64, h: f64 },
}
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rect { w, h } => w * h,
}
}
fn main() {
let p = Point { x: 3.0, y: 4.0 };
println!("{:?} {}", p, p.distance()); // Point { x: 3.0, y: 4.0 } 5
println!("{}", area(&Shape::Rect { w: 2.0, h: 3.0 })); // 6
let nums = [10, 20];
if let Some(v) = nums.first() {
println!("first is {v}"); // first is 10
}
}trait 定義共用的行為,類似介面:#[derive(Debug)] 請編譯器實作 Debug trait,這樣就能用 {:?} 印出該 struct。
集合:Vec 與 HashMap
use std::collections::HashMap;
fn main() {
let mut nums = vec![3, 1, 2];
nums.push(5);
nums.sort();
println!("{:?} {}", nums, nums.len()); // [1, 2, 3, 5] 4
let evens: Vec<i32> = nums.iter().filter(|n| *n % 2 == 0).map(|n| n * n).collect();
println!("{:?}", evens); // [4]
let mut ages: HashMap<&str, u32> = HashMap::new();
ages.insert("ann", 31);
ages.insert("bob", 25);
*ages.entry("ann").or_insert(0) += 1;
println!("{:?} {:?}", ages.get("ann"), ages.get("zed")); // Some(32) None
}搭配閉包(|n| ...)的迭代器,例如 filter、map 和 collect,編譯後的程式碼與手寫迴圈一樣快。
使用 Result 與 ? 處理錯誤
可能失敗的函式會回傳 Result<T, E>:不是 Ok(value) 就是 Err(error)。? 運算子會提早把錯誤回傳給呼叫端,讓錯誤處理路徑保持簡短。無法復原的錯誤則使用 panic!。
use std::num::ParseIntError;
fn parse_age(text: &str) -> Result<u32, ParseIntError> {
let age: u32 = text.trim().parse()?; // returns Err early on bad input
Ok(age)
}
fn main() {
match parse_age("41") {
Ok(age) => println!("age {age}"), // age 41
Err(e) => println!("error: {e}"),
}
match parse_age("abc") {
Ok(age) => println!("age {age}"),
Err(e) => println!("error: {e}"), // error: invalid digit found in string
}
}完整的小程式:單字頻率統計
將這段程式碼放進 src/main.rs,然後執行 cargo run -- notes.txt。
use std::collections::HashMap;
use std::{env, fs, process};
fn main() {
let path = env::args().nth(1).unwrap_or_else(|| {
eprintln!("usage: words FILE");
process::exit(1);
});
let text = match fs::read_to_string(&path) {
Ok(t) => t.to_lowercase(),
Err(e) => {
eprintln!("cannot read {path}: {e}");
process::exit(1);
}
};
let mut counts: HashMap<&str, usize> = HashMap::new();
for word in text.split(|c: char| !c.is_alphanumeric()).filter(|w| !w.is_empty()) {
*counts.entry(word).or_insert(0) += 1;
}
let mut top: Vec<(&str, usize)> = counts.into_iter().collect();
top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in top.iter().take(5) {
println!("{n:5} {word}");
}
}HashMap 儲存的是向 text 借用的 &str 切片,所以不會複製任何單字;借用檢查器保證 text 的存活時間夠長。
初學者常見錯誤
- 在值被移動之後還繼續使用它。請借用它(
&value),或刻意複製(clone)它。 - 試圖用
s[0]對String做索引。字串是 UTF-8,所以請使用s.chars(),或在字元邊界上使用位元組切片。 - 在持有指向
Vec內部的參考時對它做 push;push 可能重新配置記憶體,所以編譯器會禁止這種做法。 - 搞混
String(擁有所有權)與&str(借用)。函式參數接受&str就能同時支援兩者。
接下來學什麼
閱讀「The Rust Programming Language」(俗稱 the book),可在 doc.rust-lang.org 免費取得,並用 Rust by Example 來看簡短且可執行的範例。接著深入學習 trait 與泛型、生命週期、使用 cargo test 的測試,以及 crates.io 上的套件,例如用於序列化的 serde 與用於非同步程式的 tokio。執行 cargo clippy 可以獲得額外的程式碼檢查建議。
常見問題
Rust 很難學嗎?
最初幾週會比多數語言更困難,因為所有權與借用是全新的概念。熟悉之後,編譯器會及早抓出許多錯誤,許多開發者也覺得 Rust 程式碼更容易維護。
Rust 有垃圾回收嗎?
沒有。記憶體會在擁有者離開作用域時自動釋放,這在編譯期由所有權規則決定。需要共享所有權時,則有參考計數型別 Rc 與 Arc。
什麼是 unsafe Rust?
unsafe 區塊內的程式碼可以做編譯器無法驗證的事,例如解參考原始指標或呼叫 C 函式。它只應少量使用,通常會包裝在安全的 API 之內。
Rust 還是 Go:該學哪一個?
Go 更簡單、學得更快,在網路服務上很受歡迎。Rust 提供更多控制力與效能以及更強的保證,代價是學習曲線更陡。