嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第十三天!
經過前兩天對生命週期的深入探討,相信你已經對 Rust 的參考和借用有了深刻的理解。今天我們要來學習另一個重要的記憶體管理工具:智慧指標 (Smart Pointers)。
如果說參考是「借用」資料的方式,那麼智慧指標就是「擁有」資料的進階方式。在其他語言中,我們可能習慣了垃圾回收器自動管理記憶體,但 Rust 透過智慧指標,讓我們能在保持記憶體安全的同時,獲得更精細的控制能力。
老實說,剛開始接觸智慧指標時,我覺得它們像是「高級版的指標」,但實際上它們更像是「有超能力的容器」。每種智慧指標都有其特定的使用場景和能力,掌握它們將讓你在面對複雜的記憶體管理需求時游刃有餘。
今天讓我們一起探索這些強大的工具!
智慧指標是一種資料結構,它們:
在 Rust 中,智慧指標通常實現以下 traits:
Deref
:允許解參考操作 (*
)Drop
:在離開作用域時自動清理資源fn main() {
// 普通參考:借用值,不擁有所有權
let x = 5;
let y = &x; // y 借用 x
// 智慧指標:擁有值,負責清理
let z = Box::new(5); // z 擁有堆積上的值
println!("y: {}, z: {}", y, z);
} // x, y, z 都離開作用域,但只有 z 會觸發清理操作
Box<T>
是最簡單的智慧指標,它將資料分配到堆積而不是堆疊上:
fn main() {
// 在堆疊上
let stack_value = 5;
// 在堆積上
let heap_value = Box::new(5);
println!("堆疊值:{}", stack_value);
println!("堆積值:{}", heap_value); // 自動解參考
println!("明確解參考:{}", *heap_value);
}
#[derive(Debug)]
struct LargeStruct {
data: [u8; 1024 * 1024], // 1MB 的資料
}
fn main() {
// 這會在堆疊上分配 1MB,可能造成堆疊溢位
// let large = LargeStruct { data: [0; 1024 * 1024] };
// 使用 Box 將大型結構移到堆積上
let large = Box::new(LargeStruct { data: [0; 1024 * 1024] });
println!("大型結構已安全分配到堆積");
// 也可以作為函式參數傳遞
process_large_struct(large);
}
fn process_large_struct(data: Box<LargeStruct>) {
println!("處理大型結構,大小:{} 位元組",
std::mem::size_of_val(&*data));
}
這是 Box<T>
最重要的使用場景之一:#### 3. Trait Objects
trait Drawable {
fn draw(&self);
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Drawable for Circle {
fn draw(&self) {
println!("畫一個半徑為 {:.2} 的圓", self.radius);
}
}
impl Drawable for Rectangle {
fn draw(&self) {
println!("畫一個 {:.2} x {:.2} 的長方形", self.width, self.height);
}
}
fn main() {
// 使用 Box 儲存不同型別的 trait objects
let shapes: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 10.0, height: 8.0 }),
Box::new(Circle { radius: 3.0 }),
];
// 多型呼叫
for shape in &shapes {
shape.draw();
}
}
當你需要讓多個擁有者共享同一份資料時,Rc<T>
(Reference Counted) 就派上用場了:
use std::rc::Rc;
fn main() {
let data = Rc::new(String::from("共享資料"));
// 複製 Rc,增加參考計數
let data1 = Rc::clone(&data);
let data2 = Rc::clone(&data);
println!("參考計數:{}", Rc::strong_count(&data)); // 3
println!("data: {}", data);
println!("data1: {}", data1);
println!("data2: {}", data2);
// 當變數離開作用域時,參考計數會減少
drop(data1);
println!("刪除 data1 後的參考計數:{}", Rc::strong_count(&data)); // 2
} // data 和 data2 離開作用域,參考計數變為 0,記憶體被釋放
Rc<T>
只能用於單執行緒環境,並且其中的資料是不可變的:
use std::rc::Rc;
fn main() {
let data = Rc::new(vec![1, 2, 3]);
let data_clone = Rc::clone(&data);
// data.push(4); // 編譯錯誤!Rc<T> 中的資料是不可變的
// 如果需要可變性,需要配合 RefCell
use std::cell::RefCell;
let mutable_data = Rc::new(RefCell::new(vec![1, 2, 3]));
let mutable_clone = Rc::clone(&mutable_data);
// 現在可以修改了
mutable_data.borrow_mut().push(4);
mutable_clone.borrow_mut().push(5);
println!("可變資料:{:?}", mutable_data.borrow());
}
Arc<T>
(Atomically Reference Counted) 是 Rc<T>
的執行緒安全版本:
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for i in 0..3 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
println!("執行緒 {} 看到的資料:{:?}", i, data_clone);
data_clone.len()
});
handles.push(handle);
}
// 等待所有執行緒完成
for handle in handles {
match handle.join() {
Ok(len) => println!("資料長度:{}", len),
Err(_) => println!("執行緒執行失敗"),
}
}
println!("主執行緒的參考計數:{}", Arc::strong_count(&data));
}
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for i in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
for _ in 0..1000 {
let mut num = counter_clone.lock().unwrap();
*num += 1;
}
println!("執行緒 {} 完成", i);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("最終計數:{}", *counter.lock().unwrap());
}
RefCell<T>
提供「內部可變性」(interior mutability),允許在不可變參考存在時修改資料:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// 借用不可變參考
{
let borrowed = data.borrow();
println!("資料:{:?}", *borrowed);
} // 不可變借用結束
// 借用可變參考
{
let mut borrowed_mut = data.borrow_mut();
borrowed_mut.push(4);
borrowed_mut.push(5);
} // 可變借用結束
println!("修改後的資料:{:?}", *data.borrow());
}
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
// 這會在執行時 panic
let _borrow1 = data.borrow_mut();
// let _borrow2 = data.borrow_mut(); // panic!同時有兩個可變借用
// 正確的使用方式
let value = {
let borrowed = data.borrow();
*borrowed
}; // 借用結束
{
let mut borrowed_mut = data.borrow_mut();
*borrowed_mut = value * 2;
} // 借用結束
println!("最終值:{}", *data.borrow());
}
Deref
trait 讓智慧指標能夠像普通參考一樣使用:
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
// Deref 強制轉型:MyBox<String> -> &String -> &str
hello(&m);
// 等價於:
hello(&(*m)[..]);
// 直接解參考
println!("內容:{}", *m);
}
Drop
trait 定義了值離開作用域時的清理邏輯:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("正在釋放 CustomSmartPointer,資料:{}", self.data);
}
}
struct DatabaseConnection {
id: u32,
}
impl DatabaseConnection {
fn new(id: u32) -> Self {
println!("建立資料庫連線:{}", id);
DatabaseConnection { id }
}
}
impl Drop for DatabaseConnection {
fn drop(&mut self) {
println!("關閉資料庫連線:{}", self.id);
}
}
fn main() {
println!("=== Drop Trait 示範 ===");
{
let _c = CustomSmartPointer {
data: String::from("我的資料"),
};
let _db1 = DatabaseConnection::new(1);
let _db2 = DatabaseConnection::new(2);
println!("CustomSmartPointer 和 DatabaseConnection 已建立");
} // 在這裡,變數會以相反的順序被 drop
println!("作用域結束");
// 也可以手動呼叫 drop
let c = CustomSmartPointer {
data: String::from("手動釋放"),
};
drop(c); // 明確釋放
println!("手動釋放完成");
}
use std::rc::Rc;
use std::sync::Arc;
fn main() {
println!("=== 記憶體開銷比較 ===");
let value = 42i32;
let boxed = Box::new(42i32);
let rc_value = Rc::new(42i32);
let arc_value = Arc::new(42i32);
println!("i32 大小:{} 位元組", std::mem::size_of_val(&value));
println!("Box<i32> 大小:{} 位元組", std::mem::size_of_val(&boxed));
println!("Rc<i32> 大小:{} 位元組", std::mem::size_of_val(&rc_value));
println!("Arc<i32> 大小:{} 位元組", std::mem::size_of_val(&arc_value));
// 指標本身的大小
println!("\n=== 指標大小 ===");
println!("&i32 大小:{} 位元組", std::mem::size_of::<&i32>());
println!("Box<i32> 指標大小:{} 位元組", std::mem::size_of::<Box<i32>>());
println!("Rc<i32> 指標大小:{} 位元組", std::mem::size_of::<Rc<i32>>());
println!("Arc<i32> 指標大小:{} 位元組", std::mem::size_of::<Arc<i32>>());
}
use std::time::Instant;
use std::rc::Rc;
fn main() {
let iterations = 1_000_000;
// 測試 Box 的分配效能
let start = Instant::now();
for _ in 0..iterations {
let _b = Box::new(42);
}
let box_time = start.elapsed();
// 測試 Rc 的克隆效能
let rc_value = Rc::new(42);
let start = Instant::now();
for _ in 0..iterations {
let _clone = Rc::clone(&rc_value);
}
let rc_clone_time = start.elapsed();
println!("Box 分配 {} 次耗時:{:?}", iterations, box_time);
println!("Rc 克隆 {} 次耗時:{:?}", iterations, rc_clone_time);
// Rc 克隆比 Box 分配快很多,因為它只是增加參考計數
if box_time > rc_clone_time {
let ratio = box_time.as_nanos() as f64 / rc_clone_time.as_nanos() as f64;
println!("Rc 克隆比 Box 分配快 {:.1} 倍", ratio);
}
}
// 以下是選擇智慧指標的指導原則
fn choose_smart_pointer_example() {
// 1. 需要在堆積上分配資料?
// Yes -> 考慮 Box<T>
// 2. 需要多個擁有者?
// Yes -> 單執行緒:Rc<T>,多執行緒:Arc<T>
// 3. 需要內部可變性?
// Yes -> RefCell<T> (單執行緒) 或 Mutex<T> (多執行緒)
// 4. 可能產生循環參考?
// Yes -> 使用 Weak<T>
println!("選擇指南:");
println!("📦 Box<T>: 堆積分配、遞迴資料結構、trait objects");
println!("🔄 Rc<T>: 單執行緒共享所有權");
println!("⚛️ Arc<T>: 多執行緒共享所有權");
println!("🔒 RefCell<T>: 執行時借用檢查、內部可變性");
println!("🧵 Mutex<T>: 執行緒安全的可變性");
println!("💨 Weak<T>: 打破循環參考");
}
// ✅ 好:只在需要時使用 Box
fn process_small_data(data: i32) -> i32 {
data * 2 // 直接在堆疊上操作
}
// ❌ 不好:不必要的堆積分配
fn process_small_data_bad(data: Box<i32>) -> Box<i32> {
Box::new(*data * 2) // 額外的分配開銷
}
// ✅ 好:大型資料使用 Box
fn process_large_data(data: Box<[u8; 1024 * 1024]>) -> Box<[u8; 1024 * 1024]> {
// 處理大型資料時,Box 是合適的選擇
data
}
use std::rc::{Rc, Weak};
use std::cell::RefCell;
// ✅ 好:使用 Weak 打破循環參考
#[derive(Debug)]
struct Parent {
children: RefCell<Vec<Rc<Child>>>,
}
#[derive(Debug)]
struct Child {
parent: Weak<Parent>, // 使用 Weak 而不是 Rc
}
// ❌ 不好:會造成記憶體洩漏的循環參考
// struct BadParent {
// children: RefCell<Vec<Rc<BadChild>>>,
// }
//
// struct BadChild {
// parent: Rc<BadParent>, // 這會造成循環參考
// }
use std::cell::RefCell;
// ✅ 好:明確的借用作用域
fn good_refcell_usage() {
let data = RefCell::new(vec![1, 2, 3]);
// 短暫的不可變借用
{
let borrowed = data.borrow();
println!("資料長度:{}", borrowed.len());
} // 借用結束
// 短暫的可變借用
{
let mut borrowed_mut = data.borrow_mut();
borrowed_mut.push(4);
} // 借用結束
}
// ❌ 不好:可能導致執行時 panic
fn bad_refcell_usage() {
let data = RefCell::new(vec![1, 2, 3]);
let _borrow1 = data.borrow_mut();
// let _borrow2 = data.borrow_mut(); // 執行時 panic!
}
今天我們深入探討了 Rust 的智慧指標系統:
核心智慧指標:
重要概念:
最佳實務:
為什麼智慧指標很重要?
智慧指標是 Rust 記憶體管理的精髓,它們讓我們能在保持安全的同時,獲得對記憶體的精細控制。掌握智慧指標的使用,將讓你能夠設計出既安全又高效的複雜資料結構。
為了鞏固今天的學習,嘗試實作一個檔案系統樹狀結構模擬器:
功能需求:
技術要求:
Rc<T>
和 Weak<T>
管理節點間的參考關係RefCell<T>
提供內部可變性Drop
trait 來追蹤節點的釋放技術提示:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
use std::collections::HashMap;
enum NodeType {
File { size: u64, content: String },
Directory { children: RefCell<HashMap<String, Rc<FileNode>>> },
}
struct FileNode {
name: String,
node_type: NodeType,
parent: RefCell<Weak<FileNode>>,
}
struct FileSystem {
root: Rc<FileNode>,
}
impl FileSystem {
fn new() -> Self {
// 建立根目錄
}
fn create_file(&self, path: &str, content: String) -> Result<(), String> {
// 在指定路徑建立檔案
}
fn create_directory(&self, path: &str) -> Result<(), String> {
// 在指定路徑建立資料夾
}
fn find_node(&self, path: &str) -> Option<Rc<FileNode>> {
// 根據路徑查找節點
}
fn list_directory(&self, path: &str) -> Result<Vec<String>, String> {
// 列出資料夾內容
}
fn calculate_size(&self, path: &str) -> Result<u64, String> {
// 計算檔案或資料夾總大小
}
}
這個挑戰將讓你綜合運用所有智慧指標的技巧:Rc<T>
用於共享所有權、Weak<T>
避免循環參考、RefCell<T>
提供可變性、以及 Box<T>
存儲動態資料。
明天我們將學習 測試 (Testing),探討如何為 Rust 程式建立完整的測試體系,確保程式碼品質和可靠性。測試是軟體開發的重要環節,Rust 內建的測試框架讓測試變得簡單而強大!
如果在實作過程中遇到任何問題,歡迎在留言區討論!
我們明天見!