Rust vs C++:超越記憶體安全的型別革命
為什麼 Rust 是一個革命者:從 C++ 型別安全性中學到的教訓
程式世界中有許多語言興起和衰落,但很少有語言能像 Rust 那樣快速獲得關注。雖然許多人認為 Rust 的記憶體安全性保障是其主要特點,但這個現代語言還有更多值得欣賞的地方。在本文中,我們將探討 Rust 的設計原則如何超越記憶體安全性,創建既直觀又難以誤用的 API,這與 C++ 需要的冗長安全措施形成鮮明對比。
從 Matt Godbolt 的 C++ 見解中學習
著名的 Compiler Explorer 創建者 Matt Godbolt 最近發表了一場題為「透過構造正確:易於使用且難以誤用的 API」的精彩演講。擁有超過 20 年與 C/C++ 工作經驗的開發者們深有同感,他們曾與由易於誤用的 API 所導致的微妙 bug 奮戰過。
觀看 Matt 的報告時,我不斷想到:「沒錯!這就是 Rust 為什麼這麼做的原因。」這場演講無意中強調了 Rust 的設計哲學如何解決 C++ 開發中的許多痛點。讓我們透過一個具體的例子來理解為什麼 Rust 的型別安全性方法對於來自 C/C++ 背景的開發者來說感覺如此革命性。
問題:型別安全和隱式轉換
想像一下用於向股票交易所發送訂單的函式。在 C++ 中,你可能會這樣定義它:
void sendOrder(const char *symbol, bool buy, int quantity, double price)
乍一看,這似乎合理。然而,這個 API 存在幾個潛在的陷阱。最明顯的問題是調用者可能會意外交換數量和價格參數——兩者都是數值型別,而 C++ 將樂於在它們之間進行隱式轉換且不發出警告。在金融系統中,這可能導致災難性錯誤!
bool 參數代表買入/賣出訂單同樣有問題——並不是馬上就能明白哪個值對應於哪個動作。應該讓 "true" 代表買入還是賣出訂單?若沒有文檔,很容易出錯。
C++ 解決方案:型別別名和類別
在 C++ 中,第一個自然的嘗試可能是使用型別別名:
using Price = double; using Quantity = int; void sendOrder(const char *symbol, bool buy, Quantity quantity, Price price) { std::cout << symbol << " " << buy << " " << quantity << " " << price << std::endl; }
不幸的是,這並沒有太大幫助。即使使用了這些型別別名,C++ 還是會在數值類型間進行隱式轉換而不提出抱怨。你可以輕易地以交換價格和數量參數的方式來調用這個函式,而你的代碼會正常編譯甚至不發出警告——即便使用嚴格的編譯器標記如 -Wall -Wextra -Wpedantic
。
更穩健的解決方案需要創建具有顯式構造函式的實際類別:
class Price { public: explicit Price(double price) : m_price(price) {}; double m_price; }; class Quantity { public: explicit Quantity(unsigned int quantity) : m_quantity(quantity) {}; unsigned int m_quantity; };
現在我們有所進展。通過這些包裝類別和顯式構造函式,混淆價格和數量要困難得多。然而,我們還有其他問題需要解決——例如防止在此上下文中不合適的負數量出現。
為了解決這個問題,我們需要使用模板和靜態斷言來編寫更複雜的代碼:
class Quantity { public: templateexplicit Quantity(T quantity) : m_quantity(quantity) { static_assert(std::is_unsigned (), "Please use only unsigned types"); } unsigned int m_quantity; };
這個解決方案有效,但看看我們需要編寫多少代碼才能僅僅防止簡單的參數交換!而且我們尚未解決所有潛在問題,尤其是當處理來自用戶輸入的運行時數據時:
sendOrder("GOOG", false, Quantity(static_cast(atoi("-100"))), Price(1000.00)); // 錯誤
在這種情況下,無論是編譯時或運行時檢查都無法捕捉到負值,導致 -100 變成 4294967196 的溢出——這在生產環境中可能是非常昂貴的 bug!
引入 Rust:設計上的型別安全
現在讓我們看看 Rust 如何處理同樣的問題。這是我們的第一次嘗試:
fn send_order(symbol: &str, buy: bool, quantity: i64, price: f64) { println!("{symbol} {buy} {quantity} {price}"); } fn main() { send_order("GOOG", false, 100, 1000.00); // 正確 send_order("GOOG", false, 1000.00, 100); // 錯誤 }
立刻,Rust 展現了它的強項。嘗試編譯上述代碼,你將會得到明確的錯誤:
error[E0308]: arguments to this function are incorrect --> order/order-1.rs:7:5 | 7 | send_order("GOOG", false, 1000.00, 100); // 錯誤 | ^^^^^^^^^^ ------- --- expected `f64`, found `{integer}` | | | expected `i64`, found `{float}` | note: function defined here --> order/order-1.rs:1:4 | 1 | fn send_order(symbol: &str, buy: bool, quantity: i64, price: f64) { | ^^^^^^^^^^ ------------ --------- ------------- ---------- help: swap these arguments | 7 | send_order("GOOG", false, 100, 1000.00); // 錯誤 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Rust 不僅捕捉錯誤,還建議了解決方案!編譯器清楚地識別出我們交換了參數,並告訴我們如何修正。
為了提供更多型別安全,我們可以用 Rust 創建自定義型別,代碼量非常少:
struct Price(pub f64); struct Quantity(pub u64); fn send_order(symbol: &str, buy: bool, quantity: Quantity, price: Price) { println!("{symbol} {buy} {} {}", quantity.0, price.0); }
注意我們使用 u64(無符號 64 位整數)來表示數量以防止負值。如果嘗試創建負的數量:
send_order("GOOG", false, Quantity(-100), Price(1000.00)); // 錯誤
Rust 立刻報告錯誤:
error[E0600]: cannot apply unary operator `-` to type `u64` --> order/order-4.rs:10:40 | 10 | send_order("GOOG", false, Quantity(-100), Price(1000.00)); // 錯誤 | ^^^^ cannot apply unary operator `-` | = note: unsigned values cannot be negated
那麼從用戶輸入轉換呢?Rust 也強制你正確地處理這個問題:
send_order( "GOOG", false, Quantity("-100".parse::().unwrap()), Price(1000.00), ); // 錯誤
編譯器回應:
error[E0308]: mismatched types --> order/order-6.rs:13:18 | 13 | Quantity("-100".parse::().unwrap()), | -------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `u64`, found `Result ` | | | arguments to this struct are incorrect | = note: expected type `u64` found enum `Result ` note: tuple struct defined here --> order/order-6.rs:2:8 | 2 | struct Quantity(pub u64); | ^^^^^^^^ help: consider using `Result::expect` to unwrap the `Result ` value, panicking if the value is a `Result::Err` | 13 | Quantity("-100".parse:: ().expect("REASON")), | +++++++++++++++++++++++++++++++++++++
Rust 強迫你處理可能失敗的解析過程,而不是默默地接受無效輸入。如果你按照建議添加 .expect(),當解析失敗時你將得到明確的運行時錯誤,這比沉默的數據損壞要好得多。
理解 Rust 的 API 設計優勢
Rust 的強大之處遠不止於記憶體安全性。以下是 Rust 提供的 API 設計主要優勢:
強大但不冗長的型別系統
Rust 的型別系統既具表達性又嚴格,但創建型別安全的 API 並不需要過多的樣板代碼。像我們的 Price 和 Quantity,那樣創建的「新型別」模式既簡潔又有效。
無隱式轉換
與 C++ 不同,Rust 不會在不同數字型別間進行隱式轉換。這防止了意外交換具有不同但兼容類型的參數。
Result 型別的錯誤處理
Rust 的 Result 型別迫使開發者顯式處理潛在的失敗情況,例如解析錯誤。這消除了輸入驗證被意外跳過的整個錯誤類別。
有幫助的編譯器信息
Rust 的編譯器不僅報告錯誤,還解釋為什麼有錯並且通常提供修正建議。這極大地改善了開發人員的體驗。
Rust 對亞洲科技市場的吸引力
對於亞洲快速增長的科技行業的開發者和公司,Rust 提供了令人信服的優勢:
可靠性在關鍵任務系統中的應用
像金融技術、汽車和物聯網這樣的行業——這些都是亞洲科技中心的主要領域——需要極其可靠的系統。Rust 的安全性保障可以幫助預防代價高昂且潛在危險的 bug。
無犧牲的性能
Rust 提供了 C/C++ 级别的性能,而不犧牲安全性,使其理想用於性能至關重要的應用,這在遊戲、嵌入式系統和雲基礎設施中很常見——這些都是亞洲科技公司做出重大投資的領域。
快速發展的生態系統
雖然比 C++ 更新,但 Rust 拥有一个快速发展的生態系統,透過 Cargo 提供強大的包管理,這使其越來越適合在多個領域的生產環境中使用。
緩解開發人員短缺
亞洲各地的公司在聘用經驗豐富的開發人員方面面臨挑戰。Rust 的強大型別系統和有幫助的編譯器可以作為護欄,讓經驗較淺的程式員寫出更安全的代碼。
Rust 初學者:學習曲線的挑戰
Rust 被認為難以學習,尤其是因為它的借用檢查器。雖然這確實對新手構成挑戰,但理解其背後的原因和為何改進值得努力是很重要的:
理解借用檢查器
借用檢查器是 Rust 保證記憶體安全性而不需垃圾收集的機制。它強制關於如何創建和使用數據引用的規則。這通常是新手面臨的最大障礙。
用新的思維方式理解所有權
Rust 要求你明確思考誰擁有數據以及如何共享。這一思維模型不同於垃圾回收語言和 C/C++ 的手動記憶體管理。
所得回報:更少的 bug,更多的信心
一旦理解了 Rust 的所有權系統,你會發現許多常見的 bug 根本不會顯現在你的代碼中,這導致在重構或添加功能時有更多信心。
學習資源
Rust 社群已創建了出色的學習資源,包括《The Book》(Rust 的官方文檔)、交互式教程和支持性的論壇。許多這些資源都有多種語言版本,包括中文、日文和韓文。
結論:超越記憶體安全
這次對 Rust 與 C++ 的比較最引人入勝的是,記憶體安全性——Rust 最著名的特性——甚至不是重點。我們看到了 Rust 的周到語言設計如何防止與型別安全、參數順序和輸入驗證相關的整個錯誤類別。
正如 Matt Godbolt 的 C++ 示例所展示,要在 C++ 中創建真正安全的 API 需要大量努力和紀律。相比之下,Rust 使安全路徑成為默認路徑——要編寫不安全代碼比編寫安全代碼更費力。
對於亞洲充滿活力的科技行業的開發者來說,Rust 代表了一個建立更可靠的系統,減少可能降低維護成本並提高安全性的 bug 的機會。儘管學習曲線確實存在,但其益處不僅限於記憶體安全,還延伸至日常編碼任務。
無論你是在構建金融系統、嵌入式設備還是雲基礎設施,Rust 的 API 設計和型別安全方法都可以幫助你創建不僅僅是記憶體安全且透過構造全面正確的代碼。
留言
張貼留言