2020/05/24

rust15 Smart Pointers

pointer 是儲存記憶體位址的變數,會指向其他資料。rust 最常見的 pointer 是 chap4 介紹的引用 reference。利用 & 參考到指向的值,沒有特殊功能,也沒有額外開銷,所以最常用。

smart pointers 是另一種資料結構,類似 pointer 但有額外的 metadata 及功能。smart pointer 概念起源於 C++ 及其他語言。本章會討論 reference counting smart pointer,允許該資料有多個使用者。reference counting smart pointer 會記錄有幾個使用者,在沒有任何使用者時,會清除資料。

rust 的 reference 與 smart pointer 的差異是,smart pointer 擁有 指向的資料。

chap 8 的 StringVec<T> 都是 smart pointer。因為可修改這些資料,且帶有 metadata (ex: 容量) 與額外的功能 (ex: String 是 UTF-8 編碼)

smart pointer 通常用 struct 實作。另外實作了 DerefDrop trait。Deref trait 可讓 smart pointer 表現得像引用。Drop trait 可自訂當 smart pointer 離開 scope 時的程式。

smart pointer 是 rust 常用的通用設計模式。以下會討論到

  • Box<T> 可在 heap 配值
  • Rc<T> 一個引用計數類別,該資料可以有多個使用者
  • Ref<T>RefMut<T> ,透過 RefCell<T> 存取,是在執行期而不是編譯時執行借用規則的類別

還會討論 內部可變性 interior mutability 模式,可讓不可變的類別暴露出可改變內部值的 API

引用循環 reference cycles 會如何 leak memory,要如何避免

使用 Box 指向 heap 的資料

Box<T> 是最簡單直接的 smart pointer。可將值放在 heap 而不是 stack,存在 stack 的是指向 heap 的 pointer。除了資料存在 heap 非 stack 以外,沒有性能損失。多用於以下場景:

  • 有一個在邊一時不知道大小的類別,又想在需要確切大小的 context 中使用該類別時

    會在後面 "以 Boxes 實作遞迴類別" 這個部分說明

  • 有大量資料,並希望確保資料不被 Copy 的狀況下移轉所有權時

    轉移大量資料的 ownership 會花很多時間,因為 Copy 的關係,為改善效能可使用 box

  • 希望擁有一個值,並只關心他的類別是否實現了特定 trait,而不是具體類別時

    稱為 trait object, chap17 會說明

使用 Box<T> 在 heap 儲存資料

定義變數 b,是儲存到 heap 的 Box,當 b 在 main 結束時,會被釋放,釋放過程作用於 box 本身以及在 heap 的資料

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

將一個值單獨放在 heap 沒有什麼意義,以下是一個只能用 box 定義的類別的例子

以 Boxes 實作遞迴類別

rust 需要在編譯時知道類別會佔用多少空間,recursive type 遞迴類別是一種類別是編譯時無法知道大小的類別,其值的一部分可以是相同類別的另一個值。在 recursive type 使用 box,就可解決不知道大小的問題。

cons list 是 functional language 常見的類別,用來說明 recursive type

cons list 源自 Lisp,cons (construct function) 利用兩個參數來產生一個新的 list,通常一個是值,另一個是 list。將 x 與 y 連接,就表示產生一個新的容器,把 x 放在新容器的開頭,後面是舊容器 y。

cons list 每一項都包含兩個元素,當前的值與下一項,當最後一項是 Nil 就表示沒有下一項。Nil 跟 chap 6 代表無效或缺少的值的 null nil 不同。

cons list 在 rust 並不常用,比較常用的是 Vec<T>

//定義一個代表 i32 值的 cons list 的enum
enum List {
    Cons(i32, List),
    Nil,
}

// 使用 List 枚舉儲存列表 1, 2, 3
use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}

編譯錯誤:List 的一個成員被定義為是遞迴:它直接存放了另一個相同類別的值。這表示 Rust 無法計算為了存放 List 值到底需要多少空間

error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:2:1
  |
2 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
3 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
  = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable

error: aborting due to previous error

計算非遞回類別的大小

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

當 Rust 需要知道要為 Message 值分配多少空間時,它可以檢查每一個成員並發現 Message::Quit並不需要任何空間,Message::Move 需要兩個 i32 值的空間,依此類推。因此,Message 值所需的空間等於儲存其最大成員的空間大小。

當 Rust 編譯器檢查 List 這樣的遞迴類別時,編譯器嘗試計算出儲存一個 List enum 需要多少內存,並開始檢查 Cons 成員,那麼 Cons 需要的空間等於 i32 的大小加上 List 的大小。為了計算 List 需要多少內存,它檢查其成員,從 Cons 成員開始。Cons成員儲存了一個 i32 值和一個List值,這樣的計算將無限進行下去

使用 Box<T> 給遞迴類別一個已知的大小

因為 Box<T> 是一個 pointer,我們總是知道它需要多少空間:指針的大小並不會根據其指向的數據量而改變。這意味著可以將 Box 放入 Cons 成員中而不是直接存放另一個 List 值。Box 會指向另一個位於堆上的 List 值,而不是存放在 Cons 成員中。概念上,我們仍然有一個通過在其中 “存放” 其他列表創建的列表,不過現在實現這個概念的方式更像是一個項接著另一項,而不是一項包含另一項。

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));
}

Cons 將會需要一個 i32 的大小加上儲存 box 指針資料的空間。Nil 成員不儲存值,所以它比 Cons 成員需要更少的空間。現在我們知道了任何 List 值最多需要一個 i32 加上 box 指針數據的大小。使用 box ,打破了無限遞迴的連鎖,這樣編譯器就能夠計算出儲存 List 值需要的大小了。

box 只提供間接儲存與 heap 配置,沒有其他特殊功能。Box<T> 類型是一個 smart pointer,因為它實現了 Deref trait,它允許 Box<T> 值被當作引用對待。當 Box<T> 值離開作用域時,由於 Box<T> 類型 Drop trait 的實現,box 所指向的heap data也會被清除。

透過 Deref trait 以 regular reference 的方式使用 smart pointer

實作 Deref trait 可允許 overload dereference operator * ,這樣就可以用傳統的 reference 方式使用 smart pointer

以下會定義一個 MyBox<T> 類別,類似 Box<T> 但不會在 heap 儲存資料,用來說明 dereference operator 如何運作

透過 rereference operator 追蹤 pointer 的值

以下產生一個 i32 的引用,並用 dereference operator 使用該值

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

像引用一樣使用 Box<T>

y 為一個指向 x 值的 box 實例,而不是指向 x 值的引用。

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

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

自訂 smart pointer

struct MyBox<T>(T);

impl<T> MyBox<T> {
    // 獲取一個 T 類型的參數並回傳一個存放傳入值的 MyBox 實例
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

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

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

編譯錯誤

error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:15:19
   |
15 |     assert_eq!(5, *y);
   |                   ^^

error: aborting due to previous error

為了啟用 * 運算符的解引用功能,需要實現 Deref trait。

實作 Deref trait

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

use std::ops::Deref;

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

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

deref 中寫入了 &self.0,這樣 deref 回傳了希望通過 * 運算符訪問的值的引用。main 函數中對 MyBox<T> 值的 * ,現在可以編譯並能通過 assert 檢查了。

*y 實際上是 *(y.deref())

function, method 的 implicit Deref Coercions

deref coercions 是 rust 實作 function, method 傳遞參數的方法。它會將實作了 Deref 類別的 reference 轉換為 Deref 能夠從原始類別轉換而來的類別的 reference。Deref coercion converts a reference to a type that implements Deref into a reference to a type that Deref can convert the original type into.

deref coercions 會自動發生,這時候會有一系列的 deref method 被呼叫,將我們提供的類別,轉換成參數所需要的類別。

deref coercions 讓 rust programmer 編寫 function, method 呼叫時,不需要增加過多的 &* 的引用與解引用。

因為 deref coercion,可使用 MyBox<String> 的引用呼叫 hello

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

這裡使用 &m 呼叫 hello 函數,其為 MyBox<String> 值的引用。因為 MyBox<T>實現了 Deref trait,Rust 可以通過 deref&MyBox<String> 變為 &String。標準庫中提供了 String 上的 Deref 實現,其會返回字符串 slice,這可以在 Deref 的 API 文檔中看到。Rust 再次呼叫 deref&String 變為 &str,這就符合 hello 函數的定義了。

如果沒有 deref coercion,就得用這種方式撰寫

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

(*m)MyBox<String> 解引用為 String。接著 &[..] 獲取了整個 String 的字符串 slice 來匹配 hello 的簽名。

deref coercion 如何跟 mutability 交互運作

類似使用 Deref trait overload 不可變引用的 * operator, rust 提供 DerefMut trait 用於重載可變引用的 * operator

rust 在發現類別跟 trait 實作滿足三種情況時,會進行 deref coercion

  • T: Deref<Target=U> 時,從 &T&U
  • T: DerefMut<Target=U> 時,從 &mut T&mut U
  • T: Deref<Target=U> 時,從 &mut T&U

前兩個除了可變性以外,都是一樣的,第一種情況代表,如果有 &T,而 T 實作了回傳 U 類別的 Deref ,則可直接得到 &U。第二種情況表示,對可變引用也有相同的結果

第三種比較特別,rust 也能將可變引用強制轉為不可變引用,但不可變引用"不能"轉為可變引用。根據借用規則,可變引用必須是這些資料的唯一引用,否則會無法編譯。將可變引用轉為不可變引用也沒關係。將不可變引用轉為可變引用,則需要限制資料只能有一個不可變引用,但就無法遵循借用規則,因此 rust 無法將不可變引用轉換為可變引用。

透過 Drop trait 在 cleanup 時執行程式

smart pointer 次要的是 Drop trait,可在值要離開 scope 時,執行一些程式。可為所有類別提供 Drop trait 的實作,這些 code 可以用來釋放檔案或網路連結的資源。smart pointer 的 Drop 一定會被用到。例如 Box<T> 自訂了 Drop 用來釋放 box 指向的 heap。

其他程式語言必須要自行在使用 smart pointer instance 後,自己呼叫清理 memory 或資源的程式碼。rust 會在 compiler 自動插入這些離開 scope 被執行的code

指定在值離開 scope 就執行的 code 的方法就是實作 Drop trait。會需要實作 drop method,它會取得一個 self 可變引用。

在drop 呼叫 println! 觀察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.");
}

Drop trait 包含在 prelude,不需要 use

執行結果

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

利用 std::mem::drop 提早丟棄值

rust 限制不能直接呼叫 drop,但有時需要提早清理某些值。ex: 使用 smart pointer 管理 locks 時,可能會需要強制呼叫 drop 釋放 lock,但 rust 有限制,故需要改用 std::mem::drop

fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");

    // 編譯錯誤
    c.drop();
    println!("CustomSmartPointer dropped before the end of main.");
}
error[E0040]: explicit use of destructor method
  --> src/main.rs:14:7
   |
14 |     c.drop();
   |       ^^^^ explicit destructor calls not allowed

error: aborting due to previous error

改使用 prelude 裡面的 std::mem::drop

fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

執行結果

CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

不需擔心 drop 正在使用的值,因為 rust compiler 能夠檢查出來

ownership 系統可確保 drop 只在值不被使用時,被呼叫一次

Rc Reference Counted Smart Pointer

為了多所有權,rust 有 Rc<T> 的類別,稱為 reference counting。引用計數可記錄一個值被引用的次數,確認該值是否仍被使用,如果是 0,就可以被清理。

Rc<T> 用在當我們希望在 heap 配置一些資料,讓程式多個地方讀取,但無法再編一時,就知道程式哪一個部分最後結束,不使用它。注意 Rc<T> 只能用在單一線程的狀況。

chat16 會討論多線程如何進行 引用計數。

利用 Rc<T> 共享資料

想產生兩個 (b,c) 共享第三個 list (a) 所有權的 list

這種方式不能運作

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5,
        Box::new(Cons(10,
            Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

編譯錯誤

error[E0382]: use of moved value: `a`
  --> src/main.rs:13:30
   |
9  |     let a = Cons(5,
   |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
...
12 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
13 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move

因為 Cons 擁有儲存的資料,產生 b 時,a 被移動到 b,b 擁有 a,再用 a 產生 c 就會發生編譯錯誤

改用 Rc<List> ,產生 b 時,不會獲取 a,這邊會複製 a 裡面的 Rc,將引用計數由 1 增加為 2。產生 c 時,引用計數由 2 增加為 3。每次呼叫 Rc::clone,Rc 中的資料的引用計數,都會增加。

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

必須 use std::rc::Rc;因為不在 prelude 裡面。

也可呼叫 a.clone(),而不是 Rc::clone(&a),但 rust 習慣使用 Rc::clone。因為 Rc::clone 不像大部分類別的 clone 會進行 deep copy

cloning Rc<T> 會增加 reference count

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    // a 中 Rc<List> 的初始引用計數為一,接著每次調用 clone,計數會增加一
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    // Drop trait 的實現,當 Rc<T> 值離開作用域時自動減少引用計數
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

執行結果

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

Rc<T> 允許通過不可變引用,讓程式的多個部分讀取數據。如果 Rc<T> 也允許多個可變引用,則會違反第四章討論的借用規則之一:相同位置的多個可變借用可能造成資料競爭和不一致。不過可以修改資料是非常有用的!在下一部分,我們將討論內部可變性模式和 RefCell<T> 類型,它可以與 Rc<T> 結合使用來處理不可變性的限制。

RefCell 及 interior mutability pattern

interior mutability pattern 是 rust 的一個 design pattern。可在有不可變引用時,改變資料,通常這是借用規則中不允許的。為了改變資料,要在資料結構上,使用 unsafe 模糊 rust 既有的可變性及借用歸則 (chap19)。當可確保程式碼在運作時,會遵守借用規則,即使編譯器不能確認的狀況,可以選用內部可變性模式的類別。相關的 unsafe code 會被封裝到安全的 API 中,但外部類別不變。

利用 RefCell<T> 在執行時檢查借用規則

不同於 Rc<T>RefCell<T> 代表其資料的唯一的所有權。讓 RefCell<T> 不同於像 Box<T> 這樣的類別的原因是:回憶一下第四章所學的借用規則

  1. 在任意給定時間,只能擁有一個可變引用或任意數量的不可變引用 之一(而不是全部)。
  2. 引用必須永遠有效

對於引用和 Box<T>,借用規則的不可變性作用於編譯時期。對於 RefCell<T>,這些不可變性作用於 執行時期。對於引用,如果違反這些規則,會得到一個編譯錯誤。而對於 RefCell<T>,如果違反這些規則程式會 panic 並 exit。

在編譯時檢查借用規則的優勢是這些錯誤將在開發過程的早期被捕獲,同時對沒有執行時的性能影響,因為所有的分析都提前完成了。在編譯時檢查借用規則是大部分情況的最佳選擇,這也正是其為何是 Rust 的默認行為。

相反在執行時檢查借用規則的好處則是允許出現特定 memory-safe 的場景,而它們是不允許在編譯時被檢查。靜態分析,正如 Rust 編譯器,是天生保守的。程式的一些屬性則不可能通過分析代碼發現:其中最著名的就是 停機問題(Halting Problem)

因為有時候無法進行分析,如果 Rust 編譯器不能通過所有權規則編譯,它可能會拒絕一個正確的程式;從這種角度考慮它是保守的。如果 Rust 接受不正確的程序,那麼用戶也就不會相信 Rust 所做的保證了。然而,如果 Rust 拒絕正確的程序,雖然會給程序員帶來不便,但不會帶來災難。RefCell<T> 正是用於當你確信程式碼遵守借用規則,而編譯器不能理解和確定的時候。

類似於 Rc<T>RefCell<T> 只能用於單線程場景。如果嘗試在多線程上下文中使用RefCell<T>,會得到一個編譯錯誤。chap 16 會介紹如何在多線程程序中使用 RefCell<T> 的功能。

如下為選擇 Box<T>Rc<T>RefCell<T> 的理由:

  • Rc<T> 允許相同資料有多個所有者;Box<T>RefCell<T> 有單一所有者。
  • Box<T> 允許在編譯時執行不可變或可變借用檢查;Rc<T>僅允許在編譯時執行不可變借用檢查;RefCell<T> 允許在運行時執行不可變或可變借用檢查。
  • 因為 RefCell<T> 允許在執行時進行可變借用檢查,所以我們可以在即使 RefCell<T> 自身是不可變的情況下修改其內部的值。

在不可變值內部改變值就是 內部可變性 模式。讓我們看看何時內部可變性是有用的,並討論這是如何成為可能的。

Interior Mutability 內部可變性: A Mutable Borrow to an Immutable Value 不可變值的可變借用

借用規則的一個推論是當有一個不可變值時,不能可變的借用它。例如,如下程式不能編譯:

fn main() {
    let x = 5;
    let y = &mut x;
}

然而,特定情況下在值的方法內部能夠修改自己是很有用的;而在其他程式碼中,值仍然是不可變。在該值方法外部的程式碼不能修改其值。RefCell<T> 是一個獲得內部可變性的方法。RefCell<T>並沒有完全繞開借用規則,編譯器中的借用檢查器允許內部可變性並相應的在執行時檢查借用規則。如果違反了這些規則,會得到 panic! 而不是編譯錯誤。

讓我們通過一個實際的例子來瞭解何處可以使用 RefCell<T> 來修改不可變值並看看為何這麼做是有意義的。

interior mutability 實例:mock object

test double (測試替身) 是一個通用程式概念,可在測試中,替代某個類別的類別。mock object 是特定類別的測試替身,可記錄測試過程中發生的事情,用以確認測試是正確的。

雖然 rust 沒有與其他程式語言中完全相同的物件,誒沒有在 std library 建立 mock object。我們可自行產生一個跟 mock object 有相同功能的 struct。

ex: 一個記錄某個值與最大值的差距的library,並根據當前值與最大值的差距來發送消息。這個 library 可以用來記錄用戶所允許的呼叫 API 次數限額。

該library只提供記錄與最大值的差距,以及何種情況發送什麼消息的功能。使用此庫的程式則期望提供實際發送消息的機制:程序可以選擇記錄一條消息、發送 email、發送短信等等。庫本身無需知道這些細節;只需實現其提供的 Messenger trait 即可。

記錄某個值與最大值差距的library,並根據此值的特定級別發出警告

lib.rs

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: 'a + Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
    where T: Messenger {
    pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 0.75 && percentage_of_max < 0.9 {
            self.messenger.send("Warning: You've used up over 75% of your quota!");
        } else if percentage_of_max >= 0.9 && percentage_of_max < 1.0 {
            self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        }
    }
}

程式中一個重要部分是擁有一個方法 sendMessenger trait,它會獲取一個 self 的不可變引用和文字信息。這是我們的 mock 對象所需要擁有的 API interface。另一個重要的部分是我們需要測試 LimitTrackerset_value 方法的行為。可以改變傳遞的 value 參數的值,不過 set_value並沒有返回任何可供 assert 的值。也就是說,如果使用某個實現了 Messenger trait 的值和特定的 max 創建 LimitTracker,當傳遞不同 value 值時,消息發送者應被告知發送合適的消息。

我們所需的 mock object 是,呼叫 send 不同於實際發送 email 或短息,其只記錄信息被通知要發送了。可以產生一個 mock object instance,用其創建 LimitTracker,呼叫 LimitTrackerset_value方法,然後檢查 mock 對象是否有我們期望的消息。以下是展示了一個如此嘗試的 mock object 實作,不過借用檢查器並不允許這樣做:

lib.rs

#[cfg(test)]
mod tests {
    use super::*;

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger { sent_messages: vec![] }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}

測試定義了一個 MockMessenger struct,其 sent_messages 字段為一個 String 值的 Vec用來記錄被告知發送的消息。我們還定義了一個關聯函數 new 以便於產生從空消息列表開始的 MockMessenger 值。接著為 MockMessenger 實現 Messenger trait 這樣就可以為 LimitTracker提供一個 MockMessenger。在 send 方法的定義中,獲取傳入的消息作為參數並儲存在 MockMessengersent_messages 列表中。

在測試中,我們測試了當 LimitTracker 被告知將 value 設置為超過 max 值 75% 的某個值。首先新建一個 MockMessenger,其從空消息列表開始。接著新建一個 LimitTracker 並傳遞新建 MockMessenger 的引用和 max 值 100。我們使用值 80 呼叫 LimitTrackerset_value 方法,這超過了 100 的 75%。接著 assert MockMessenger 中記錄的消息列表應該有一條消息。

error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:52:13
   |
51 |         fn send(&self, message: &str) {
   |                 ----- help: consider changing this to be a mutable reference: `&mut self`
52 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

error: aborting due to previous error

不能修改 MockMessenger 來記錄消息,因為 send 方法獲取 self 的不可變引用。我們也不能參考錯誤文本的建議使用 &mut self 替代,因為這樣 send 的簽名就不符合 Messenger trait 定義中的簽名了(請隨意嘗試如此修改並看看會出現什麼錯誤信息)。

這正是內部可變性的用武之地!我們將通過 RefCell 來儲存 sent_messages,然而 send 將能夠修改 sent_messages 並儲存消息。

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger { sent_messages: RefCell::new(vec![]) }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);
        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

現在 sent_messages 的類別是 RefCell<Vec<String>> 而不是 Vec<String>。在 new 函數中新建了一個 RefCell instance 替代空 vector。

對於 send 方法的實現,第一個參數仍為 self 的不可變借用,這是符合方法定義的。我們呼叫 self.sent_messagesRefCellborrow_mut 方法來獲取 RefCell 中值的可變引用,這是一個 vector。接著可以對 vector 的可變引用呼叫 push 以便記錄測試過程中看到的消息。

最後必須做出的修改位於斷言中:為了看到其內部 vector 中有多少個項,需要調用 RefCellborrow 以獲取 vector 的不可變引用。

在 runtime 以 RefCell<T> 追蹤借用

當產生不可變和可變引用時,我們分別使用 &&mut 語法。對於 RefCell<T> 來說,則是 borrowborrow_mut 方法,這屬於 RefCell<T> 安全 API 的一部分。

borrow 方法回傳 Ref 類別的 smart pointer,borrow_mut 方法返回 RefMut 類別的 smart pointer。這兩個類別都實現了 Deref 所以可以當作一般引用。

RefCell<T> 記錄當前有多少個活動的 Ref<T>RefMut<T> 智能指針。每次呼叫 borrowRefCell<T> 將活動的不可變借用計數加一。當 Ref 值離開作用域時,不可變借用計數減一。就像編譯時借用規則一樣,RefCell<T> 在任何時候只允許有多個不可變借用或一個可變借用。

如果我們嘗試違反這些規則,相比引用時的編譯時錯誤,RefCell<T> 的實現會在運行時 panic!。示例 15-23 展示了對示例 15-22 中 send 實現的修改,這裡我們故意嘗試在相同作用域產生兩個可變借用以便demo RefCell<T> 不允許我們在運行時這麼做:

impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
        let mut one_borrow = self.sent_messages.borrow_mut();
        let mut two_borrow = self.sent_messages.borrow_mut();

        one_borrow.push(String::from(message));
        two_borrow.push(String::from(message));
    }
}
test tests::it_sends_an_over_75_percent_warning_message ... FAILED

failures:

---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at 'already borrowed: BorrowMutError', src/libcore/result.rs:997:5
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

使用 RefCell 使得在只允許不可變值的上下文中編寫修改自身以記錄消息的 mock object 成為可能。雖然有取捨,但是我們可以選擇使用 RefCell<T> 來獲得比一般引用所能提供的更多的功能。

組合 Rc<T> and RefCell<T> 讓 mutable data 能有多個 owner

RefCell<T> 的一個常見用法是與 Rc<T> 結合。

Rc<T> 允許對相同數據有多個所有者,不過只能提供資料的不可變訪問。如果有一個儲存了 RefCell<T>Rc<T> 的話,就可以得到有多個所有者 並且 可以修改的值了!

例如,ex: 15-18 的 cons list 的例子中使用 Rc<T> 使得多個列表共享另一個列表的所有權。因為 Rc<T> 只存放不可變值,所以一旦創建了這些列表值後就不能修改。讓我們加入 RefCell<T> 來獲得修改列表中值的能力。示例 15-24 展示了通過在 Cons 定義中使用 RefCell<T>,我們就允許修改所有列表中的值了:

Rc<RefCell<i32>> 產生可以修改的 List

main.rs

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

這裡產生了一個 Rc<RefCell<i32> instance 並儲存在變數 value 中以便之後直接訪問。接著在 a 中用包含 valueCons 成員產生了一個 List。需要 clone value 以便 avalue 都能擁有其內部值 5 的所有權,而不是將所有權從 value 移動到 a 或者讓 a 借用 value

我們將列表 a 封裝進了 Rc<T> 這樣當產生列表 bc 時,他們都可以引用 a,正如示例 15-18 一樣。

一旦產生了列表 abc,我們將 value 的值加 10。為此對 value 呼叫了 borrow_mut,這裡使用了第五章討論的自動解引用功能(“-> 運算符到哪去了?” 部分)來解引用 Rc<T> 以獲取其內部的 RefCell<T> 值。borrow_mut 方法回傳 RefMut<T> 智能指針,可以對其使用解引用運算符並修改其內部值。

當我們列印出 abc 時,可以看到他們都擁有修改後的值 15 而不是 5:

a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))

使用 RefCell<T>,我們可以擁有一個表面上不可變的 List,不過可以使用 RefCell<T> 中提供內部可變性的方法來在需要時修改數據。RefCell<T> 的執行時借用規則檢查也確實保護我們免於出現數據競爭,而且我們也能犧牲一些速度來換取資料結構的靈活性。

std library 中也有其他提供內部可變性的類型,比如 Cell<T>,它有些類似(RefCell<T>)除了相比提供內部值的引用,其值被拷貝進和拷貝出 Cell<T>。還有 Mutex<T>,其提供線程間安全的內部可變性,下一章並發會討論它的應用。請查看標準庫來獲取更多細節和不同類型之間的區別。

reference cycle 會造成 memory leak

Rust 的內存安全保證難以意外地製造永遠也不會被清理的內存(被稱為 內存洩露memory leak)),但並不是不可能。與在編譯時拒絕資料競爭不同, Rust 並不保證完全避免內存洩露,這意味著內存洩露在 Rust 被認為是內存安全的。這一點可以通過 Rc<T>RefCell<T> 看出:有可能會創建個個項之間相互引用的引用。這會造成內存洩露,因為每一項的引用計數將永遠也到不了 0,其值也永遠也不會被丟棄。

製造 reference cycle

一個存放 RefCell 的 cons list 定義,可以修改 Cons 成員所引用的資料

main.rs

use std::rc::Rc;
use std::cell::RefCell;
use crate::List::{Cons, Nil};

// 一個存放 RefCell 的 cons list 定義,可以修改 Cons 成員所引用的資料
// 能夠修改 Cons 成員所指向的 List
// 增加了一個 tail 方法來方便我們在有 Cons 成員的時候訪問其第二項
#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    // 在 a 產生了一個list,一個指向 a 裡面的 list 的 b list,接著修改 b 中的list指向 a 中的list,這會創建一個引用循環。
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    // 修改 a 使其指向 b 而不是 Nil
    // 用 tail 方法獲取 a 中 RefCell<Rc<List>> 的引用,並放入變數 link 中
    // 使用 RefCell<Rc<List>> 的 borrow_mut 方法將其值從存放 Nil 的 Rc 修改為 b 中的 Rc<List>
    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // 取消如下行的註釋來觀察引用循環;
    // 這會導致 stack overflow
    println!("a next item = {:?}", a.tail());
}

如果沒有最後一行,正確的執行結果

a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

最後一行發生的錯誤

thread 'main' has overflowed its stack
fatal runtime error: stack overflow
Abort trap: 6

Rc<T> 改為 Weak<T> 避免引用循環

呼叫 Rc::clone 會增加 Rc<T> instance 的 strong_count,和 Rc<T> instance 只在其 strong_count 為 0 時才會被清理。也可以呼叫 Rc::downgrade 並傳遞 Rc instance 的引用來產生其值的 弱引用weak reference)。

呼叫 Rc::downgrade 時會得到 Weak<T> 類型的智能指針。不同於將 Rc<T> 實例的 strong_count 加一,呼叫 Rc::downgrade 會將 weak_count加一。Rc<T> 類型使用 weak_count 來記錄其存在多少個 Weak<T> 引用,類似於 strong_count。其區別在於 weak_count 無需計數為 0 就能使 Rc 實例被清理。

強引用代表如何共享 Rc<T> instance 的所有權。弱引用並不代表所有權關係。他們不會造成引用循環,因為任何引入了弱引用的循環一旦所涉及的強引用計數為 0 就會被打破。

因為 Weak<T> 引用的值可能已經被丟棄了,為了使用 Weak<T> 所指向的值,我們必須確保其值仍然有效。為此可以呼叫 Weak<T> 實例的 upgrade 方法,這會回傳 Option<Rc<T>>。如果 Rc<T>值還未被丟棄則結果是 Some,如果 Rc<T> 已經被丟棄則結果是 None。因為 upgrade 回傳一個 Option<T>,Rust 會處理 SomeNone的情況,並且不會有一個無效的 pointer。

以下是一個例子,不同於使用一個某項只知道其下一項的列表,我們會產生一個某項知道其子項 父項的樹形結構。

產生 Tree Data Structure: 帶有 Child Nodes 的 Node

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

#[derive(Debug)]
// Node 的存放擁有所有權的 i32 值和其子 Node 值引用的 struct
// 希望 Node 擁有其子節點,同時也希望通過變數來共享所有權,以便可以直接訪問 tree 的每一個 Node。為此 Vec<T> 的項的類別被定義為 Rc<Node>。我們還希望能修改其他節點的子節點,所以 children 中 Vec<Rc<Node>> 被放進了 RefCell<T>
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    // 產生一個叫做 leaf 的帶有值 3 且沒有子節點的 Node instance,和另一個帶有值 5 並以 leaf 作為子節點的 branch
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    // 這裡 clone 了 leaf 中的 Rc<Node> 並儲存在了 branch 中,這表示 leaf 中的 Node 現在有兩個所有者:leaf和branch。
    // 可以透過 branch.children 從 branch 中獲得 leaf,不過無法從 leaf 到 branch。leaf 沒有到 branch 的引用且並不知道他們相互關聯。我們希望 leaf 知道 branch 是其父節點。
    let _branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

增加子到父節點的引用

為了使子節點知道其父節點,需要在 Node 定義中增加一個 parent。問題是 parent的類型應該是什麼。我們知道其不能包含 Rc<T>,因為這樣 leaf.parent 將會指向 branchbranch.children 會包含 leaf 的指針,這會形成引用循環,會造成其 strong_count 永遠也不會為 0.

現在換一種方式思考這個關係,父節點應該擁有其子節點:如果父節點被丟棄了,其子節點也應該被丟棄。然而子節點不應該擁有其父節點:如果丟棄子節點,其父節點應該依然存在。這正是弱引用的例子!

所以 parent 使用 Weak<T> 類別而不是 Rc<T>,具體來說是 RefCell<Weak<Node>>。現在 Node結構體定義看起來像這樣:

use std::rc::{Rc, Weak};
use std::cell::RefCell;

// 一個節點就能夠引用其父節點,但不擁有其父節點
#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    // leaf 擁有指向其父節點 branch 的 Weak 引用
    // leaf 一開始時沒有父節點,所以我們產生了一個空的 Weak 引用實例。
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    // 當嘗試使用 upgrade 方法獲取 leaf 的父節點引用時,會得到 None。如 println! 輸出所示:leaf parent = None
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    // 產生 branch 節點時,其也會產生一個 Weak<Node> 引用,因為 branch 並沒有父節點。leaf 仍然作為 branch 的一個子節點。一旦在 branch 中有了 Node 實例,就可以修改 leaf 使其擁有指向父結節點的 Weak<Node> 引用。
    // 這裡使用了 leaf 中 parent 裡的 RefCell<Weak<Node>> 的 borrow_mut 方法,接著使用了 Rc::downgrade 函數來從 branch 中的 Rc 值產生了一個指向 branch 的 Weak<Node> 引用。
    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    // 列印 leaf 的父節點時,這一次將會得到存放了 branch 的 Some 值

    // leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

觀察 strong_countweak_count 的變化

use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    // 產生 leaf,其 Rc<Node> 的強引用計數為 1,弱引用計數為 0。
    // leaf strong = 1, weak = 0
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );

    {
        // 在內部作用域中產生 branch 並與 leaf 相關聯,此時 branch 中 Rc<Node> 的強引用計數為 1,弱引用計數為 1(因為 leaf.parent 通過 Weak<Node> 指向 branch)
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        // branch strong = 1, weak = 1
        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        );

        // 這裡 leaf 的強引用計數為 2,因為現在 branch 的 branch.children 中儲存了 leaf 的 Rc<Node> 的拷貝,不過弱引用計數仍然為 0
        // leaf strong = 2, weak = 0
        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        );
    }

    // 當內部作用域結束時,branch 離開作用域,Rc<Node> 的強引用計數減少為 0,所以其 Node 被丟棄。來自 leaf.parent 的弱引用計數 1 與 Node 是否被丟棄無關,所以並沒有產生任何 memory leak
    // 在內部作用域結束後嘗試訪問 leaf 的父節點,會再次得到 None。在程序的結尾,leaf 中 Rc<Node> 的強引用計數為 1,弱引用計數為 0,因為現在 leaf 又是 Rc<Node> 唯一的引用了。
    // leaf parent = None
    // leaf strong = 1, weak = 0
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );
}

執行結果

leaf strong = 1, weak = 0
branch strong = 1, weak = 1
leaf strong = 2, weak = 0
leaf parent = None
leaf strong = 1, weak = 0

References

The Rust Programming Language

中文版

中文版 2

沒有留言:

張貼留言