Rust 語言自問世以來,已迅速成為系統程式設計領域的重要角色。這門語言結合了低階語言的效能與高階語言的安全性,創造出獨特的開發體驗。作為一位長期關注系統程式設計的技術工作者,玄貓認為 Rust 代表了現代程式設計的一個重要發展方向,特別是在需要兼顧效能與安全性的領域。
Rust 的核心特質與獨特優勢
Rust 的設計哲學可以用三個關鍵字來概括:安全、現代、開放。這些特質共同塑造了 Rust 的獨特定位。
安全性設計
Rust 的最大亮點在於其所有權系統與借用檢查器。這套機制在編譯時就能捕捉大多數記憶體安全問題,同時不需要垃圾收集器的介入。當我第一次深入理解這套系統時,確實被其設計的精妙所震撼。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有權轉移
// 下面這行程式碼不會編譯
// println!("{}", s1); // 錯誤:s1 的值已被移動
}
這段程式碼展示了 Rust 所有權系統的核心概念。當 s1
指定給 s2
時,String 的所有權被轉移,原變數 s1
不再有效。這種機制防止了「雙重釋放」等記憶體錯誤,在編譯時就能發現潛在問題。與 C/C++ 不同,Rust 不允許存取已失效的資料,這大幅減少了記憶體安全漏洞的可能性。
現代語言特性
Rust 融合了許多現代程式語言的優秀特性:
- 強大的型別系統與型別推斷
- 模式比對
- 泛型與特徵系統
- 零成本抽象
- 高效能平行處理
這些特性讓 Rust 在保持高效能的同時,提供了良好的開發體驗。我特別欣賞 Rust 的錯誤處理方式,它強制開發者處理所有可能的錯誤情況,避免了許多潛在的執行時問題。
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
return Err(String::from("除數不能為零"));
}
Ok(a / b)
}
fn main() {
match divide(10, 2) {
Ok(result) => println!("結果: {}", result),
Err(e) => println!("錯誤: {}", e),
}
// 或使用 ? 運算元簡化錯誤處理
fn calculate() -> Result<i32, String> {
let result = divide(10, 2)?;
Ok(result * 2)
}
}
這個範例展示了 Rust 的錯誤處理方式。Result<T, E>
是一個列舉類別,要麼包含成功值 Ok(T)
,要麼包含錯誤 Err(E)
。這強制開發者明確處理錯誤情況,不會像一些語言那樣默忽略錯誤。?
運算元提供了簡潔的錯誤傳播方式,大幅簡化了錯誤處理的程式碼量。這種設計讓程式更加健壯,也使得維護變得更加容易。
開放原始碼社群與生態系統
Rust 是完全開放原始碼的語言,由活躍的社群驅動發展。它擁有優秀的套件管理系統 Cargo,以及豐富的第三方函式庫為「crates」)。經過多年發展,Rust 生態系統已經相當成熟,從網路服務、系統工具到嵌入式開發,都有相應的解決方案。
Rust 與其他語言的比較
相比其他流行的系統程式設計語言,Rust 有著明顯的差異:
相較於 C/C++:Rust 提供了更強的記憶體安全保證,同時保持相近的效能水準。Rust 沒有未定義行為,減少了許多棘手的除錯情境。
相較於 Go:Rust 提供更細粒度的記憶體控制和更高的效能上限,但學習曲線較陡。Go 的平行模型更簡單直觀,而 Rust 的平行安全性由型別系統保證。
相較於 Java/C#:Rust 不需要垃圾收集器,可以應用於更廣泛的場景,包括資源受限的環境。但 Rust 的開發速度可能不及這些語言。
從我的經驗來看,Rust 在效能、安全性和現代語言特性之間取得了絕佳的平衡。它不是萬能的,但在很多場景下確實是理想的選擇。
Rust 的適用場景
Rust 特別適合以下應用場景:
- 系統程式設計:作業系統、驅動程式、嵌入式系統等
- 高效能網路服務:需要低延遲、高平行的後端服務
- 遊戲開發:遊戲引擎和高效能遊戲邏輯
- WebAssembly 應用:跨平台高效能 Web 應用
- 安全關鍵系統:需要高度安全保證的系統
- 跨平台應用程式:需要在多平台上保持一致行為的應用
在這些領域,Rust 的優勢特別明顯。例如,Mozilla 使用 Rust 重寫了 Firefox 的關鍵元件,微軟和 Amazon 也在安全關鍵的基礎設施中採用了 Rust。
Rust 專案管理:使用 Cargo 提升開發效率
Cargo 是 Rust 的官方套件管理器和構建系統,它極大簡化了 Rust 專案的管理。在我多年的 Rust 開發經驗中,Cargo 是讓我持續感到驚艷的工具之一,它將套件管理、構建、測試、檔案生成等功能整合在一起,提供了一站式的專案管理解決方案。
Cargo 基本使用
建立新專案
# 建立可執行程式
cargo new my_app
# 建立函式庫argo new my_lib --lib
這些命令會自動生成專案結構,包括 Cargo.toml
設定檔和基本的程式碼檔案。
構建與執行
# 構建專案
cargo build
# 發布模式構建(最佳化
cargo build --release
# 構建並執行
cargo run
# 僅檢查程式碼,不生成執行檔
cargo check
# 執行測試
cargo test
切換工具鏈
Rust 支援多個版本的工具鏈並存,可以輕鬆切換:
# 使用特定版本的 Rust
cargo +nightly build
# 使用特定版本並啟用實驗性功能
cargo +nightly build -Z unstable-options
相依性管理
Cargo 的相依性管理統簡潔而強大。在 Cargo.toml
中宣告依賴:
[dependencies]
serde = "1.0"
tokio = { version = "1.0", features = ["full"] }
# 開發依賴,只在開發時使用
[dev-dependencies]
criterion = "0.3"
# 構建依賴,用於構建指令碼[build-dependencies]
cc = "1.0"
Cargo 會自動下載和管理這些依賴項。依賴版本遵循語義化版本規範,可以精確控制版本範圍。
Cargo.lock 檔案管理
Cargo.lock
檔案記錄了確切的依賴版本,確保構建的一致性。對於可執行程式,應該將此檔案納入版本控制;對於函式庫需要。
# 更新依賴到最新版本
cargo update
# 更新特定依賴
cargo update -p serde
功能標記(Feature Flags)
功能標記允許條件性地啟用程式碼和依賴項,這對於提供可選功能非常有用:
[features]
default = ["json", "yaml"] # 預設啟用的功能
json = ["serde_json"] # 啟用 json 時也會啟用 serde_json 依賴
yaml = ["serde_yaml"] # 啟用 yaml 時也會啟用 serde_yaml 依賴
postgresql = ["sqlx/postgres"] # 啟用依賴的特定功能
[dependencies]
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.8", optional = true }
sqlx = { version = "0.5", default-features = false, optional = true }
使用功能標記:
# 構建時啟用特定功能
cargo build --features "json postgresql"
# 停用預設功能
cargo build --no-default-features
# 停用預設並啟用特定功能
cargo build --no-default-features --features "yaml"
在程式碼中使用功能標記:
// 只有啟用 json 功能時才編譯此程式碼
#[cfg(feature = "json")]
pub fn parse_json(input: &str) -> Result<Value, Error> {
serde_json::from_str(input)
}
// 可以組合多個功能標記
#[cfg(all(feature = "json", feature = "postgresql"))]
pub fn store_json_to_postgres() {
// ...
}
依賴修補
有時需要臨時修改依賴項的行為,Cargo 提供了幾種修補機制:
[patch.crates-io]
# 使用本地修改版本替代依賴
serde = { path = "../my-serde-fork" }
# 使用 Git 倉函式庫
tokio = { git = "https://github.com/myname/tokio.git", branch = "my-fix" }
對於間接依賴的修補,可以使用重寫規則:
[patch."https://github.com/someorg/somerepo"]
somepackage = { git = "https://github.com/myname/somepackage.git" }
在實際開發中,我發現依賴修補是解決緊急問題的有效手段,但應該視為臨時解決方案。長期來看,最好將修復提交回上游專案。
發布 Crates
當你開發的函式庫好與社群分享時,可以發布到 crates.io:
# 登入 crates.io
cargo login
# 發布 crate
cargo publish
發布前,確保 Cargo.toml
中包含完整的元資料:
[package]
name = "my_awesome_lib"
version = "0.1.0"
authors = ["玄貓 <blackcat@example.com>"]
edition = "2021"
description = "一個超棒的 Rust 函式庫repository = "https://github.com/blackcat/my_awesome_lib"
license = "MIT OR Apache-2.0"
keywords = ["awesome", "rust", "library"]
categories = ["development-tools"]
CI/CD 整合
在持續整合環境中,可以自動化發布流程:
# GitHub Actions 工作流程例
name: Publish
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Publish
run: cargo publish --token ${CRATES_TOKEN}
env:
CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }}
這個工作流程推播標籤時自動發布新版本,適合遵循語義化版本的專案。
連線 C 函式庫Rust 可以無縫地連線 C 函式庫大擴充套件了其應用範圍:
[build-dependencies]
cc = "1.0"
pkg-config = "0.3"
使用 build.rs
指令碼理外部依賴:
// build.rs
fn main() {
// 使用系統安裝的 libpq
pkg_config::probe_library("libpq").unwrap();
// 或者編譯和連線自定義 C
Rust 開發之路:從入門到精通的實用
即使是經驗豐富的開發者,在學習新的程式語言或工具時,也常常會遇到困難和挫折。Rust 作為一門獨特的程式語言,引入了許多概念,即使是有多年程式設計經驗的開發者可能也從未接觸過。
在我多年的 Rust 開發過程中,無論是在專業開發還是社群專案貢獻中,我積累了大量實戰經驗。這篇文章將分享我在這段旅程中學到的關鍵知識,幫助你避開新手常見的陷阱和問題,節省寶貴的學習時間。
Rust 的獨特魅力與挑戰
Rust 語言的設計初衷是提供記憶體安全保證,同時不犧牲執行效能。這種平衡是透過其獨特的所有權系統和嚴格的編譯時檢查實作的。對於初學者來說,這些機制可能會讓人感到受限,但實際上它們是 Rust 最強大的特性之一。
目前 Rust 生態系統正在快速發展。雖然核心語言特性可能不會有太大變化,但實際使用 Rust 可能涉及數百個獨立的函式庫案。透過這篇文章,你將瞭解幫助你在這個不斷發展的生態系統中導航的策略和技巧。
實用導向的技術分享
這篇文章著重於 Rust 的實際應用,同時考慮巨集主題、Rust 及其工具的限制,以及開發者如何能夠快速提高 Rust 的生產力。這不是 Rust 語言的入門,也不能替代官方 Rust 檔案。它旨在補充現有的檔案和資源,提供一些在官方檔案中難以找到的重要經驗。
Rust 工具鏈:高效開發的根本
Cargo:Rust 的專案管理利器
Cargo 是 Rust 的官方套件管理器和建置系統,它極大地簡化了 Rust 開發流程。在我的開發經驗中,Cargo 的強大功能常常被低估,尤其是它的多種子命令和可擴充套件性。
// 建立新的 Rust 專案
$ cargo new my_project
$ cd my_project
// Cargo.toml 檔案結構範例
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
上面的命令和設定展示了 Cargo 的基本用法。cargo new
命令建立一個具有標準結構的新專案。Cargo.toml 是專案的核心設定檔案,定義了專案中繼資料依賴關係。在這個例子中,我們增加了兩個常用的依賴:serde(用於序列化/反序列化)和 tokio(非同步執行時)。透過這種方式,Cargo 讓相依性管理得簡單而強大。
Rust 分析工具:最佳化式碼品質
Rust 提供了豐富的工具鏈,幫助開發者提升程式碼品質和效能。以下是幾個我日常開發中常用的工具:
// 使用 clippy 進行程式碼查
$ cargo clippy
// 使用 rustfmt 格式化程式碼$ cargo fmt
// 使用 cargo-expand 檢視巨集開
$ cargo install cargo-expand
$ cargo expand
這些命令展示了 Rust 工具鏈的多樣性。Clippy 是一個強大的靜態分析工具,可以檢測各種常見錯誤和不佳實踐;rustfmt 確保程式碼風格一致;cargo-expand 則是一個非常有用的工具,可以顯示巨集展開後的程式碼,幫助理解 Rust 的抽象機制。在處理複雜的 Rust 程式碼時,這些工具能夠大大提高開發效率和程式碼品質。
Rust 資料結構與記憶體管理
資料結構選擇的藝術
選擇適當的資料結構對於程式的效能和可維護性至關重要。Rust 標準函式庫了豐富的資料結構,但選擇哪一種往往取決於特定的使用場景。
use std::collections::{HashMap, BTreeMap, HashSet, VecDeque};
fn data_structures_example() {
// 向量(Vector):動態大小的陣列
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
// 雜湊對映(HashMap):鍵值對集合,適合快速查詢
let mut map = HashMap::new();
map.insert("key1", "value1");
map.insert("key2", "value2");
// BTreeMap:有序的鍵值對集合,適合需要排序的情況
let mut btree = BTreeMap::new();
btree.insert(1, "one");
btree.insert(2, "two");
// HashSet:無重複元素的集合
let mut set = HashSet::new();
set.insert("unique_item");
// VecDeque:雙端佇列,適合從兩端增加和移除元素
let mut queue = VecDeque::new();
queue.push_back(1);
queue.push_front(0);
}
上面的程式碼展示了 Rust 標準函式庫個常用資料結構的基本用法。Vec 是最常用的集合類別,適用於大多數需要動態大小陣列的情況;HashMap 提供了近乎 O(1) 的查詢效能,非常適合需要快速查詢的場景;BTreeMap 在需要有序迭代的場景中非常有用;HashSet 適用於需要快速查詢和去重的場景;VecDeque 則在需要高效的雙端操作時很有價值。
在選擇資料結構時,我發現考量操作模式(存取、插入、刪除)、記憶體使用和效能需求是關鍵。例如,如果需要頻繁插入和刪除中間元素,Vec 可能不是最佳選擇;如果需要保持元素有序,BTreeMap 或 BTreeSet 可能比其對應的雜湊版本更適合。
Rust 的記憶體管理模型
Rust 的所有權系統是其最獨特的特性之一,也是許多初學者感到困惑的地方。理解這個系統對於寫出高效與安全的 Rust 程式碼至關重要。
fn ownership_example() {
// 所有權轉移
let s1 = String::from("hello");
let s2 = s1; // s1 的所有權被移動到 s2
// println!("{}", s1); // 錯誤:s1 的值已移動
// 借用(參照)
let s3 = String::from("world");
let len = calculate_length(&s3); // 傳遞參照
println!("The length of '{}' is {}.", s3, len); // s3 仍然有效
// 可變借用
let mut s4 = String::from("hello");
change(&mut s4); // 傳遞可變參照
println!("Changed string: {}", s4);
}
fn calculate_length(s: &String) -> usize {
s.len() // 回傳字串長度,不取得所有權
}
fn change(s: &mut String) {
s.push_str(", world"); // 修改參照的值
}
這段程式碼展示了 Rust 所有權系統的核心概念。當值被賦給另一個變數時(如 s2 = s1
),所有權被轉移,原變數不再可用。為了避免這種情況,可以使用參照(借用)來暫時存取值而不取得所有權。
參照分為不可變參照和可變參照。不可變參照(&String
)允許讀取但不能修改值,與可以同時存在多個;可變參照(&mut String
)允許修改值,但在同一時間只能存在一個,與不能與不可變參照同時存在。
這些規則看似嚴格,但它們是 Rust 能夠在編譯時防止記憶體錯誤的關鍵。透過這種方式,Rust 實作了「零成本抽象」—在提供高階抽象的同時不犧牲執行效能。
Rust 測試方法論
單元測試:確保程式碼正確性
Rust 內建了強大的測試框架,使得編寫和執行測試變得輕而易舉。單元測試是確保程式碼正確性的第一道防線。
// 在 src/lib.rs 或任何模組中
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
#[should_panic(expected = "Negative values not allowed")]
fn test_panic() {
add(-1, 5); // 應該會觸發恐慌
}
}
// 被測試的函式
pub fn add(a: i32, b: i32) -> i32 {
if a < 0 || b < 0 {
panic!("Negative values not allowed");
}
a + b
}
__CODE_BLOCK_21__rust
// 在 tests/ 目錄下的檔案
// tests/integration_test.rs
use my_crate::add;
#[test]
fn test_add_from_integration() {
assert_eq!(add(10, 20), 30);
}
// 模糊測試範例
// 需要安裝 cargo-fuzz: cargo install cargo-fuzz
// fuzz/fuzz_targets/fuzz_target_1.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use my_crate::parse_input;
fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
let _ = parse_input(s);
}
});
__CODE_BLOCK_22__rust
fn main() {
let s1 = String::from("hello"); // s1是字串的所有者
let s2 = s1; // 所有權從s1轉移到s2
// 下面這行會導致編譯錯誤,因為s1已經失去了所有權
// println!("{}", s1);
// 這行正常工作,因為s2現在擁有該字串
println!("{}", s2);
}
__CODE_BLOCK_23__rust
fn main() {
let s1 = String::from("hello");
// 不可變借用,可以有多個
let r1 = &s1;
let r2 = &s1;
println!("{} and {}", r1, r2);
// 可變借用,同一時間只能有一個
let mut s2 = String::from("world");
let r3 = &mut s2;
r3.push_str("!");
println!("{}", r3);
// 在可變借用存在的情況下,不能再有其他借用
// 下面的程式碼會導致編譯錯誤
// let r4 = &s2;
// println!("{}", r4);
}
__CODE_BLOCK_24__rust
// 'a是一個生命週期引數,表示參照的有效期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
// 結果的生命週期受限於string2的生命週期
result = longest(string1.as_str(), string2.as_str());
} // string2在這裡離開作用域
// 下面這行會導致編譯錯誤,因為result參照了已經失效的string2
// println!("The longest string is {}", result);
}
在這個例子中,longest
函式回傳的參照必須與輸入引數的生命週期相關聯。生命週期標註'a
告訴編譯器,回傳的參照的有效期不會超過輸入參照的有效期。
當我們嘗試在string2
已經離開作用域後使用result
時,編譯器會檢測到這個錯誤並拒絕編譯程式。這種機制防止了懸垂參照,確保所有參照都指向有效的記憶體。
與其他程式語言的比較
為了更好地理解Rust的獨特性,讓我們將其與一些常見的程式語言進行比較:
Rust vs. C/C++
- 記憶體安全:Rust在編譯時強制執行記憶體安全,而C/C++依賴程式設計師正確管理記憶體。
- 並發安全:Rust的類別系統確保並發安全,防止資料競爭,而C/C++中並發程式設計更容易出錯。
- 零成本抽象:兩者都提供零成本抽象,但Rust的抽象通常更安全。
- 學習曲線:Rust的所有權和借用概念可能需要更長時間掌握。
Rust vs. Java/C#
- 記憶體管理:Java和C#使用垃圾收集器自動管理記憶體,而Rust使用所有權系統在編譯時管理記憶體。
- 效能:Rust通常比Java和C#更高效,因為它沒有垃圾收集器的開銷。
- **並
Rust 的核心安全機制:借用檢查器
記憶體安全:Rust 的核心競爭力
記憶體安全是 Rust 語言的標誌性特徵,也是它與其他程式語言最大的區別所在。Rust 能夠提供強大的安全保證,這主要歸功於一個稱為「借用檢查器」(borrow checker) 的獨特功能。
在 C 和 C++ 等語言中,記憶體管理基本上是一個手動過程,開發人員必須時刻關注實作細節來處理記憶體。而 Java、Go 和 Python 等語言則使用自動記憶體管理(又稱垃圾回收),這種方式雖然簡化了記憶體分配和管理的細節,但代價是帶來一定的效能開銷。
Rust 採取了一條獨特的路徑:借用檢查器在編譯時驗證參考的有效性,而不是在執行時進行參考計數或垃圾回收。這是 Rust 獨有的特性,也是初學者編寫 Rust 程式時面臨的主要挑戰之一。
借用檢查器如何運作
借用檢查器是 Rust 編譯器 rustc
的一部分,它確保對於任何給定的物件或變數,同一時間內最多隻能有一個可變參考(mutable reference)。雖然可以有多個不可變參考(immutable reference,即唯讀參考),但絕不能同時擁有多個活動中的可變參考。
Rust 透過確保可變參考和不可變參考永不重疊來保證記憶體安全。下圖展示了這一核心規則:
+---------------------------+ +---------------------------+
| | | |
| 只能有一個可變參考 | | 可以有多個不可變參考 |
| &mut T | | &T |
| | | |
+---------------------------+ +---------------------------+
不可同時存在
Rust 使用「資源取得即初始化」(Resource Acquisition Is Initialization,RAII) 原則來追蹤變數及其所有參考何時進入和離開作用域。一旦它們離開作用域,記憶體就可以被釋放。借用檢查器不允許參考已離開作用域的變數,並且只允許一個可變參考或多個不可變參考,但絕不允許兩者同時存在。
借用檢查器與並發安全
借用檢查器對於並發程式設計同樣提供了安全保障。當在不同執行緒間分享資料時,常見的競態條件根本原因通常是相同:同時存在分享的可變參考。在 Rust 中,不可能同時存在多個可變參考,這確保了資料同步問題能被避免,或至少不會被無意中建立。
初學時,Rust 的借用檢查器確實有點難以掌握,但很快你就會發現它是 Rust 最出色的特性。類別於 Haskell 這樣的語言,一旦你的程式碼能夠編譯透過,這通常就足以保證你的程式能正常工作與永不當機(當然,還需要適當的測試)。雖然有例外情況,但總體而言,用 Rust 編寫的程式不會因為常見的記憶體錯誤而當機,例如讀取超出緩衝區末尾的資料,或錯誤處理記憶體分配和釋放。
Rust:現代程式設計的代表
現代程式設計正規化的支援
Rust 語言開發者特別注重支援現代程式設計正規化。從其他語言轉過來的開發者可能會注意到 Rust 的「拋棄舊有,擁抱新生」的方法。Rust 在很大程度上避開了物件導向程式設計等正規化,而更偏向於特徵(traits)、泛型(generics)和函式式程式設計。
Rust 特別強調以下正規化和功能:
- 函式式程式設計 - 閉包、匿名函式和迭代器
- 泛型程式設計 - 提供強大的抽象能力
- 特徵系統 - 在其他語言中有時被稱為介面
- 生命週期 - 用於處理參考
- 元程式設計 - 透過其巨集統實作
- 非同步程式設計 - 透過 async/await 語法
- *套件和相依性管理 - 透過 Cargo 工具
- 零成本抽象 - 抽象不帶來執行時間成本
傳統的物件導向特性在 Rust 中明顯缺席。雖然你可以在 Rust 中模擬類別於類別繼承的模式,但術語不同,與 Rust 更傾向於函式式程式設計。對於來自物件導向背景的開發者(如 C++、Java 或 C# 開發者),可能需要一些時間來適應。但一旦適應了新模式,許多程式設計師會發現,擺脫物件導向意識形態的嚴格限制,帶來了一種愉悅和自由感。
Rust 的純開放原始碼特性
在考慮構建專案的語言和平台時,社群治理對於任何專案的長期維護都是重要考量因素。一些雖然開放原始碼但主要由大公司管理的語言和平台,如 Go(Google)、Swift(Apple)和 .NET(Microsoft),帶來了某些風險,例如可能做出有利於其產品的技術決策。
Rust 是一個社群驅動的專案,主要由非營利的 Mozilla 基金會長官。Rust 程式語言本身採用 Apache 和 MIT 雙重授權。Rust 生態系統中的各個專案都有各自的授權,但大多數關鍵元件和函式庫在於開放原始碼授權下,如 MIT 或 Apache。
大型科技公司對 Rust 有強烈支援。Amazon、Facebook、Google、Apple、Microsoft 等公司都已計劃使用或承諾支援 Rust。由於不繫結於任何特定實體,Rust 是一個良好的長期選擇,具有最小的潛在利益衝突。 Rust 團隊在官方網站上維護了一份生產環境使用者列表:https://www.rust-lang.org/production
Rust 與其他主流語言的比較
以下表格雖然不夠全面,但提供了 Rust 與其他流行程式語言之間的差異摘要:
語言 | 程式設計正規化 | 型別系統 | 記憶體模型 | 關鍵特性 |
---|---|---|---|---|
Rust | 並發、函式式、泛型、命令式 | 靜態、強型別 | RAII、顯式 | 安全性、效能、非同步 |
C | 命令式 | 靜態、弱型別 | 顯式 | 效率、可移植性、低階記憶體管理、廣泛支援 |
C++ | 命令式、物件導向、泛型、函式式 | 靜態、混合 | RAII、顯式 | 效率、可移植性、低階記憶體管理、廣泛支援 |
C# | 物件導向、命令式、事件驅動、函式式、反射、並發 | 靜態、動態、強型別 | 垃圾回收 | Microsoft 平台支援、大型生態系統、進階語言特性 |
JavaScript | 原型、函式式、命令式 | 動態、鴨子型別、弱型別 | 垃圾回收 | 廣泛支援、非同步 |
Java | 泛型、物件導向、命令式、反射 | 靜態、強型別 | 垃圾回收 | 根據位元組碼、生產級 JVM、廣泛支援、大型生態系統 |
Python | 函式式、命令式、物件導向、反射 | 動態、鴨子型別、強型別 | 垃圾回收 | 直譯式、高度可移植、廣泛使用 |
Ruby | 函式式、命令式、物件導向、反射 | 動態、鴨子型別、強型別 | 垃圾回收 | 語法優雅、一切皆表示式、簡單並發模型 |
TypeScript | 函式式、泛型、命令式、物件導向 | 靜態、動態、鴨子型別、混合 | 垃圾回收 | 型別系統、JavaScript 相容性、非同步 |
何時應該使用 Rust?
Rust 是一種系統程式設計語言,通常用於較低階的系統程式設計,適用於類別於 C 或 C++ 的場景。如果你想最佳化發者生產力,Rust 可能不是最合適的選擇,因為與 Go、Python、Ruby 或 Elixir 等流行語言相比,編寫 Rust 通常更具挑戰性。
隨著 WebAssembly (Wasm) 的興起,Rust 也成為網頁程式設計的絕佳選擇。你可以用 Rust 構建應用程式和函式庫 Wasm 編譯它們,並同時享受 Rust 安全模型的優勢和網頁的可移植性。
Rust 沒有特定的使用案例——你應該在合適的場景中使用它。玄貓個人曾將 Rust 用於許多小型一次性專案,僅僅是因為編寫 Rust 程式碼愉快,而與一旦程式碼譯成功,通常可以確信它能正常工作。透過正確使用 Rust 編譯器和工具,你的程式碼錯或行為不確定的可能性大大降低——這對任何專案都是理想的特性。
選擇正確的工具對於任何專案的成功都至關重要,但要知道哪些工具是正確的,你必須先獲得經驗。Rust 的學習曲線可能較陡,但一旦掌握,它提供的安全保證和效能優勢可以大大提高系統程式的品質和可靠性。
Rust 借用檢查器的實際應用
借用檢查器雖然增加了學習曲線,但它在實際開發中能有效防止許多常見的程式錯誤。讓我們看一個簡單例子來理解借用檢查器的運作方式:
fn main() {
let mut data = vec![1, 2, 3];
// 建立一個可變參考
let ref1 = &mut data;
// 嘗試同時建立另一個可變參考 - 這會導致編譯錯誤
// let ref2 = &mut data;
// 修改資料
ref1.push(4);
// 可變參考使用結束後,可以建立新的參考
let ref3 = &data;
let ref4 = &data; // 多個不可變參考是允許的
println!("Data: {:?}", ref3);
}
上面的程式碼展示了 Rust 借用檢查器的基本規則。我們首先建立一個可變向量 data
,然後建立一個可變參考 ref1
。依據借用規則,此時不能再建立另一個可變參考(被註解掉的那行),因為同一時間只能有一個可變參考。
當我們使用 ref1
完成資料修改後,可以建立多個不可變參考(ref3
和 ref4
)來讀取資料。這種機制在編譯時就能防止資料競爭,是 Rust 記憶體安全的核心。
在實際專案中,這種安全機制尤其在並發程式設計中顯得極為重要。由於借用檢查器的存在,Rust 能夠在編譯時就防止許多在其他語言中只能在執行時檢測到的並發問題。
Rust 的生態系統與實用性
Rust 不僅僅是一種語言,它還擁有強大的生態系統和工具鏈,使其成為實際專案開發的理想選擇:
- Cargo - Rust 的套件管理器和構建工具,簡化了相依性管理專案構建流程
- Rustup - 版本管理工具,讓開發者輕鬆在不同 Rust 版本間切換
- **C
Cargo:Rust生態系統的核心引擎
Rust作為一門現代系統程式設計語言,其成功不僅歸功於語言本身的設計,還有賴於圍繞它建立的強大工具鏈。在這個工具生態系統中,Cargo作為Rust的官方套件管理工具,扮演著核心角色。玄貓在多年的Rust開發經驗中發現,掌握Cargo不僅能加速開發流程,還能確保專案的穩定性和可維護性。
在深入技術細節之前,我想強調工具掌握的重要性。許多開發者急於學習語言本身,卻忽略了工具的價值。然而,工具熟練度往往是區分初級和高階開發者的關鍵因素。正如木匠需要熟悉他的鋸子和鑿子一樣,Rust開發者需要精通Cargo的各種功能。
Cargo是Rust官方的套件管理工具,它不僅僅是一個簡單的相依性管理,更是連線Rust編譯器(rustc)、crates.io套件函式庫其他Rust工具的橋樑。雖然技術上可以不使用Cargo而直接使用rustc,但這就像放棄自動擋開手動車一樣,會讓開發過程變得不必要的複雜。
接下來,讓我們一起探索Cargo的核心功能,從基本使用到進階技巧,看看它如何改變我們的Rust開發體驗。
Cargo基礎導覽
要了解Cargo的強大功能,最好的方式就是親身實踐。讓我們從Cargo的基本指令開始,逐步探索它的各種功能。
基本指令概覽
首先,我們可以透過執行cargo help
來檢視所有可用的指令:
$ cargo help
Rust的套件管理器
用法:
cargo [+toolchain] [選項] [子指令]
選項:
-V, --version 顯示版本資訊並結束
--list 列出已安裝的指令
--explain <程式碼 執行 `rustc --explain 程式碼
-v, --verbose 使用詳細輸出 (-vv 非常詳細/build.rs輸出)
-q, --quiet 不在stdout輸出任何內容
--color <WHEN> 著色: auto, always, never
--frozen 要求Cargo.lock和快取是最新的
--locked 要求Cargo.lock是最新的
--offline 在不存取網路的情況下執行
-Z <標誌>... 不穩定(僅限nightly)的Cargo標誌,詳情見'cargo -Z help'
-h, --help 顯示幫助訊息
一些常用的cargo指令包括(使用--list檢視所有指令):
build, b 編譯當前套件
check, c 分析當前套件並報告錯誤,但不生成目標檔案
clean 移除target目錄
doc 構建此套件及其依賴項的檔案
new 建立新的cargo套件
init 在現有目錄中建立新的cargo套件
run, r 執行本地套件的二進位檔案或範例
test, t 執行測試
bench 執行基準測試
update 更新Cargo.lock中列出的依賴項
search 在registry中搜尋crates
publish 封裝並將此套件上載到registry
install 安裝Rust二進位檔案。預設位置是$HOME/.cargo/bin
uninstall 解除安裝st二進位檔案
使用'cargo help <指令>'取得有關特定指令的更多訊息。
這些指令涵蓋了Rust開發週期的各個方面,從建立新專案到最終釋出。實際輸出可能因Cargo版本不同而略有差異。
建立新的應用程式或函式庫Cargo內建了樣板生成器,可以快速建立"Hello, world!“應用程式或函式庫省初始設定時間。在你的開發目錄中執行以下指令:
$ cargo new dolphins-are-cool
Created binary (application) `dolphins-are-cool` package
這個指令會建立一個名為dolphins-are-cool
的新應用程式樣板(當然,你可以使用任何你喜歡的名稱)。讓我們檢查一下生成的檔案結構:
$ cd dolphins-are-cool/
$ tree
.
├── Cargo.toml
└── src
└── main.rs
Cargo建立了一個精簡但完整的專案結構:
Cargo.toml
:專案設定檔,包含套件中繼資料依賴訊息src/
目錄:包含原始碼main.rs
:應用程式的入口點
讓我們看看Cargo.toml
的內容:
[package]
name = "dolphins-are-cool"
version = "0.1.0"
edition = "2021"
[dependencies]
這個檔案定義了專案的基本訊息:
name
:套件名稱version
:版本號,遵循語義化版本規範edition
:使用的Rust版本(2021是最新的穩定版)dependencies
:專案依賴項(目前為空)
再來看看src/main.rs
:
fn main() {
println!("Hello, world!");
}
這是一個簡單的"Hello, world!“程式。讓我們執行它:
$ cargo run
Compiling dolphins-are-cool v0.1.0 (/path/to/dolphins-are-cool)
Finished dev [unoptimized + debuginfo] target(s) in 0.80s
Running `target/debug/dolphins-are-cool`
Hello, world!
cargo run
指令編譯並執行程式。Cargo在首次編譯時會建立一個target/
目錄,用於存放編譯產物。
建立函式庫
如果要建立函式庫應用程式,可以使用--lib
選項:
$ cargo new rust-utils --lib
Created library `rust-utils` package
函式庫的結構略有不同:
rust-utils/
├── Cargo.toml
└── src
└── lib.rs
lib.rs
取代了main.rs
,成為函式庫口點:
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
函式庫不僅包含一個範例函式,還包含一個簡單的測試。這體現了Rust對測試的重視,鼓勵開發者從一開始就進行測試驅動開發。
管理專案依賴
在實際開發中,幾乎所有專案都需要依賴其他套件。Cargo使相依性管理得簡單而強大。
增加依賴
假設我們想在專案中增加一個用於處理命令列引數的套件,如clap
。我們有幾種方式可以實作:
- 手動編輯
Cargo.toml
:
[dependencies]
clap = "4.3.0"
- 使用
cargo add
指令(需要安裝cargo-edit工具):
$ cargo add clap
Adding clap v4.3.0 to dependencies.
增加依賴後,下次執行cargo build
或cargo run
時,Cargo會自動下載並編譯這些依賴。
依賴版本控制
Cargo使用語義化版本控制(SemVer)來管理依賴版本。在Cargo.toml
中,你可以用多種方式指定版本要求:
[dependencies]
# 與4.3.0完全相同的版本
exact_version = "=4.3.0"
# 任何4.3.x版本(>=4.3.0, <4.4.0)
caret_version = "^4.3.0" # 或者簡寫為 "4.3.0"
# 任何4.x.y版本(>=4.0.0, <5.0.0)
compatible_version = "~4.0.0"
# 大於等於1.0.0的版本
minimum_version = ">=1.0.0"
# 複雜的版本要求
complex_version = ">1.0.0, <2.0.0"
# 最新版本(不推薦用於生產環境)
latest_version = "*"
# 從特定Git儲存取得
git_dependency = { git = "https://github.com/username/repo" }
# 從本地路徑取得
path_dependency = { path = "../local-package" }
__CODE_BLOCK_39__toml
[dependencies]
clap = { version = "4.3.0", features = ["derive"] }
tokio = { version = "1.28.1", features = ["full"] }
__CODE_BLOCK_40__toml
[features]
default = ["feature1"]
feature1 = []
feature2 = ["dep:optional-dependency"]
all = ["feature1", "feature2"]
[dependencies]
optional-dependency = { version = "1.0", optional = true }
__CODE_BLOCK_41__bash
# 開發模式構建(未最佳化包含除錯訊息)
$ cargo build
# 發布模式構建(最佳化移除除錯訊息)
$ cargo build --release
# 檢查程式碼不生成二進位檔案
$ cargo check
__CODE_BLOCK_42__bash
# 開發模式執行
$ cargo run
# 發布模式執行
$ cargo run --release
# 帶引數執行
$ cargo run -- --help
__CODE_BLOCK_43__bash
# 執行所有測試
$ cargo test
# 執行特定測試
$ cargo test test_name
# 執行包含特定字元串的測試
$ cargo test part_of_name
# 顯示測試輸出
$ cargo test -- --nocapture
__CODE_BLOCK_44__rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_addition() {
assert_eq!(add(2, 2), 4);
}
#[test]
#[should_panic(expected = "溢位")]
fn test_overflow() {
add(usize::MAX, 1);
}
}
__CODE_BLOCK_45__rust
/// 將兩個數字相加
///
/// # 範例
///
/// __CODE_BLOCK_46__
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
cargo test
會自動執行這些檔案測試,確保檔案中的範例始終可用與正確。
檔案生成
Cargo可以從程式註解中生成HTML檔案:
# 生成本地檔案
$ cargo doc
# 生成檔案並開啟瀏覽器
$ cargo doc --open