Rust 的所有權和借用系統在編譯時期就確保了記憶體安全,但某些底層操作或與外部系統互動時,仍需藉助 unsafe
關鍵字。理解 unsafe
的使用時機和潛在風險至關重要,才能在效能和安全性之間取得平衡。本文除了介紹 Cow
、im
、rpds
等實作不可變資料結構的方式外,也深入剖析了 unsafe
的應用場景和注意事項,例如與 C 函式庫互動、高效能運算以及底層資料結構的操作,並提供程式碼範例和最佳實踐建議,幫助開發者更安全有效地使用 unsafe
。
不變性(Immutability)深度解析與實務應用
在軟體開發領域中,不變性(Immutability)是一項強大的抽象概念,能夠顯著提升程式碼的可靠性和可維護性。本章將探討不變性的原理、實務應用以及如何在 Rust 中有效地利用不變性來編寫更高品質的程式碼。
瞭解 Cow
(Copy-on-Write)機制
Cow
(Copy-on-Write)是 Rust 中一個重要的智慧指標型別,它允許開發者在需要時才進行資料的複製。以下是一個使用 Cow
實作不可變清單(CowList)的範例:
use std::borrow::Cow;
#[derive(Debug, Clone)]
struct CowList<'a> {
cows: Cow<'a, Vec<String>>,
}
impl<'a> CowList<'a> {
fn new() -> Self {
CowList {
cows: Cow::from(Vec::new()),
}
}
fn add_cow(mut self, cow: &str) -> Self {
self.cows.to_mut().push(cow.to_string());
self
}
}
impl Default for CowList<'_> {
fn default() -> Self {
CowList::new()
}
}
fn main() {
let list_of_cows = CowList::default()
.add_cow("Bessie")
.add_cow("Daisy")
.add_cow("Moo");
dbg!(&list_of_cows);
let list_of_cows_plus_one = list_of_cows.add_cow("Penelope");
dbg!(&list_of_cows);
dbg!(&list_of_cows_plus_one);
}
內容解密:
- 我們定義了一個名為
CowList
的結構體,其中包含一個Cow
包裝的Vec
。 add_cow
方法會在需要時複製底層的Vec
,然後新增新的元素。- 這種實作方式確保了原始的
CowList
不變,同時傳回了一個新的CowList
。
使用外部套件實作不可變資料結構
Rust 社群提供了多個優秀的套件來簡化不可變資料結構的實作,例如 im
和 rpds
。
使用 im
套件
im
套件提供了一系列為不可變性最佳化的資料結構,包括向量、集合和對映。
use im::vector;
fn main() {
let shopping_list = vector!["milk", "bread", "butter", "cheese", "eggs"];
let mut updated_shopping_list = shopping_list.clone();
updated_shopping_list.push_back("grapes");
dbg!(&shopping_list);
dbg!(&updated_shopping_list);
}
內容解密:
- 我們使用
vector!
巨集建立了一個不可變向量shopping_list
。 - 克隆
shopping_list
以建立一個可變的副本updated_shopping_list
。 - 對
updated_shopping_list
進行修改,不會影響原始的shopping_list
。
使用 rpds
套件
rpds
套件提供了更多的不可變資料結構,包括佇列和堆積疊。
use rpds::Vector;
fn main() {
let streets = Vector::new()
.push_back("Elm Street")
.push_back("Maple Street")
.push_back("Oak Street");
let updated_streets = streets.push_back("Pine Street");
dbg!(&streets);
dbg!(&updated_streets);
}
內容解密:
- 我們使用
Vector::new()
建立了一個不可變向量streets
。 - 連續呼叫
push_back
方法來新增元素,每次都傳回一個新的向量。 - 這種鏈式呼叫的方式簡潔明瞭,且保持了不可變性。
不變性的優勢與應用場景
- 邏輯錯誤預防:不可變資料結構可以有效防止意外的資料修改,減少邏輯錯誤。
- 平行程式設計:不可變性使得資料可以在多個執行緒之間安全分享,無需額外的同步機制。
- 程式碼可讀性與可測試性:純函式和參照透明性使得程式碼更容易理解和測試。
第10章:反模式(Antipatterns)
在軟體開發領域中,反模式是指那些在特定情況下或在所有情況下都被視為有害的程式設計實踐。這些反模式往往源於對語言的誤解或缺乏特定技術堆疊的經驗。在本章中,我們將探討Rust中一些常見的反模式以及如何避免它們。
10.1 什麼是反模式?
反模式這個術語有點像是一個模糊的詞語,經常被貶義地用來指代任何演講者不喜歡的做法。最終,反模式的定義是一個見仁見智的問題。然而,在某些情況下,一種做法客觀上是糟糕的,例如當它不安全、低效或難以維護時。這些情況很可能是由於糟糕的設計、維持向後相容性的願望以及不斷變化的可接受軟體設計環境所致。
C語言提供了一個很好的案例研究,展示了語言設計實踐如何演變。C語言可以說是歷史上最具影響力的程式語言。儘管它無處不在,但C語言在安全性和易用性方面也可以說是最糟糕的語言之一,特別是在系統程式設計中。即使是高技能的專家也很容易在C語言中犯下難以檢測和糾正的錯誤。
Rust語言經過仔細和深思熟慮的設計,以避免像C語言這樣的語言中的陷阱。Rust還試圖透過與C一樣快(如果不是更快的話)來預先阻止贊成C的一個令人信服的論點。在許多基準測試中,Rust在原始速度方面優於C,並且在沒有使用unsafe
程式碼的情況下實作了這一點。
即使是Rust,也已經成為其自身成功的犧牲品,因為它的流行使得進行重大更改變得困難。那些會破壞向後相容性的更改很難被論證和實施,因為重寫現有程式碼的成本足夠高,以至於人們會避免升級(這是一個C語言和其他語言已經存在了幾十年的問題)。
將Rust與C進行比較有點不公平,因為Rust之所以成為可能,是因為編譯器基礎設施(即LLVM專案;https://llvm.org)的顯著改進,以及對程式語言設計的更好理解,而不是我們在1970年代所擁有的。我們今天認為是C語言中的反模式,在1970年代可能是一種最佳實踐。Rust的酷之處在於,只要你避免使用unsafe
程式碼區塊,編譯器就會為你完成大部分工作。這並不一定適用於像C這樣的語言,其中編譯器是一種有些鈍的工具,並且將許多最佳化留給了程式設計師。(嚴格來說,Clang和GCC都對C程式碼進行了出色的最佳化。)
10.2 使用unsafe
Rust中最主要的反模式是不當使用危險的unsafe
關鍵字。你需要在Rust中做很多事情時使用unsafe
關鍵字,但它也是自殘的最佳方式。你可以將unsafe
視為一個逃生艙,允許你執行違反Rust語言規則的操作,例如使用原始指標、呼叫C函式以及存取或修改程式分配的記憶體空間之外的資源。在絕大多數的Rust使用案例中,你不應該需要unsafe
關鍵字,你應該仔細檢查任何對它的使用。
話雖如此,幾乎不可能在不使用unsafe
程式碼的情況下使用Rust,至少是間接的,因為標準函式庫在整個過程中都使用了unsafe
程式碼。你不必在標準函式庫的程式碼中尋找unsafe
的使用,例如在Box
、Vec
和String
的實作中。記憶體分配和釋放、作業系統系統呼叫和其他低階操作也是不安全的操作。標準函式庫中的許多unsafe
程式碼範例要麼是最佳化,要麼是無法以其他方式安全執行的必要操作(C風格的外國函式介面[FFI]、系統呼叫等)。例如,Vec::insert()
方法的實作包括以下清單中顯示的不安全程式碼區塊,它提供了向量內插入的最佳化實作。
pub fn insert(&mut self, index: usize, element: T) {
#[cold]
#[cfg_attr(not(feature = "panic_immediate_abort"), inline(never))]
#[track_caller]
fn assert_failed(index: usize, len: usize) -> ! {
panic!("insertion index (is {index}) should be <= len (is {len})");
}
let len = self.len();
// 為新元素留出空間
if len == self.buf.capacity() {
self.reserve(1);
}
unsafe {
// 不會失敗
// 放置新值的位置
{
let p = self.as_mut_ptr().add(index);
if index < len {
// 將所有元素向右移動以騰出空間。(將第index個元素複製到連續的兩個位置。)
ptr::copy(p, p.add(1), len - index);
} else if index == len {
// 無需移動元素。
} else {
// ...
內容解密:
此段程式碼展示了Vec::insert()
方法如何在向量中插入新元素。首先,它檢查向量是否已滿,如果是,則呼叫reserve()
方法來分配更多的記憶體。然後,它使用unsafe
區塊來執行實際的插入操作。在這個區塊中,它首先取得向量緩衝區的可變指標,並將其移動到插入索引的位置。如果插入索引小於向量的長度,它會將該索引之後的所有元素向右移動一位,以騰出空間給新元素。如果插入索引等於向量的長度,則不需要移動任何元素。
graph LR; A[開始插入] --> B{檢查向量是否已滿}; B -->|是| C[分配更多記憶體]; B -->|否| D[直接插入]; C --> D; D --> E[取得可變指標]; E --> F[移動元素]; F --> G[插入新元素];
圖表翻譯: 此圖表展示了Vec::insert()
方法插入新元素的基本流程。首先檢查向量是否已滿,如果是,則分配更多記憶體。然後取得可變指標,並根據需要移動元素以騰出空間,最後插入新元素。
本章討論了Rust中的反模式,包括不當使用unsafe
關鍵字等常見問題,並提供了相關範例和解析。透過瞭解這些反模式,開發者可以更好地避免它們,寫出更安全、更高效、更易於維護的Rust程式碼。
使用 Unsafe 的必要性與應用場景
在 Rust 程式語言中,unsafe
關鍵字扮演著舉足輕重的角色。它允許開發者在特定的情況下繞過 Rust 的安全檢查,從而實作某些特殊的功能或最佳化效能。本章節將探討 unsafe
的使用場景、其背後的原理以及相關的最佳實踐。
為何需要 Unsafe?
Rust 以其嚴格的記憶體安全保證而聞名,但某些情況下,這些保證會成為實作特定功能或與其他語言互動的障礙。unsafe
提供了一種機制,讓開發者可以在需要時放寬這些限制。
Unsafe 的主要功能
- 解參照原始指標:允許直接操作記憶體地址。
- 呼叫 Unsafe 函式或方法:某些函式或方法被標記為
unsafe
,因為它們可能違反 Rust 的安全規則。 - 存取或修改可變靜態變數:靜態變數在 Rust 中是特殊的,修改它們需要
unsafe
。 - 實作 Unsafe 特徵:某些特徵可能包含
unsafe
方法。 - 存取 Union 型別的欄位:Union 是為了與 C 語言相容而提供的資料結構。
實際應用場景
在實際開發中,unsafe
最常被用於以下場景:
- 與 C 函式庫互動:許多系統呼叫和第三方函式庫都是用 C 語言寫的,直接呼叫這些函式庫需要使用
unsafe
。 - 高效能運算:在某些效能關鍵的程式碼中,使用
unsafe
可以繞過 Rust 的安全檢查,直接進行低階的記憶體操作或使用特定的硬體指令。 - 實作底層資料結構:某些資料結構或演算法可能需要直接操作記憶體,這時就需要使用
unsafe
。
使用 Unsafe 的範例
以下是一個簡單的範例,展示如何使用 unsafe
呼叫 C 語言的 printf
函式:
extern crate libc;
use libc::printf;
unsafe fn unsafe_function() {
printf("Hello from Rust using C's printf()!\n\0".as_ptr() as *const i8);
}
fn main() {
unsafe {
unsafe_function();
}
}
程式碼解析
extern crate libc;
:引入libc
套件,它提供了 Rust 與 C 語言標準函式庫之間的介面。use libc::printf;
:匯入printf
函式。unsafe fn unsafe_function()
:定義一個unsafe
函式,因為它呼叫了 C 語言的printf
函式。printf("Hello...!\n\0".as_ptr() as *const i8);
:呼叫printf
,注意字串結尾的\0
是 C 風格字串的結束符號。fn main() { unsafe { unsafe_function(); } }
:在main
函式中,使用unsafe
區塊呼叫unsafe_function
。
最佳實踐
雖然 unsafe
提供了很大的彈性,但使用它時必須格外小心。以下是一些最佳實踐:
- 最小化 Unsafe 程式碼:盡量將
unsafe
程式碼限制在最小的範圍內,並提供安全的抽象介面給其他程式碼使用。 - 詳細註解:在使用
unsafe
時,應該提供詳細的註解,解釋為什麼需要使用unsafe
以及它是如何被安全使用的。 - 測試:對於包含
unsafe
的程式碼,應該進行徹底的測試,以確保它在各種情況下都能正確運作。