Rust 學習筆記:Box
- Rust 學習筆記:Box<T\>
- Box\<T> 簡介
- 使用 Box\<T\> 在堆上存儲數據
- 啟用帶有 box 的遞歸類型
- 關于 cons 列表的介紹
- 計算非遞歸類型的大小
- 使用 Box\<T\> 獲取大小已知的遞歸類型
Rust 學習筆記:Box<T>
指針是在內存中包含地址的變量的一般概念。這個地址引用或“指向”其他一些數據。在 Rust 中最常見的指針類型是引用,由 & 符號表示,并借用它們所指向的值。除了引用數據之外,它們沒有任何特殊功能,也沒有開銷。
智能指針是一種像指針一樣的數據結構,但還具有額外的元數據和功能。Rust 在標準庫中定義了各種智能指針,這些指針提供的功能超出了引用所提供的功能。
具有所有權和借用概念的 Rust 在引用和智能指針之間有一個額外的區別:引用只借用數據,而在許多情況下,智能指針擁有它們所指向的數據。
我們遇到了一些智能指針:String 和 Vec<T>。這兩種類型都算作智能指針,因為它們擁有一些內存,并允許對其進行操作。它們還具有元數據和額外的功能或保證。例如,String 將其容量存儲為元數據,并具有確保其數據始終是有效的 UTF-8 的額外能力。
智能指針通常使用結構體實現。與普通結構體不同,智能指針實現了 Deref 和Drop trait。Deref trait 允許智能指針結構體的實例表現得像引用一樣,這樣就可以編寫代碼來使用引用或智能指針。Drop trait 允許自定義當智能指針的實例超出作用域時運行的代碼。
Box<T> 簡介
最直接的智能指針是 Box<T>,它運行將數據存儲在堆中而不是棧中,留在棧上的是指向堆數據的指針。
Box<T> 沒有性能開銷,但它們也沒有太多額外的功能。最常在以下情況下使用它:
-
當你的類型在編譯時無法知道其大小,并且你希望在需要精確大小的上下文中使用該類型的值時
-
當你有大量的數據,你想要轉移所有權,但要確保數據不會被復制時
-
當你想擁有一個值,你只關心它是一個實現了特定特性的類型,而不是一個特定的類型
我們將在下文中演示第一種情況。在第二種情況下,傳輸大量數據的所有權可能需要很長時間,因為數據是在棧上復制的。為了在這種情況下提高性能,我們可以將大量數據存儲在堆中的盒子中。然后,只有少量的指針數據在棧上被復制,而它引用的數據留在堆上的一個地方。第三種情況被稱為 trait 對象,后續文章將專門討論了這個主題。
使用 Box<T> 在堆上存儲數據
首先介紹 Box<T> 的語法以及如何與存儲在 Box<T> 中的值進行交互。
fn main() {let b = Box::new(5);println!("b = {b}");
}
我們將變量 b 定義為具有指向值 5 的 box 的值,該值在堆上分配。這個程序將輸出 b = 5。在這種情況下,我們可以訪問 box 中的數據,就像我們訪問棧中的數據一樣。
當一個 box 超出作用域時,就像 main 語句末尾的 b 變量那樣,它將被釋放。對 box(存儲在棧上)和它所指向的數據(存儲在堆上)都進行釋放。
將單個值放在堆上并不是很有用,在棧上使用單個 i32 這樣的值更合適。
Box<T> 在定義類型時更有用。
啟用帶有 box 的遞歸類型
遞歸類型的值可以有另一個相同類型的值作為其本身的一部分。遞歸類型造成了一個問題,因為 Rust 需要在編譯時知道一個類型占用了多少空間。然而,遞歸類型的值的嵌套理論上可以無限地繼續下去,因此 Rust 無法知道值需要多少空間。因為 box 的大小是已知的,所以我們可以通過在遞歸類型定義中插入一個 box 來啟用遞歸類型。
作為遞歸類型的一個示例,讓我們研究一下 cons 列表。這是函數式編程語言中常見的一種數據類型。
關于 cons 列表的介紹
cons 列表是一種來自 Lisp 編程語言的數據結構,由嵌套對組成,是 Lisp 版本的鏈表。它的名字來自于 Lisp 中的c ons 函數(construct function 的縮寫),它從它的兩個參數構造一個新的 pair。通過對由一個值和另一個值組成的對調用 cons,我們可以構造由遞歸對組成的 cons 列表。
例如,下面是一個 cons 列表的偽代碼表示,其中包含列表 1、2、3,每一對都在括號中:
(1, (2, (3, Nil)))
cons 列表中的每一項包含兩個元素:當前項的值和下一項的值。列表中的最后一項只包含一個名為 Nil 的值,沒有下一項。
cons 列表不是Rust中常用的數據結構。但從本章的 cons 列表開始,我們可以探索 box 如何讓我們定義遞歸數據類型。
下列代碼包含了 cons 列表的枚舉定義。
enum List {Cons(i32, List),Nil,
}
注意,這段代碼還不能編譯,因為 List 類型沒有已知的大小,我們將對此進行演示。
嘗試構建一個 cons 列表:
use crate::List::{Cons, Nil};fn main() {let list = Cons(1, Cons(2, Cons(3, Nil)));
}
第一個 Cons 值保存 1 和另一個 List 值。這個 List 值是另一個 Cons 值,它包含 2 和另一 List 值。這個 List 值是另一個 Cons 值,它包含 3 和一個 List 值,最后是 Nil,這是表示列表結束的非遞歸變體。
嘗試運行這段代碼,報錯:
錯誤顯示 List 類型“具有無限大小”。原因是我們用遞歸的變量定義了 List:它直接保存自身的另一個值。因此,Rust 無法計算出它需要多少空間來存儲 List 值。
讓我們分析一下為什么會出現這個錯誤。首先,我們來看一下 Rust 如何決定存儲非遞歸類型的值需要多少空間。
計算非遞歸類型的大小
以一個 Message 枚舉為例:
enum Message {Quit,Move { x: i32, y: i32 },Write(String),ChangeColor(i32, i32, i32),
}
為了確定為 Message 值分配多少空間,Rust 遍歷每個變體,以查看哪個變體需要最多的空間。Message::Quit 不需要任何空間,Message::Move 占用兩個 i32 值大小的空間,以此類推。因為只使用一個變體,所以 Message 值所需的最大空間就是存儲其最大變體所需的空間。
與此形成對比的是,當 Rust 試圖確定 List 枚舉這樣的遞歸類型需要多少空間時發生的情況。編譯器首先查看 Cons 變量,它包含一個 i32 類型的值和一個 List 類型的值。因此,Cons 需要的空間量等于 i32 的大小加上 List 的大小。為了計算出 List 類型需要多少內存,編譯器從 Cons 變量開始,這個過程無限地繼續下去。
使用 Box<T> 獲取大小已知的遞歸類型
因為 Rust 不能計算出為遞歸定義的類型分配多少空間,編譯器給出了一個錯誤,并給出了這個有用的建議:
在這個建議中,間接意味著不是直接存儲一個值,而是通過存儲指向該值的指針來改變數據結構,從而間接存儲該值。
因為 Box<T> 是一個指針,指針的大小不會根據它所指向的數據量而改變。這意味著我們可以在 Cons 變量中放入 Box<T>,而不是直接放入另一個 List 值。Box<T> 將指向下一個 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<T> 指針。Nil 不存儲任何值,因此它比 Cons 變量需要更少的空間。通過使用盒子,我們打破了無限的遞歸鏈,因此編譯器可以計算出存儲 List 值所需的大小。
盒子只提供間接分配和堆分配,沒有任何其他特殊功能,也沒有這些特殊功能所帶來的性能開銷,因此它們在像 cons 列表這樣的情況下非常有用,其中間接是我們唯一需要的特性。
Box<T> 類型是一個智能指針,因為它實現了 Deref trait,它允許 Box<T> 值被當作引用來對待。當 Box<T> 值超出作用域時,由于 Drop trait 的實現,該指針所指向的堆數據也會被清理。