Rust 的核心特性之一,就是其獨特的所有權系統,賦予了編譯器在編譯時期檢查記憶體安全的能力,避免了常見的記憶體錯誤,例如懸空指標和資料競爭。所有權規則確保每個值都只有一個所有者,並在所有者超出作用域時自動釋放資源。Rust 的移動語義不同於複製,它會轉移所有權而非複製資料,除非是基本型別。深複製則需要明確使用 clone() 方法。借用和參照允許在不取得所有權的前提下存取資料,並區分可變和不可變借用,進一步提升了記憶體安全性。智慧指標 Box 則用於在堆積上分配記憶體,搭配 Option 使用能有效處理空值情況,避免潛在的空指標錯誤。try_new() 方法則提供更安全的記憶體分配方式,能有效處理記憶體不足的狀況。
Rust 記憶體管理與所有權機制
Rust 的所有權機制是其安全保證的核心,編譯器的借用檢查器負責強制執行一組簡單的所有權規則。每個值都有一個所有者,一次只能有一個所有者;當所有者超出範圍時,該值將被丟棄。
瞭解所有權:複製、借用、參照和移動
Rust 的所有權語義在某些方面與 C、C++ 和 Java 相似,但 Rust 沒有複製建構函式的概念,且很少與原始指標互動。當你將一個變數的值賦給另一個變數(即 let a = b;)時,這被稱為移動,即所有權的轉移(且一個值只能有一個所有者)。移動不會建立副本,除非你正在指定基本型別(即將整數指定給另一個值會建立副本)。
fn main() {
let mut top_grossing_films = vec!["Avatar", "Avengers: Endgame", "Titanic"];
let top_grossing_films_mutable_reference = &mut top_grossing_films;
top_grossing_films_mutable_reference.push("Star Wars: The Force Awakens");
let top_grossing_films_reference = &top_grossing_films;
println!("Printed using immutable reference: {:#?}", top_grossing_films_reference);
let top_grossing_films_moved = top_grossing_films;
println!("Printed after moving: {:#?}", top_grossing_films_moved);
}
內容解密:
let mut top_grossing_films = vec!["Avatar", "Avengers: Endgame", "Titanic"];:建立一個可變的Vec並填充一些值。let top_grossing_films_mutable_reference = &mut top_grossing_films;:借用top_grossing_films的可變參照。top_grossing_films_mutable_reference.push("Star Wars: The Force Awakens");:使用可變參照修改Vec的內容。let top_grossing_films_reference = &top_grossing_films;:取得top_grossing_films的不可變參照,使先前的可變參照失效。let top_grossing_films_moved = top_grossing_films;:將top_grossing_films的所有權轉移到top_grossing_films_moved。
深複製
在 Rust 中,沒有淺複製的概念,而是透過借用和參照來實作。需要明確指示編譯器要執行的操作。
為什麼需要深複製?
在某些語言中,如 Python 或 Ruby,為了防止複製資料,通常會使用指標、參照和複製寫入語義等最佳化手段。因此,在這些語言中,有時需要執行顯式的深複製。
Rust 中的深複製
Rust 不假設任何關於你的意圖,因此你總是需要明確指示編譯器要做什麼。Rust 提供了 clone() 方法來實作深複製。
let original_vec = vec![1, 2, 3];
let cloned_vec = original_vec.clone();
println!("Original Vec: {:?}", original_vec);
println!("Cloned Vec: {:?}", cloned_vec);
內容解密:
let original_vec = vec![1, 2, 3];:建立一個原始的Vec。let cloned_vec = original_vec.clone();:對original_vec執行深複製,建立一個新的Vec。println!:列印原始和複製的Vec。
借用和參照的重要性
在 Rust 中,借用和參照是用於在不取得所有權的情況下存取資料的主要手段。資料可以透過值(即移動)或參照傳遞給函式。借用的資料可以是不可變或可變的,預設情況下,借用是不可變的。
fn print_vec(vec_ref: &Vec<i32>) {
println!("Vec contents: {:?}", vec_ref);
}
fn main() {
let my_vec = vec![1, 2, 3];
print_vec(&my_vec);
println!("Vec after function call: {:?}", my_vec);
}
內容解密:
fn print_vec(vec_ref: &Vec<i32>):定義一個函式,接受一個Vec<i32>的不可變參照。print_vec(&my_vec);:將my_vec的不可變參照傳遞給print_vec函式。- 資料在函式內部被存取,但未被修改或移動,因此在函式呼叫後仍然可用。
使用記憶體的智慧
避免不必要的複製
在Rust程式設計中,資料結構的複製可能導致效能問題,尤其是在處理大型資料集時。瞭解何時會發生複製以及如何避免,是寫出高效能Rust程式的關鍵。
Clone Trait的運作機制
Rust中的Clone trait用於建立資料結構的深度複製。當對一個資料結構呼叫clone()方法時,會遞迴地複製其所有內容,只要這些內容也實作了Clone trait。這使得複製複雜的巢狀結構變得簡單。
fn main() {
let mut most_populous_us_cities =
vec!["New York City", "Los Angeles", "Chicago", "Houston"];
let most_populous_us_cities_cloned = most_populous_us_cities.clone();
most_populous_us_cities.push("Phoenix");
println!("most_populous_us_cities = {:#?}", most_populous_us_cities);
println!(
"most_populous_us_cities_cloned = {:#?}",
most_populous_us_cities_cloned
);
}
內容解密:
- 在這段程式碼中,我們首先建立了一個包含美國人口最多的城市的向量
most_populous_us_cities。 - 我們使用
clone()方法建立了這個向量的深度複製,儲存在most_populous_us_cities_cloned中。 - 之後,我們向原始向量中新增了一個城市"Phoenix"。
- 列印結果顯示,原始向量和複製的向量是兩個獨立的結構,對原始向量的修改不會影響到複製的向量。
辨識和避免不必要的複製
Rust的核心函式庫函式通常傳回物件的複製,而不是直接修改原始物件。這種設計有助於保持資料的不變性,但也可能導致不必要的記憶體複製。瞭解函式的行為對於避免不必要的複製至關重要。
Rust核心字串函式的行為分析
| 函式名稱 | 描述 | 是否複製 | 辨識方法 |
|---|---|---|---|
replace | 取代字串中的模式 | 是 | 傳回新的String,原始字串不變 |
to_lowercase | 將字串轉換為小寫 | 是 | 傳回新的String,原始字串不變 |
make_ascii_lowercase | 將ASCII字串轉換為小寫 | 否 | 直接修改原始字串 |
trim | 移除字串的首尾空白 | 否 | 傳回原始字串的切片 |
內容解密:
- 表格中的函式展示了Rust中常見的字串操作及其是否會導致複製。
replace和to_lowercase會傳回新的字串,是因為它們需要建立新的記憶體空間來儲存結果。make_ascii_lowercase直接修改原始字串,因此不會導致複製。trim傳回的是原始字串的切片,也不會導致複製。
智慧指標:Box的使用時機
Rust中的Box是一種智慧指標,主要用於在堆積上分配記憶體。瞭解何時使用Box,對於有效管理記憶體非常重要。
Box的基本用途
Box主要用於在堆積上分配資料。- 與
Vec不同,Box主要用於單一物件的記憶體管理。
// 使用Box在堆積上分配一個i32
let b = Box::new(5);
println!("b = {}", b);
內容解密:
- 這段程式碼展示瞭如何使用
Box::new在堆積上分配一個i32型別的變數。 Box自動管理它所指向的記憶體,當Box離開作用域時,它所指向的記憶體會被釋放。
智慧指標:Box 與 Option 的結合使用
在 Rust 中,Box 是一種智慧指標,用於在堆積疊上分配記憶體。然而,Box 本身不能為空,除非在某些特殊情況下。因此,當被 Box 的資料可能不存在時,我們通常會將 Box 包裝在 Option 中。
使用 Option 包裝 Box
選項型別(Optional types)並不是 Rust 獨有的,其他語言如 Ada、Haskell、Scala 和 Swift 等也都有類別似的概念。選項型別是一種函式式設計模式,將不應直接存取的值包裝在一個函式中。Rust 提供了一些語法糖,使操作選項型別更加方便。
為何使用 Option 和 Box
Option 在 Rust 中被廣泛使用。如果你之前沒接觸過選項型別,可以將其視為安全處理空值(例如:指標)的一種方式。Rust 不支援空指標(除了在 unsafe 程式碼中),但它有 None,功能上等同於空指標,卻沒有安全問題。
Box 和 Option 的結合
當 Box 和 Option 一起使用時,幾乎不可能出現因無效、未初始化或重複釋放記憶體而導致的執行階段錯誤(例如:空指標例外)。然而,有一個需要注意的地方:堆積疊分配可能會失敗。處理這種情況相當棘手,不在本文的討論範圍內,且在一定程度上取決於作業系統及其設定。
記憶體分配失敗的處理
記憶體分配失敗的一個常見原因是系統記憶體耗盡。處理這種情況(如果你選擇處理的話)很大程度上取決於應用程式。大多數時候,處理記憶體分配失敗的預期結果是程式「快速失敗」並離開,出現記憶體不足的錯誤(OOM)。這幾乎總是預設行為(也就是說,如果開發者不處理 OOM 錯誤時會發生的情況)。你可能已經遇到過這種情況。一些值得注意的應用程式提供了自己的記憶體管理功能,例如網頁瀏覽器,它們通常有自己的內建任務管理器和記憶體管理,就像作業系統一樣。如果你正在編寫關鍵任務軟體,例如資料函式庫或線上交易處理系統,你可能希望優雅地處理記憶體分配失敗。
使用 try_new() 方法
在懷疑分配可能會失敗的情況下,Box 提供了 try_new() 方法,該方法傳回一個 Result。這與 Option 類別似,可能處於成功或失敗狀態。Box 的預設 new() 方法在分配失敗時會產生 panic,導致程式當機。在大多數情況下,當機是處理分配失敗的最佳方式。另外,你可以在自訂分配器中捕捉分配失敗(本章稍後會討論)。
使用 Box 實作單連結串列
為了說明 Box 的使用,考慮 Rust 中的一個基本單連結串列。
struct ListItem<T> {
data: Box<T>,
next: Option<Box<ListItem<T>>>,
}
struct SinglyLinkedList<T> {
head: ListItem<T>,
}
impl<T> ListItem<T> {
fn new(data: T) -> Self {
ListItem {
data: Box::new(data),
next: None,
}
}
fn next(&self) -> Option<&Self> {
if let Some(next) = &self.next {
Some(next.as_ref())
} else {
None
}
}
fn mut_tail(&mut self) -> &mut Self {
if self.next.is_some() {
self.next.as_mut().unwrap().mut_tail()
} else {
self
}
}
fn data(&self) -> &T {
self.data.as_ref()
}
}
impl<T> SinglyLinkedList<T> {
fn new(data: T) -> Self {
SinglyLinkedList {
head: ListItem::new(data),
}
}
fn append(&mut self, data: T) {
let mut tail = self.head.mut_tail();
tail.next = Some(Box::new(ListItem::new(data)));
}
fn head(&self) -> &ListItem<T> {
&self.head
}
}
內容解密:
ListItem結構定義:每個連結串列節點包含一個被Box包裝的資料和一個可選的指向下一個節點的next指標。SinglyLinkedList結構定義:單鏈表本身只包含頭節點。ListItem的方法實作:new:建立一個新的連結串列節點,將資料包裝在Box中,並將next初始化為None。next:傳回當前節點的下一個節點的參考,如果存在的話。mut_tail:遞迴地找到連結串列的最後一個節點,並傳回其可變參考。data:傳回當前節點資料的參考。
SinglyLinkedList的方法實作:new:建立一個新的單連結串列,需要提供初始資料。append:在連結串列的末尾新增新的節點。head:傳回連結串列頭節點的參考。
圖示說明:
此圖示展示了一個單連結串列的結構,其中每個節點包含資料和指向下一個節點的指標。頭節點是連結串列的入口點,透過它可以遍歷整個連結串列。
深入理解 Rust 的記憶體管理:參考計數
在前面的章節中,我們討論了 Box,這是一種實用但功能有限的智慧指標。在 Rust 程式中,Box 無法被共用,也就是說,你不能讓兩個不同的 Box 指向相同的資料,因為 Box 擁有其資料,並且不允許多個借用同時存在。這種設計在大多數情況下是一種優勢,但有些情況下,我們確實需要共用資料,例如跨執行緒或將相同資料儲存在多個結構中以便不同方式存取(如 Vec 和 HashMap)。
參考計數的原理
在 Box 無法滿足需求的情況下,參考計數智慧指標(reference-counted smart pointers)就派上用場了。參考計數是一種常見的記憶體管理技術,用於避免追蹤指標的複製次數,並在沒有複製時釋放記憶體。實作上通常依賴於維護一個靜態計數器,每當建立新的複製時計數器就會遞增,而當複製被銷毀時,計數器就會遞減。如果計數器歸零,表示沒有任何複製存在,記憶體就可以被釋放,因為它不再被使用或存取。
實作參考計數智慧指標
實作參考計數智慧指標是一個有趣的練習,但它在 Rust 中有點棘手,需要使用原始(即不安全的)指標。如果你覺得實作鏈結串列太簡單,可以嘗試建立自己的參考計數智慧指標。
Rust 中的參考計數智慧指標
Rust 提供了兩種不同的參考計數指標:
Rc:單執行緒的參考計數智慧指標,允許物件的共用所有權Arc:多執行緒的參考計數智慧指標,允許跨執行緒共用物件的所有權
要有效地使用參考計數指標,我們還需要介紹 Rust 中的另一個概念:內部可變性(interior mutability)。內部可變性是你在某些情況下可能需要的技術。
單執行緒與多執行緒物件
許多程式語言區分可以在執行緒間安全使用的函式或物件(執行緒安全)和不安全的函式或物件。在 Rust 中,這種區分並不直接適用,因為預設情況下一切都是安全的。相反,有些物件可以跨執行緒移動或同步,而其他物件則不能。這種行為取決於物件是否實作了 Send 和 Sync 特徵,我們將在第 6 章中更詳細地討論。
對於 Rc 和 Arc,Rc 沒有提供 Send 或 Sync(事實上,Rc 明確標記為未實作這些特徵),因此 Rc 只能在單一執行緒中使用。另一方面,Arc 實作了 Send 和 Sync,因此可以在多執行緒程式碼中使用。
特別是,Arc 使用原子計數器,這些計數器是平台相關的,通常由作業系統或 CPU 層級實作。原子操作比常規算術運算更昂貴,因此只有在需要原子性時才使用 Arc。
值得注意的是,只要你不使用 unsafe 關鍵字繞過語言規則,Rust 程式碼總是安全的。然而,當你不瞭解 Rust 獨特的模式和術語時,要讓程式碼編譯透過可能會相當具有挑戰性。
鏈結串列的重溫
在探討參考計數之前,我們先來看看前面提到的鏈結串列範例。這個範例展示了 Rust 獨特的功能,並且是安全的。鏈結串列永遠不會是空的、無效的或包含空指標。我們可以使用以下程式碼測試剛建立的鏈結串列:
fn main() {
let mut list = SinglyLinkedList::new("head");
list.append("middle");
list.append("tail");
let mut item = list.head();
loop {
println!("item: {}", item.data());
if let Some(next_item) = item.next() {
item = next_item;
} else {
break;
}
}
}
執行上述程式碼會產生以下輸出:
item: head
item: middle
item: tail
程式碼解析
- 建立一個新的鏈結串列,並新增 “head”、“middle” 和 “tail” 三個元素。
- 取得鏈結串列的頭部參考。
- 使用迴圈遍歷鏈結串列中的每個元素,並印出其值。
- 使用
if let陳述式取得下一個元素,如果下一個元素存在,則繼續迴圈;否則,跳出迴圈。
Plantuml 圖解:鏈結串列結構
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Rust 記憶體管理與智慧指標應用
package "安全架構" {
package "網路安全" {
component [防火牆] as firewall
component [WAF] as waf
component [DDoS 防護] as ddos
}
package "身份認證" {
component [OAuth 2.0] as oauth
component [JWT Token] as jwt
component [MFA] as mfa
}
package "資料安全" {
component [加密傳輸 TLS] as tls
component [資料加密] as encrypt
component [金鑰管理] as kms
}
package "監控審計" {
component [日誌收集] as log
component [威脅偵測] as threat
component [合規審計] as audit
}
}
firewall --> waf : 過濾流量
waf --> oauth : 驗證身份
oauth --> jwt : 簽發憑證
jwt --> tls : 加密傳輸
tls --> encrypt : 資料保護
log --> threat : 異常分析
threat --> audit : 報告生成
@enduml
此圖示展示了鏈結串列的結構,每個節點指向下一個節點,最後一個節點指向 None。
在接下來的章節中,我們將探討更進階的鏈結串列實作。如果你想更好地理解 Rust 的記憶體管理,請嘗試從頭開始實作鏈結串列,並在需要時參考提供的範例。實際上,在 Rust 中,你很可能永遠不需要實作自己的鏈結串列,因為 Rust 的核心函式庫提供了 std::collections::LinkedList。然而,瞭解其背後的原理仍然是非常有價值的。