Rust 中的資料佈局--非正常大小的型別
非正常大小的型別
大多數的時候,我們期望型別在編譯時能夠有一個靜態已知的非零大小,但這並不總是 Rust 的常態。
Dynamically Sized Types (DSTs)
Rust 支援動態大小的型別(DST):這些型別沒有靜態(編譯時)已知的大小或者佈局。從表面上看這有點離譜:Rust 必須知道一個東西的大小和佈局,才能正確地進行處理。從這個角度上看,DST 不是一個普通的型別,因為它們沒有編譯時靜態可知的大小,它們只能存在於一個指標之後。任何指向 DST 的指標都會變成一個包含了完善 DST 型別資訊的胖指標(詳情見下方)。
Rust 暴露了兩種主要的 DST 型別:
Trait 物件代表某種型別,實現了它所指定的 Trait。確切的原始型別被刪除,以利於執行時的反射,其中包含使用該型別的所有必要資訊的 vtable。補全 Trait 物件指標所需的資訊是 vtable 指標,被指向的物件的執行時的大小可以從 vtable 中動態地獲取。
一個 slice 只是一些只讀的連續儲存——通常是一個數組或Vec
。補全一個 slice 指標所需的資訊只是它所指向的元素的數量,指標的執行時大小隻是靜態已知元素的大小乘以元素的數量。
結構實際上可以直接儲存一個 DST 作為其最後一個欄位,但這也會使它們自身成為一個 DST:
如果這樣的型別沒有方法來構造它,那麼它在很大程度上來看是沒啥用的。目前,唯一支援的建立自定義 DST 的方法是使你的型別成為泛型,並執行非固定大小轉換(unsizing coercion):
struct MySuperSliceable<T: ?Sized> {
info: u32,
data: T,
}
fn main() {
let sized: MySuperSliceable<[u8; 8]> = MySuperSliceable {
info: 17,
data: [0; 8],
};
let dynamic: &MySuperSliceable<[u8]> = &sized;
// 輸出:"17 [0, 0, 0, 0, 0, 0, 0, 0]"
println!("{} {:?}", dynamic.info, &dynamic.data);
}
(是的,自定義 DST 目前僅僅是一個基本半成品的功能。)
零大小型別 (ZSTs)
Rust 也允許型別指定他們不佔空間:
就其本身而言,零尺寸型別(ZSTs)由於顯而易見的原因是相當無用的。然而,就像 Rust 中許多奇怪的佈局選擇一樣,它們的潛力在通用語境中得以實現。在 Rust 中,任何產生或儲存 ZST 的操作都可以被簡化為無操作(no-op)。首先,儲存它甚至沒有意義——它不佔用任何空間。另外,這種型別的值只有一個,所以任何載入它的操作都可以直接憑空產生它——這也是一個無操作(no-op),因為它不佔用任何空間。
這方面最極端的例子之一是 Set 和 Map。給定一個Map<Key, Value>
,通常可以實現一個Set<Key>
,作為Map<Key, UselessJunk>
的一個薄封裝。在許多語言中,這將需要為無用的封裝分配空間,並進行儲存和載入無用封裝的工作,然後將其丟棄。對於編譯器來說,證明這一點是不必要的,是一個困難的分析。
然而在 Rust 中,我們可以直接說Set<Key> = Map<Key, ()>
。現在 Rust 靜態地知道每個載入和儲存都是無用的,而且沒有分配有任何大小。其結果是,單例化的程式碼基本上是 HashSet 的自定義實現,而沒有 HashMap 要支援值所帶來的開銷。
安全的程式碼不需要擔心 ZST,但是不安全的程式碼必須小心沒有大小的型別的後果。特別是,指標偏移是無操作的,而分配器通常需要一個非零的大小。
請注意,對 ZST 的引用(包括空片),就像所有其他的引用一樣,必須是非空的,並且適當地對齊。解引用 ZST 的空指標或未對齊指標是未定義的行為,就像其他型別的引用一樣。
空型別
Rust 還允許宣告不能被例項化的型別。這些型別只能在型別層討論,而不能在值層討論。空型別可以通過指定一個沒有變體的列舉來宣告:
空型別甚至比 ZST 更加邊緣化。空型別的主要作用是為了讓某個型別不可達。例如,假設一個 API 需要在一般情況下返回一個結果,但一個特定的情況實際上是不可能的。實際上可以通過返回一個Result<T, Void>
來在型別級別上傳達這個資訊。API 的消費者可以放心地 unwrap 這樣一個結果,因為他們知道這個值在本質上不可能是Err
,因為這需要提供一個Void
型別的值。
原則上,Rust 可以基於這個事實做一些有趣的分析和優化,例如,Result<T, Void>
只表示為T
,因為Err
的情況實際上並不存在(嚴格來說,這只是一種優化,並不保證,所以例如將一個轉化為另一個仍然是 UB)。
比如以下的例子,曾經是可以編譯成功的:
但現在,已經不讓這麼玩兒了。
關於空型別的最後一個微妙的細節是,構造一個指向它們的原始指標實際上是有效的,但對它們的解引用是未定義行為,因為那是沒有意義的。
我們建議不要用*const Void
來模擬 C 的void*
型別。很多人之前這樣做,但很快就遇到了麻煩,因為 Rust 沒有任何安全防護措施來防止用不安全的程式碼來例項化空型別,如果你這樣做了,就是未定義行為。因為開發者有將原始指標轉換為引用的習慣,而構造一個&Void
也是未定義行為,所以這尤其成問題。
*const ()
(或等價物)對void*
來說效果相當好,可以做成引用而沒有任何安全問題。它仍然不能阻止你試圖讀取或寫入數值,但至少它可以編譯成一個 no-op 而不是 UB。
外部型別
有一個已被接受的 RFC 來增加具有未知大小的適當型別,稱為 extern 型別,這將讓 Rust 開發人員更準確地模擬像 C 的void*
和其他“宣告但從未定義”的型別。然而,截至 Rust 2018,該功能在size_of_val::<MyExternType>()
應該如何表現方面遇到了一些問題。