DST(dynamic size type)-中譯“動態大小類型"。本文簡要討論動態大小類型的一些問題。
一、前言
rust作為一門靜態類型語言,和大部分其它靜態類型語言(C,C++,C#,JAVA)一樣,希望在編譯的時候知道每個實例/類型的大小。
作為靜態類型語言,優點是毋庸置疑的的:
1.類型錯誤(如字符串與整數運算)在編譯階段即可被捕獲,減少運行時崩潰風險
2.編譯器可基於類型信息優化內存分配與代碼執行效率
但無論哪一種靜態類型語言,都有同樣的問題:實際業務場景中,必然有動態大小的類型,那麼應該如何處理了?
每個靜態類型語言都有它的處理機制,但由於rust的設計哲學和目標,所以它的處理方式是非常特別的!
無論如何,rust必須能夠處理動態大小類型,否則這個語言無法用(或者變為極其難用的玩具)。
二、RUST動態類型
如前,rust也有動態類型,例如常見的str。
只是可惜的是,我們常用的其實是&str,注意不是str。如果直接let a:str="abc"是報告編譯錯誤的:
2.1、如何處理動態大小類型
rust使用了新類型設計模式(個人更願意看作是封裝模式)來解決這個問題。
具體而言就是用智能指針來解決這個問題。
如我們所知,智能指針的典型結構:一個指向數據的指針、少量的其它元數據、額外實現的一些特質(例如Deref,Drop).
因此,編譯器會把智能指針視為一個固定大小。 是的,String也可以看作式某種智能指針。
我們來看經典的Box盒子指針的定義:
pub struct Box<
T: ?Sized,
#[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
>(Unique<T>, A);
#[lang = "ptr_unique"]
pub struct Unique<T: ?Sized> {
pointer: NonNull<T>,
// NOTE: this marker has no consequences for variance, but is necessary
// for dropck to understand that we logically own a `T`.
//
// For details, see:
// https://github.com/rust-lang/rfcs/blob/master/text/0769-sound-generic-drop.md#phantom-data
_marker: PhantomData<T>,
}
pub struct NonNull<T: ?Sized> {
pointer: *const T,
}
pub struct PhantomData<T: ?Sized>;
#[unstable(feature = "allocator_api", issue = "32838")]
pub unsafe trait Allocator {
//此處略
}
注意,T是?Sized,意思T可以是固定大小或者動態大小。其次根據文檔説明 ?T(T是特質)目前只能有?Sized,換言之,不會有?Drop,?Deref之類的申明。
在Box的底層通過 *const T(不可變原始指針)來指向實際的數據,整體上可以看作是固定大小的。
2.2、Sized特質
Sized特質,故名思意就是大小的意思,在rust中表示一個特質,表示被它綁定(限定)的類型是固定大小。rust編譯器會為每一個添加了Sized特質的類型實現具體內容。
?Size則表示類型是可以固定也可以不是固定大小。其次根據文檔説明 ?T(T是特質)目前只能有?Sized,換言之,不會有?Drop,?Deref之類的申明。
看看Box的定義就是知道類型是?Sized,而實踐也告訴我們,可以在Box中存放標量類型和堆棧類型。
2.3、動態分發(dyn)
動態分發(dynamic dispatch),意思就是在運行的時候才確定特質對應的實際類型。
在編碼的時候,如果使用指針存儲一個特質,那麼必須添加一個dyn,這樣rustc通過編譯,並在運行的時候,會把特質替換為實際的類型實例。
三、示例
trait Animal {
fn eat(&self);
}
struct Tiger {
name: String,
age: u8,
}
struct Pig {
name: String,
age: u8,
}
impl Animal for Tiger {
fn eat(&self) {
println!("{}歲{} 正在吃野豬", self.age, self.name);
}
}
impl Animal for Pig {
fn eat(&self) {
println!("{}歲{} 正在吃竹筍和地瓜", self.age, self.name);
}
}
impl Tiger {
fn clone(&self) -> Tiger {
Tiger {
name: self.name.clone(),
age: self.age,
}
}
}
fn feed_animal_dyn(animals: Vec<Box<dyn Animal>>) {
for animal in animals {
animal.eat();
}
}
/**
* Sized特質測試,告知編譯器這個參數可以是固定大小也可以是動態大小,而非固定大小
*/
fn feed_animal<T: Sized>(animal: T)
where
T: Animal,
{
animal.eat();
}
fn main() {
//這樣定義會報告編譯錯誤:doesn't have a size known at compile-time
//let me:str="lzf";
dst_test();
dyn_test();
sized_test();
}
fn sized_test() {
let tiger = Tiger {
name: "大王🐯".to_string(),
age: 3,
};
let 武松的老虎 = tiger.clone();
feed_animal(武松的老虎);
}
/**
* 動態大小類型測試,使用&T和Box<T>來封裝動態大小類型(DST)
*/
fn dst_test() {
//Box指針封裝動態大小類型(DST).編譯器認為Box指針式固定大小,典型的障眼法
let code: Box<str> = Box::from("Hello, world!");
println!("{}", code);
let name:Box<&str> = Box::from("狄仁傑");
println!("{}", name);
}
/**
* 動態分發測試,使用dyn告訴編譯器這是一個動態分發,而非靜態分發,是特質而不是其它的類型(stuct,enum等)
*/
fn dyn_test() {
let tiger = Tiger {
name: "鬆崽🐅".to_string(),
age: 10,
};
let pig = Pig {
name: "小胖🐖".to_string(),
age: 5,
};
// 必須使用as 關鍵字,將Tiger和Pig轉換為特質對象(trait object)
// 必須使用dyn關鍵字,告訴編譯器這是一個動態分發(dynamic dispatch),即在運行的時候才使用具體的類型
let animals = vec![
Box::new(tiger) as Box<dyn Animal>,
Box::new(pig) as Box<dyn Animal>,
];
feed_animal_dyn(animals);
}
本例基本模仿了書本上的例子,演示了三種情況:
1.不能直接定義一個動態大小類型,否則編譯器報錯
2.如何用智能指針處理動態大小類型,以便可以通過編譯
3.Sized特質的使用,以及如何使用Box存儲特質。使用了dyn(動態分發)
動態分發(dyn)
函數fn feed_animal_dyn(animals: Vec<Box<dyn Animal>>)只接收Animal特質的Box指針向量,無法定義為:
fn feed_animal_dyn(animals: Vec<Box<Animal>>),這是因為特質本身是無意義的,必須和特定類型關聯,要關聯必然涉及到動態分發。
演示結果:

四、小結
1.絕大部分靜態類型語言都支持固定大小類型和動態大小類型,包括rust
2.rust使用指針來解決動態大小類型的編譯問題和運行問題
3.動態分發機制可以解決指針中定義特質的問題