Rust 高級類型

2023-03-22 15:16 更新
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 模式

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é)的封裝的輕量級方法。

類型別名用來創(chuàng)建類型同義詞

連同 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> 的任何方法,以及像 ? 這樣的特殊語法。

從不返回的 never type

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 后就會終止。

動態(tài)大小類型和 Sized trait

因?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ù)和閉包!


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號