ch19-04-advanced-types.md
commit a90f07f1e9a7fc75dc9105a6c6f16d5c13edceb0
Rust 的類型系統(tǒng)有一些我們曾經(jīng)提到但沒有討論過的功能。首先我們從一個關(guān)于為什么 newtype 與類型一樣有用的更寬泛的討論開始。接著會轉(zhuǎn)向類型別名(type aliases),一個類似于 newtype 但有著稍微不同的語義的功能。我們還會討論 !
類型和動態(tài)大小類型。
這一部分假設(shè)你已經(jīng)閱讀了之前的 “newtype 模式用于在外部類型上實(shí)現(xiàn)外部 trait” 部分。
newtype 模式可以用于一些其他我們還未討論的功能,包括靜態(tài)的確保某值不被混淆,和用來表示一個值的單元。實(shí)際上示例 19-15 中已經(jīng)有一個這樣的例子:Millimeters
和 Meters
結(jié)構(gòu)體都在 newtype 中封裝了 u32
值。如果編寫了一個有 Millimeters
類型參數(shù)的函數(shù),不小心使用 Meters
或普通的 u32
值來調(diào)用該函數(shù)的程序是不能編譯的。
另一個 newtype 模式的應(yīng)用在于抽象掉一些類型的實(shí)現(xiàn)細(xì)節(jié):例如,封裝類型可以暴露出與直接使用其內(nèi)部私有類型時所不同的公有 API,以便限制其功能。
newtype 也可以隱藏其內(nèi)部的泛型類型。例如,可以提供一個封裝了 HashMap<i32, String>
的 People
類型,用來儲存人名以及相應(yīng)的 ID。使用 People
的代碼只需與提供的公有 API 交互即可,比如向 People
集合增加名字字符串的方法,這樣這些代碼就無需知道在內(nèi)部我們將一個 i32
ID 賦予了這個名字了。newtype 模式是一種實(shí)現(xiàn)第十七章 “封裝隱藏了實(shí)現(xiàn)細(xì)節(jié)” 部分所討論的隱藏實(shí)現(xiàn)細(xì)節(jié)的封裝的輕量級方法。
連同 newtype 模式,Rust 還提供了聲明 類型別名(type alias)的能力,使用 type
關(guān)鍵字來給予現(xiàn)有類型另一個名字。例如,可以像這樣創(chuàng)建 i32
的別名 Kilometers
:
type Kilometers = i32;
這意味著 Kilometers
是 i32
的 同義詞(synonym);不同于示例 19-15 中創(chuàng)建的 Millimeters
和 Meters
類型。Kilometers
不是一個新的、單獨(dú)的類型。Kilometers
類型的值將被完全當(dāng)作 i32
類型值來對待:
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
println!("x + y = {}", x + y);
因?yàn)?nbsp;Kilometers
是 i32
的別名,他們是同一類型,可以將 i32
與 Kilometers
相加,也可以將 Kilometers
傳遞給獲取 i32
參數(shù)的函數(shù)。但通過這種手段無法獲得上一部分討論的 newtype 模式所提供的類型檢查的好處。
類型別名的主要用途是減少重復(fù)。例如,可能會有這樣很長的類型:
Box<dyn Fn() + Send + 'static>
在函數(shù)簽名或類型注解中每次都書寫這個類型將是枯燥且易于出錯的。想象一下如示例 19-24 這樣全是如此代碼的項(xiàng)目:
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
// --snip--
}
fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
// --snip--
}
示例 19-24: 在很多地方使用名稱很長的類型
類型別名通過減少項(xiàng)目中重復(fù)代碼的數(shù)量來使其更加易于控制。這里我們?yōu)檫@個冗長的類型引入了一個叫做 Thunk
的別名,這樣就可以如示例 19-25 所示將所有使用這個類型的地方替換為更短的 Thunk
:
type Thunk = Box<dyn Fn() + Send + 'static>;
let f: Thunk = Box::new(|| println!("hi"));
fn takes_long_type(f: Thunk) {
// --snip--
}
fn returns_long_type() -> Thunk {
// --snip--
}
示例 19-25: 引入類型別名 Thunk
來減少重復(fù)
這樣讀寫起來就容易多了!為類型別名選擇一個好名字也可以幫助你表達(dá)意圖(單詞 thunk 表示會在之后被計(jì)算的代碼,所以這是一個存放閉包的合適的名字)。
類型別名也經(jīng)常與 Result<T, E>
結(jié)合使用來減少重復(fù)??紤]一下標(biāo)準(zhǔn)庫中的 std::io
模塊。I/O 操作通常會返回一個 Result<T, E>
,因?yàn)檫@些操作可能會失敗。標(biāo)準(zhǔn)庫中的 std::io::Error
結(jié)構(gòu)體代表了所有可能的 I/O 錯誤。std::io
中大部分函數(shù)會返回 Result<T, E>
,其中 E
是 std::io::Error
,比如 Write
trait 中的這些函數(shù):
use std::fmt;
use std::io::Error;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
fn flush(&mut self) -> Result<(), Error>;
fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}
這里出現(xiàn)了很多的 Result<..., Error>
。為此,std::io
有這個類型別名聲明:
type Result<T> = std::result::Result<T, std::io::Error>;
因?yàn)檫@位于 std::io
中,可用的完全限定的別名是 std::io::Result<T>
—— 也就是說,Result<T, E>
中 E
放入了 std::io::Error
。Write
trait 中的函數(shù)最終看起來像這樣:
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
類型別名在兩個方面有幫助:易于編寫 并 在整個 std::io
中提供了一致的接口。因?yàn)檫@是一個別名,它只是另一個 Result<T, E>
,這意味著可以在其上使用 Result<T, E>
的任何方法,以及像 ?
這樣的特殊語法。
Rust 有一個叫做 !
的特殊類型。在類型理論術(shù)語中,它被稱為 empty type,因?yàn)樗鼪]有值。我們更傾向于稱之為 never type。這個名字描述了它的作用:在函數(shù)從不返回的時候充當(dāng)返回值。例如:
fn bar() -> ! {
// --snip--
}
這讀 “函數(shù) bar
從不返回”,而從不返回的函數(shù)被稱為 發(fā)散函數(shù)(diverging functions)。不能創(chuàng)建 !
類型的值,所以 bar
也不可能返回值。
不過一個不能創(chuàng)建值的類型有什么用呢?如果你回想一下示例 2-5 中的代碼,曾經(jīng)有一些看起來像這樣的代碼,如示例 19-26 所重現(xiàn)的:
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
示例 19-26: match
語句和一個以 continue
結(jié)束的分支
當(dāng)時我們忽略了代碼中的一些細(xì)節(jié)。在第六章 “match 控制流運(yùn)算符” 部分,我們學(xué)習(xí)了 match
的分支必須返回相同的類型。如下代碼不能工作:
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
};
這里的 guess
必須既是整型 也是 字符串,而 Rust 要求 guess
只能是一個類型。那么 continue
返回了什么呢?為什么示例 19-26 中會允許一個分支返回 u32
而另一個分支卻以 continue
結(jié)束呢?
正如你可能猜到的,continue
的值是 !
。也就是說,當(dāng) Rust 要計(jì)算 guess
的類型時,它查看這兩個分支。前者是 u32
值,而后者是 !
值。因?yàn)?nbsp;!
并沒有一個值,Rust 決定 guess
的類型是 u32
。
描述 !
的行為的正式方式是 never type 可以強(qiáng)轉(zhuǎn)為任何其他類型。允許 match
的分支以 continue
結(jié)束是因?yàn)?nbsp;continue
并不真正返回一個值;相反它把控制權(quán)交回上層循環(huán),所以在 Err
的情況,事實(shí)上并未對 guess
賦值。
never type 的另一個用途是 panic!
。還記得 Option<T>
上的 unwrap
函數(shù)嗎?它產(chǎn)生一個值或 panic。這里是它的定義:
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
這里與示例 19-34 中的 match
發(fā)生了相同的情況:Rust 知道 val
是 T
類型,panic!
是 !
類型,所以整個 match
表達(dá)式的結(jié)果是 T
類型。這能工作是因?yàn)?nbsp;panic!
并不產(chǎn)生一個值;它會終止程序。對于 None
的情況,unwrap
并不返回一個值,所以這些代碼是有效的。
最后一個有著 !
類型的表達(dá)式是 loop
:
print!("forever ");
loop {
print!("and ever ");
}
這里,循環(huán)永遠(yuǎn)也不結(jié)束,所以此表達(dá)式的值是 !
。但是如果引入 break
這就不為真了,因?yàn)檠h(huán)在執(zhí)行到 break
后就會終止。
因?yàn)?Rust 需要知道例如應(yīng)該為特定類型的值分配多少空間這樣的信息其類型系統(tǒng)的一個特定的角落可能令人迷惑:這就是 動態(tài)大小類型(dynamically sized types)的概念。這有時被稱為 “DST” 或 “unsized types”,這些類型允許我們處理只有在運(yùn)行時才知道大小的類型。
讓我們深入研究一個貫穿本書都在使用的動態(tài)大小類型的細(xì)節(jié):str
。沒錯,不是 &str
,而是 str
本身。str
是一個 DST;直到運(yùn)行時我們都不知道字符串有多長。因?yàn)橹钡竭\(yùn)行時都不能知道其大小,也就意味著不能創(chuàng)建 str
類型的變量,也不能獲取 str
類型的參數(shù)。考慮一下這些代碼,他們不能工作:
let s1: str = "Hello there!";
let s2: str = "How's it going?";
Rust 需要知道應(yīng)該為特定類型的值分配多少內(nèi)存,同時所有同一類型的值必須使用相同數(shù)量的內(nèi)存。如果允許編寫這樣的代碼,也就意味著這兩個 str
需要占用完全相同大小的空間,不過它們有著不同的長度。這也就是為什么不可能創(chuàng)建一個存放動態(tài)大小類型的變量的原因。
那么該怎么辦呢?你已經(jīng)知道了這種問題的答案:s1
和 s2
的類型是 &str
而不是 str
。如果你回想第四章 “字符串 slice” 部分,slice 數(shù)據(jù)結(jié)構(gòu)儲存了開始位置和 slice 的長度。
所以雖然 &T
是一個儲存了 T
所在的內(nèi)存位置的單個值,&str
則是 兩個 值:str
的地址和其長度。這樣,&str
就有了一個在編譯時可以知道的大?。核?nbsp;usize
長度的兩倍。也就是說,我們總是知道 &str
的大小,而無論其引用的字符串是多長。這里是 Rust 中動態(tài)大小類型的常規(guī)用法:他們有一些額外的元信息來儲存動態(tài)信息的大小。這引出了動態(tài)大小類型的黃金規(guī)則:必須將動態(tài)大小類型的值置于某種指針之后。
可以將 str
與所有類型的指針結(jié)合:比如 Box<str>
或 Rc<str>
。事實(shí)上,之前我們已經(jīng)見過了,不過是另一個動態(tài)大小類型:trait。每一個 trait 都是一個可以通過 trait 名稱來引用的動態(tài)大小類型。在第十七章 “為使用不同類型的值而設(shè)計(jì)的 trait 對象” 部分,我們提到了為了將 trait 用于 trait 對象,必須將他們放入指針之后,比如 &dyn Trait
或 Box<dyn Trait>
(Rc<dyn Trait>
也可以)。
為了處理 DST,Rust 有一個特定的 trait 來決定一個類型的大小是否在編譯時可知:這就是 Sized
trait。這個 trait 自動為編譯器在編譯時就知道大小的類型實(shí)現(xiàn)。另外,Rust 隱式的為每一個泛型函數(shù)增加了 Sized
bound。也就是說,對于如下泛型函數(shù)定義:
fn generic<T>(t: T) {
// --snip--
}
實(shí)際上被當(dāng)作如下處理:
fn generic<T: Sized>(t: T) {
// --snip--
}
泛型函數(shù)默認(rèn)只能用于在編譯時已知大小的類型。然而可以使用如下特殊語法來放寬這個限制:
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
?Sized
上的 trait bound 意味著 “T
可能是也可能不是 Sized
” 同時這個注解會覆蓋泛型類型必須在編譯時擁有固定大小的默認(rèn)規(guī)則。這種意義的 ?Trait
語法只能用于 Sized
,而不能用于任何其他 trait。
另外注意我們將 t
參數(shù)的類型從 T
變?yōu)榱?nbsp;&T
:因?yàn)槠漕愋涂赡懿皇?nbsp;Sized
的,所以需要將其置于某種指針之后。在這個例子中選擇了引用。
接下來,讓我們討論一下函數(shù)和閉包!
更多建議: