iT邦幫忙

2025 iThome 鐵人賽

DAY 13
1
Rust

大家一起跟Rust當好朋友吧!系列 第 13

Day 13: 智慧指標 (Smart Pointers):Box, Rc, Arc 與進階記憶體管理

  • 分享至 

  • xImage
  •  

嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第十三天!

經過前兩天對生命週期的深入探討,相信你已經對 Rust 的參考和借用有了深刻的理解。今天我們要來學習另一個重要的記憶體管理工具:智慧指標 (Smart Pointers)

如果說參考是「借用」資料的方式,那麼智慧指標就是「擁有」資料的進階方式。在其他語言中,我們可能習慣了垃圾回收器自動管理記憶體,但 Rust 透過智慧指標,讓我們能在保持記憶體安全的同時,獲得更精細的控制能力。

老實說,剛開始接觸智慧指標時,我覺得它們像是「高級版的指標」,但實際上它們更像是「有超能力的容器」。每種智慧指標都有其特定的使用場景和能力,掌握它們將讓你在面對複雜的記憶體管理需求時游刃有餘。

今天讓我們一起探索這些強大的工具!

什麼是智慧指標?

定義與特性

智慧指標是一種資料結構,它們:

  1. 像指標一樣:可以解參考來存取資料
  2. 比指標更聰明:包含額外的後設資料和功能
  3. 實現 RAII:自動管理資源的生命週期
  4. 型別安全:在編譯時防止記憶體錯誤

在 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:最簡單的智慧指標

基本概念

Box<T> 是最簡單的智慧指標,它將資料分配到堆積而不是堆疊上:

fn main() {
    // 在堆疊上
    let stack_value = 5;
    
    // 在堆積上
    let heap_value = Box::new(5);
    
    println!("堆疊值:{}", stack_value);
    println!("堆積值:{}", heap_value);  // 自動解參考
    println!("明確解參考:{}", *heap_value);
}

使用場景

1. 大型資料結構

#[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));
}

2. 遞迴資料結構

這是 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:參考計數智慧指標

基本概念

當你需要讓多個擁有者共享同一份資料時,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 的限制

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:原子參考計數

基本概念

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));
}

結合 Mutex 實現執行緒安全的可變性

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 與內部可變性

基本概念

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 和 Drop Traits:智慧指標的魔法

Deref Trait:解參考強制轉型

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:自動清理資源

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>: 打破循環參考");
}

智慧指標的最佳實務

1. 避免不必要的分配

// ✅ 好:只在需要時使用 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
}

2. 小心循環參考

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>,  // 這會造成循環參考
// }

3. 合理使用 RefCell

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 的智慧指標系統:

核心智慧指標

  • Box:最簡單的智慧指標,用於堆積分配
  • Rc:單執行緒環境的參考計數指標
  • Arc:多執行緒環境的原子參考計數指標
  • RefCell:提供內部可變性的容器

重要概念

  • Deref trait:讓智慧指標能像普通參考一樣使用
  • Drop trait:定義資源清理邏輯,實現 RAII
  • Weak:打破循環參考的弱參考
  • 內部可變性:在不可變容器中修改資料

最佳實務

  • 選擇合適的智慧指標
  • 避免不必要的記憶體分配
  • 小心處理循環參考
  • 合理使用內部可變性

為什麼智慧指標很重要?

  • 記憶體安全:編譯時防止懸置指標和記憶體洩漏
  • 零成本抽象:智慧的功能,原生的效能
  • 所有權清晰:明確的所有權語意,便於推理
  • 併發安全:型別系統保證執行緒安全

智慧指標是 Rust 記憶體管理的精髓,它們讓我們能在保持安全的同時,獲得對記憶體的精細控制。掌握智慧指標的使用,將讓你能夠設計出既安全又高效的複雜資料結構。

今天的小挑戰

為了鞏固今天的學習,嘗試實作一個檔案系統樹狀結構模擬器

功能需求

  1. 檔案系統節點:支援檔案和資料夾兩種型別
  2. 樹狀結構:資料夾可以包含子檔案和子資料夾
  3. 共享參考:同一個檔案可能被多個符號連結參考
  4. 父節點參考:每個節點都知道其父節點(使用弱參考避免循環)
  5. 路徑查詢:根據路徑字串查找檔案
  6. 統計功能:計算資料夾大小、檔案數量等

技術要求

  • 使用 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 內建的測試框架讓測試變得簡單而強大!

如果在實作過程中遇到任何問題,歡迎在留言區討論!

我們明天見!


上一篇
Day 12: 生命週期 (Lifetimes):攻克 Rust 最難懂的概念
下一篇
Day 14: 測試 (Testing):建立可靠的測試體系
系列文
大家一起跟Rust當好朋友吧!19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言