相對於傳統直接使用記憶體位址來對應記憶體資料的指標,智慧型指標為一種資料結構,除了擁有基本的指標能對應記憶體資料的功能外,還可以提供其它不同的資訊以及額外的功能。例如我們先前使用過的Vec結構體和String結構體,就是智慧型指標。



在Rust程式語言中,有實作DerefDrop特性的結構體即為智慧型指標。Deref特性可以使結構體支援「解參考」的動作,也就是能決定當該結構實體搭配*解參考語法使用時,所回傳的資料。擁有Drop特性的結構體,其實體會在離開其所在的scope時,在被消滅前執行drop方法,用來釋放記憶體中應該也要跟著被釋放的資料。

Rust程式語言的標準函式庫提供了許多智慧型指標,其中比較基本而且常用的有:

  • Box:可以將資料配置在堆積上。
  • Rc:允許堆積上的同一筆資料有許多擁有者。
  • RefRefMutRefCell:能讓擁有權借用的規則在執行階段才發生。

Box

Box結構體可以允許我們將資料儲存在堆積中,而不是堆疊。堆疊空間中只會儲存指到堆積空間的指標。除了堆積和堆疊本身的存取效能差異之外,Box結構體並不會有額外的運算資源花費。遇到以下幾種情況,我們應該使用Box結構體來儲存資料:

1. 資料大小在編譯階段無法確定。
2. 確保龐大的資料不會在擁有權更動時被複製,因為複製資料會需要大量的時間和記憶體。
3. 對於我們想要擁有的數值,我們只在乎它實作了哪些特性,而不必知道其真正的型別。

任何型別的資料都可以利用Box結構體儲存在堆積中,可以使用Box結構體提供的new方法,透過參數傳入一個要存進堆積的值,建立出Box結構實體。

舉例來說:

fn main() {
    let b = Box::new(5); // Box<i32>

    println!("b = {b}");
}

當程式離開main函數的scope時,b變數所儲存的Box結構實體會被消滅,在其被消滅之前會自動釋放掉其allocating的堆積空間,因此不會有記憶體洩漏的問題。以上程式執行結果為:

b = 5

如果設計出一種結構體,其欄位也會儲存該結構體實體的話,這樣的結構體稱作「遞迴型別」(Recursive Type)。如以下程式:

struct RecursiveStruct {
    rs: RecursiveStruct,
}

這個程式會編譯錯誤,因為Rust的編譯器無法在編譯階段知道這個RecursiveStruct結構體究竟有多大。但是如果將rs欄位的型別改成:

struct RecursiveStruct {
    rs: Box<RecursiveStruct>,
}

如此一來,RecursiveStruct結構體的rs欄位會將其它的RecursiveStruct結構體儲存在堆積上,而Box結構體的大小跟其泛型型別沒有關聯,因此這個RecursiveStruct結構體的大小可以在編譯階段被計算出來,不會編譯錯誤。

我們也可以利用Box結構體來讓函數或是方法回傳沒有具體型別的閉包。如以下程式:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

剛才我們有提到Rust的智慧型指標有實作Deref特性,也就是說,Box結構實體可以直接使用*解參考語法來取得其堆積中的資料。舉例來說:

fn main() {
    let x = 5;
    let y = Box::new(x);
    let z = x + *y;

    println!("{z}");
}

程式執行結果如下:

10

我們也可以替自己的結構體實作Deref特性,使其能夠支援*解參考語法。程式如下:

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) -> &T {
        &self.0
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);
    let z = x + *y;

    println!("{z}");
}

直接對擁有Deref特性的結構實體使用&語法,會發生「Deref Coercion」,我們不會得到該結構實體的參考,而是會得到該結構實體解參考之後的值的參考。

舉例來說:

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) -> &T {
        &self.0
    }
}

fn print_i32(i: &i32) {
    println!("{}", i);
}

fn main() {
    let x = MyBox::new(5);
    let i = &x;

    print_i32(i);
}

main函數中的變數i所儲存的值為數值5的參考,因此可以被代入至print_i32函數的第一個參數。

如果想要讓結構實體在解參考之後所取得的值是可變的話,可以替其實作DerefMut特性。舉例來說:

use std::ops::Deref;
use std::ops::DerefMut;

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) -> &T {
        &self.0
    }
}

impl<T> DerefMut for MyBox<T> {
    fn deref_mut(&mut self) -> &mut T {
        &mut self.0
    }
}

fn main() {
    let mut x = MyBox::new(5);
    *x += 2;

    println!("{}", *x);
}

程式執行結果如下:

7

接下來談談Drop特性吧!Drop特性可以允許我們決定資料在其所屬的scope已經要結束時所要進行的動作,例如關閉檔案或是切斷網路連線等等。Box結構體的Drop特性則是被實作來釋放其在堆積中配置的空間。某些程式語言會要求使用者得自行在適當的時機點安插釋放記憶體的程式,但是在Rust程式語言,我們可以藉由實作Drop特性,讓編譯器自動在該資源應該要被消滅的位置新增釋放記憶體的程式碼,如此一來,我們就不用擔心不小心犯錯而導致記憶體洩漏了。

舉例來說:

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer { data: String::from("my stuff") };
    let d = CustomSmartPointer { data: String::from("other stuff") };

    println!("CustomSmartPointers created.");
}

程式執行結果為:

CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

變數d存的CustomSmartPointer實體會比變數c還要早的原因是:變數c比變數d還要早被存放至堆疊空間中,因此在清除堆疊空間時,變數d會比變數c還要早拿出來(先進後出)。

我們也可以利用標準函數庫中std::mem模組所提供的drop函數,讓程式在scope執行結束前就提早釋放資源。舉例來說:

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer { data: String::from("my stuff") };
    let d = CustomSmartPointer { data: String::from("other stuff") };

    drop(c);

    println!("CustomSmartPointers created.");
}

程式執行結果為:

Dropping CustomSmartPointer with data `my stuff`!
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!

Rc

Rc結構體是一個基於參考數量的智慧型指標,可以適用在值不知道會被哪個或是哪些變數擁有的情況。舉例來說,在「圖」(graph)的資料結構,可能會同時有多個邊(edge)去連結到同一個節點(vertex),每個邊都是該節點的擁有者,換句話說,只有當這個節點沒有被任何的邊連結到的時候(Isolated Vertex),這個節點才可以被消滅。Rc這個名稱為「reference counting」(參考計數)的縮寫,Rc結構體可以追蹤堆積中值的參考數量,因此可以知道該值目前究竟是不是還有再被使用,能不能將其消滅並釋放出記憶體了。

簡單來說,當我們想要在堆積中存放資料,而這個資料無法在編譯階段時決定好由哪個單獨的變數來擁有的話,就可以使用Rc結構體。

先用Box結構體舉個簡單的例子:

fn main() {
    let a = String::from("Hello world!");
    let b = Box::new(a);
    let c = Box::new(a);
}

以上程式會編譯失敗,因為變數a會在程式第3行的時候被「移動」,所以程式第4行就不能再被使用。

如果我們要讓變數a儲存的String結構實體,被變數b和變數c擁有的話,可以使用Rc結構體改寫成:

use std::rc::Rc;

fn main() {
    let a = String::from("Hello world!");
    let b = Rc::new(a); // Rc<String>
    let c = b.clone(); // or Rc::clone(b);
}

Rc結構實體的clone方法可以再複製一份新的Rc結構實體。複製之後,String結構實體就等同於有了兩個參考。我們可以使用Rc結構體提供的strong_count函數,來取得當時參考到同一個值的Rc結構實體共有幾個。

程式如下:

use std::rc::Rc;

fn main() {
    let a = String::from("Hello world!");
    let b = Rc::new(a); // Rc<String>

    println!("{}", Rc::strong_count(&b));

    let c = b.clone();

    println!("{}", Rc::strong_count(&b));

    {
        let d = b.clone();
        println!("{}", Rc::strong_count(&b));
    }

    println!("{}", Rc::strong_count(&b));
}

執行結果為:

1
2
3
2

Rc結構體無法直接用在可變的資料,需搭配RefCell結構體來使用。

Ref、RefMut、RefCell

一般來說,Rust處理資料時在編譯階段應該要去遵守我們先前介紹的擁有權規則,也就只有使用mut關鍵字的變數才可以取得其可變參考,且該值在一個scope下,同時只能有一個可變參考,不能再有其它的參考。

舉例來說:

fn main() {
    let mut x = String::from("Hello world!");
    let y = &mut x;
    let z = &x;
}

以上程式會編譯錯誤,因為變數x已經產生了可變參考給變數y,所以第4行無法再產生新的參考。

然而我們可以用RefCell結構體來改寫這個程式,使之通過編譯,程式如下:

use std::cell::RefCell;

fn main() {
    let x = String::from("Hello world!");
    let r = RefCell::new(x); // RefCell<String>
    let y = r.borrow_mut();
    let z = r.borrow();
}

RefCell結構實體的borrow_mut方法,可以回傳一個新的RefMut結構實體,能作為可變參考使用,不必使用mut關鍵字來宣告。而borrow方法,則可以回傳一個新的Ref結構實體,能作為不可變參考使用。使用RefCell結構實體提供的方法來處理擁有權的規則,是在程式執行階段才會進行,也因此以上程式可以通過編譯,但是當它在執行的時候就會因第6行已經產生可變參考給變數y使用,所以執行到第7行時程式就會發生panic。

我們也可以讓Rc結構體參考到RefCell結構體,再讓RefCell結構體去參考實際的值,如此一來Rc結構體就有可變參考的特性。程式如下:

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let a = String::from("Hello world!");
    let r = RefCell::new(a); // RefCell<String>
    let b = Rc::new(r); // Rc<RefCell<String>>
    let c = b.clone(); // or Rc::clone(b);

    (*c.borrow_mut()).push_str(" Rust!");

    println!("{}", *c.borrow());
}

記憶體洩漏

Rc結構體搭配RefCell結構體使用時要小心,若有循環參考的話,就會有記憶體洩漏的問題。程式如下:

use std::cell::RefCell;
use std::rc::Rc;

enum Node {
    Vertex(i32, Rc<RefCell<Node>>),
    Nil,
}

fn leak_call() {
    let node_nil = Rc::new(RefCell::new(Node::Nil));
    let node_5 = Rc::new(RefCell::new(Node::Vertex(5, node_nil.clone())));
    let node_4 = Rc::new(RefCell::new(Node::Vertex(4, node_5.clone())));
    let node_3 = Rc::new(RefCell::new(Node::Vertex(3, node_4.clone())));
    let node_2 = Rc::new(RefCell::new(Node::Vertex(2, node_3.clone())));
    let node_1 = Rc::new(RefCell::new(Node::Vertex(1, node_2.clone())));
    let mut n = node_5.borrow_mut();

    if let Node::Vertex(_, ref mut nv) = *n {
        *nv = node_1.clone();
    }
}

fn main() {
    loop {
        leak_call();
    }
}

以上程式第15行執行完時,leak_call函數建立出來的Node實體在記憶體中的結構如下:

node_1 -> node_2 -> node_3 -> node_4 -> node_5 -> node_nil

程式第19行,我們在leak_call函數結束前,將node_5的下一個節點設為node_1,產生循環,使得Rc結構體所記的參考數量永遠不會為0,因此在leak_call函數結束執行之後,堆積中的資料依然不會被釋放,這個就是記憶體洩漏啦!我們在main函數的部份,用無窮迴圈重複地呼叫leak_call函數,以便觀察執行時記憶體洩漏的情形。這支程式在執行的時候會隨時間流逝佔用愈來愈多的記憶體,最終將會因為記憶體不足而結束執行。

強參考和弱參考

Rc結構體屬於強參考(strong reference),使用Rc結構實體的clone方法會複製出新的強參考,同時也會讓Rc結構體的strong_count函數的回傳結果增加,而被參考到的值只有在強參考的數量為0時才會被消滅釋放。另外還有一種參考相對於強參考,稱為弱參考(weak reference)。增加弱參考的數量並不會使得強參考數量增加,換句話說在剛才記憶體洩漏的例子中,我們可以將原本強參考的部份適當地改為弱參考,這樣即便有循環參考,也不會導致記憶體洩漏。

我們可以透過Rc結構體提供的downgrade函數來產生Weak結構實體,作為弱參考使用。將記憶體洩漏的程式改寫如下:

use std::cell::RefCell;
use std::rc::Rc;
use std::rc::Weak;

enum Node {
    Vertex(i32, Weak<RefCell<Node>>),
    Nil,
}

fn leak_call() {
    let node_nil = Rc::new(RefCell::new(Node::Nil));
    let node_5 = Rc::new(RefCell::new(Node::Vertex(5, Rc::downgrade(&node_nil))));
    let node_4 = Rc::new(RefCell::new(Node::Vertex(4, Rc::downgrade(&node_5))));
    let node_3 = Rc::new(RefCell::new(Node::Vertex(3, Rc::downgrade(&node_4))));
    let node_2 = Rc::new(RefCell::new(Node::Vertex(2, Rc::downgrade(&node_3))));
    let node_1 = Rc::new(RefCell::new(Node::Vertex(1, Rc::downgrade(&node_2))));

    let mut n = node_5.borrow_mut();

    if let Node::Vertex(_, ref mut nv) = *n {
        *nv = Rc::downgrade(&node_1);
    }
}

fn main() {
    loop {
        leak_call();
    }
}

改寫後的leak_call函數,就不再有記憶體洩漏的問題了!

使用Rc結構體的weak_count函數,可以取得弱參考的數量。舉例來說:

use std::rc::Rc;

fn main() {
    let a = String::from("Hello world!");
    let b = Rc::new(a);

    println!("{}", Rc::strong_count(&b)); // 1
    println!("{}", Rc::weak_count(&b)); // 0

    let c = Rc::downgrade(&b);

    println!("{}", Rc::strong_count(&b)); // 1
    println!("{}", Rc::weak_count(&b)); // 1

    {
        let d = Rc::downgrade(&b);

        println!("{}", Rc::strong_count(&b)); // 1
        println!("{}", Rc::weak_count(&b)); // 2
    }

    println!("{}", Rc::strong_count(&b)); // 1
    println!("{}", Rc::weak_count(&b)); // 1
}

結論

這個章節我們學到了如何使用Box結構體和Rc結構體這兩種智慧型指標來將資料儲存到堆積中,前者是在確定資料的擁有者只有一個的情況下使用,本身即可作為可變參考使用;後者則是在資料會有很多個擁有者的情況下使用,本身不能作為可變參考使用,但可以搭配RefCell結構體來讓Rust的擁有權規則在執行階段才生效。RefCell結構實體的borrowborrow_mut方法分別可以產生Ref結構體和RefMut結構體這兩種功能單純的智慧型指標的實體,分別作為不可變參考或可變參考來使用。

在下一個章節中,我們會學習如何用Rust程式語言開發能並發(concurrency)與並行(parallelism)執行的程式。

下一章:並發與並行