国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

【Rust】Rust學(xué)習(xí) 第十七章Rust 的面向?qū)ο筇匦?/h1>

這篇具有很好參考價(jià)值的文章主要介紹了【Rust】Rust學(xué)習(xí) 第十七章Rust 的面向?qū)ο筇匦?。希望?duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

面向?qū)ο缶幊蹋∣bject-Oriented Programming,OOP)是一種模式化編程方式。對(duì)象(Object)來源于 20 世紀(jì) 60 年代的 Simula 編程語言。這些對(duì)象影響了 Alan Kay 的編程架構(gòu)中對(duì)象之間的消息傳遞。他在 1967 年創(chuàng)造了?面向?qū)ο缶幊?/strong>?這個(gè)術(shù)語來描述這種架構(gòu)。關(guān)于 OOP 是什么有很多相互矛盾的定義;在一些定義下,Rust 是面向?qū)ο蟮?;在其他定義下,Rust 不是。在本章節(jié)中,我們會(huì)探索一些被普遍認(rèn)為是面向?qū)ο蟮奶匦院瓦@些特性是如何體現(xiàn)在 Rust 語言習(xí)慣中的。接著會(huì)展示如何在 Rust 中實(shí)現(xiàn)面向?qū)ο笤O(shè)計(jì)模式,并討論這么做與利用 Rust 自身的一些優(yōu)勢(shì)實(shí)現(xiàn)的方案相比有什么取舍。

17.1?面向?qū)ο笳Z言的特征

關(guān)于一個(gè)語言被稱為面向?qū)ο笏璧墓δ?,在編程社區(qū)內(nèi)并未達(dá)成一致意見。Rust 被很多不同的編程范式影響,包括面向?qū)ο缶幊?;比如第十三章提到了來自函?shù)式編程的特性。面向?qū)ο缶幊陶Z言所共享的一些特性往往是對(duì)象、封裝和繼承。讓我們看一下這每一個(gè)概念的含義以及 Rust 是否支持他們。

對(duì)象包含數(shù)據(jù)和行為

由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(Addison-Wesley Professional, 1994)編寫的書?Design Patterns: Elements of Reusable Object-Oriented Software?被俗稱為?The Gang of Four,它是面向?qū)ο缶幊棠J降哪夸洝K@樣定義面向?qū)ο缶幊蹋?/p>

面向?qū)ο蟮某绦蚴怯蓪?duì)象組成的。一個(gè)?對(duì)象?包含數(shù)據(jù)和操作這些數(shù)據(jù)的過程。這些過程通常被稱為?方法?或?操作。

在這個(gè)定義下,Rust 是面向?qū)ο蟮模航Y(jié)構(gòu)體和枚舉包含數(shù)據(jù)而?impl?塊提供了在結(jié)構(gòu)體和枚舉之上的方法。雖然帶有方法的結(jié)構(gòu)體和枚舉并不被?稱為?對(duì)象,但是他們提供了與對(duì)象相同的功能。

封裝隱藏了實(shí)現(xiàn)細(xì)節(jié)

另一個(gè)通常與面向?qū)ο缶幊滔嚓P(guān)的方面是?封裝encapsulation)的思想:對(duì)象的實(shí)現(xiàn)細(xì)節(jié)不能被使用對(duì)象的代碼獲取到。所以唯一與對(duì)象交互的方式是通過對(duì)象提供的公有 API;使用對(duì)象的代碼無法深入到對(duì)象內(nèi)部并直接改變數(shù)據(jù)或者行為。封裝使得改變和重構(gòu)對(duì)象的內(nèi)部時(shí)無需改變使用對(duì)象的代碼。

就像我們?cè)诘谄哒掠懻摰哪菢樱嚎梢允褂?pub?關(guān)鍵字來決定模塊、類型、函數(shù)和方法是公有的,而默認(rèn)情況下其他一切都是私有的。比如,我們可以定義一個(gè)包含一個(gè)?i32?類型 vector 的結(jié)構(gòu)體?AveragedCollection?。結(jié)構(gòu)體也可以有一個(gè)字段,該字段保存了 vector 中所有值的平均值。這樣,希望知道結(jié)構(gòu)體中的 vector 的平均值的人可以隨時(shí)獲取它,而無需自己計(jì)算。換句話說,AveragedCollection?會(huì)為我們緩存平均值結(jié)果。

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

注意,結(jié)構(gòu)體自身被標(biāo)記為?pub,這樣其他代碼就可以使用這個(gè)結(jié)構(gòu)體,但是在結(jié)構(gòu)體內(nèi)部的字段仍然是私有的。這是非常重要的,因?yàn)槲覀兿MWC變量被增加到列表或者被從列表刪除時(shí),也會(huì)同時(shí)更新平均值??梢酝ㄟ^在結(jié)構(gòu)體上實(shí)現(xiàn)?add、remove?和?average?方法來做到這一點(diǎn)

// 公有的結(jié)構(gòu)體
pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}
impl AveragedCollection {
    // 公有
    pub fn add(&mut self, value: i32) {
        self.list.push(value);
        self.update_average();
    }
    // 公有
    pub fn remove(&mut self) -> Option<i32> {
        let result = self.list.pop();
        match result {
            Some(value) => {
                self.update_average();
                Some(value)
            },
            None => None,
        }
    }
    // 公有
    pub fn average(&self) -> f64 {
        self.average
    }

    fn update_average(&mut self) {
        let total: i32 = self.list.iter().sum();
        self.average = total as f64 / self.list.len() as f64;
    }
}

公有方法?add、remove?和?average?是修改?AveragedCollection?實(shí)例的唯一方式。當(dāng)使用?add?方法把一個(gè)元素加入到?list?或者使用?remove?方法來刪除時(shí),這些方法的實(shí)現(xiàn)同時(shí)會(huì)調(diào)用私有的?update_average?方法來更新?average?字段。

list?和?average?是私有的,所以沒有其他方式來使得外部的代碼直接向?list?增加或者刪除元素,否則?list?改變時(shí)可能會(huì)導(dǎo)致?average?字段不同步。average?方法返回?average?字段的值,這使得外部的代碼只能讀取?average?而不能修改它。

因?yàn)槲覀円呀?jīng)封裝好了?AveragedCollection?的實(shí)現(xiàn)細(xì)節(jié),將來可以輕松改變類似數(shù)據(jù)結(jié)構(gòu)這些方面的內(nèi)容。例如,可以使用?HashSet<i32>?代替?Vec<i32>?作為?list?字段的類型。只要?addremove?和?average?公有函數(shù)的簽名保持不變,使用?AveragedCollection?的代碼就無需改變。相反如果使得?list?為公有,就未必都會(huì)如此了:?HashSet<i32>?和?Vec<i32>?使用不同的方法增加或移除項(xiàng),所以如果要想直接修改?list?的話,外部的代碼可能不得不做出修改。

如果封裝是一個(gè)語言被認(rèn)為是面向?qū)ο笳Z言所必要的方面的話,那么 Rust 滿足這個(gè)要求。在代碼中不同的部分使用?pub?與否可以封裝其實(shí)現(xiàn)細(xì)節(jié)。

繼承,作為類型系統(tǒng)與代碼共享

繼承Inheritance)是一個(gè)很多編程語言都提供的機(jī)制,一個(gè)對(duì)象可以定義為繼承另一個(gè)對(duì)象的定義,這使其可以獲得父對(duì)象的數(shù)據(jù)和行為,而無需重新定義。

如果一個(gè)語言必須有繼承才能被稱為面向?qū)ο笳Z言的話,那么 Rust 就不是面向?qū)ο蟮摹?/strong>無法定義一個(gè)結(jié)構(gòu)體繼承父結(jié)構(gòu)體的成員和方法。然而,如果你過去常常在你的編程工具箱使用繼承,根據(jù)你最初考慮繼承的原因,Rust 也提供了其他的解決方案。

選擇繼承有兩個(gè)主要的原因。第一個(gè)是為了重用代碼:一旦為一個(gè)類型實(shí)現(xiàn)了特定行為,繼承可以對(duì)一個(gè)不同的類型重用這個(gè)實(shí)現(xiàn)。相反 Rust 代碼可以使用默認(rèn) trait 方法實(shí)現(xiàn)來進(jìn)行共享,在前面示例 中我們見過在?Summary?trait 上增加的?summarize?方法的默認(rèn)實(shí)現(xiàn)。任何實(shí)現(xiàn)了?Summary?trait 的類型都可以使用?summarize?方法而無須進(jìn)一步實(shí)現(xiàn)。這類似于父類有一個(gè)方法的實(shí)現(xiàn),而通過繼承子類也擁有這個(gè)方法的實(shí)現(xiàn)。當(dāng)實(shí)現(xiàn)?Summary?trait 時(shí)也可以選擇覆蓋?summarize?的默認(rèn)實(shí)現(xiàn),這類似于子類覆蓋從父類繼承的方法實(shí)現(xiàn)。

第二個(gè)使用繼承的原因與類型系統(tǒng)有關(guān):表現(xiàn)為子類型可以用于父類型被使用的地方。這也被稱為?多態(tài)polymorphism),這意味著如果多種對(duì)象共享特定的屬性,則可以相互替代使用。

多態(tài)(Polymorphism)

很多人將多態(tài)描述為繼承的同義詞。不過它是一個(gè)有關(guān)可以用于多種類型的代碼的更廣泛的概念。對(duì)于繼承來說,這些類型通常是子類。 Rust 則通過泛型來對(duì)不同的可能類型進(jìn)行抽象,并通過 trait bounds 對(duì)這些類型所必須提供的內(nèi)容施加約束。這有時(shí)被稱為?bounded parametric polymorphism。

近來繼承作為一種語言設(shè)計(jì)的解決方案在很多語言中失寵了,因?yàn)槠鋾r(shí)常帶有共享多于所需的代碼的風(fēng)險(xiǎn)。子類不應(yīng)總是共享其父類的所有特征,但是繼承卻始終如此。如此會(huì)使程序設(shè)計(jì)更為不靈活,并引入無意義的子類方法調(diào)用,或由于方法實(shí)際并不適用于子類而造成錯(cuò)誤的可能性。某些語言還只允許子類繼承一個(gè)父類,進(jìn)一步限制了程序設(shè)計(jì)的靈活性。

因?yàn)檫@些原因,Rust 選擇了一個(gè)不同的途徑,使用 trait 對(duì)象而不是繼承。讓我們看一下 Rust 中的 trait 對(duì)象是如何實(shí)現(xiàn)多態(tài)的。

17.2?為使用不同類型的值而設(shè)計(jì)的trait對(duì)象

在第八章中,談到了 vector 只能存儲(chǔ)同種類型元素的局限。其示例中提供了一個(gè)定義?SpreadsheetCell?枚舉來儲(chǔ)存整型,浮點(diǎn)型和文本成員的替代方案。這意味著可以在每個(gè)單元中儲(chǔ)存不同類型的數(shù)據(jù),并仍能擁有一個(gè)代表一排單元的 vector。這在當(dāng)編譯代碼時(shí)就知道希望可以交替使用的類型為固定集合的情況下是完全可行的。

然而有時(shí)我們希望庫用戶在特定情況下能夠擴(kuò)展有效的類型集合。為了展示如何實(shí)現(xiàn)這一點(diǎn),這里將創(chuàng)建一個(gè)圖形用戶接口(Graphical User Interface, GUI)工具的例子,它通過遍歷列表并調(diào)用每一個(gè)項(xiàng)目的?draw?方法來將其繪制到屏幕上 —— 此乃一個(gè) GUI 工具的常見技術(shù)。我們將要?jiǎng)?chuàng)建一個(gè)叫做?gui?的庫 crate,它含一個(gè) GUI 庫的結(jié)構(gòu)。這個(gè) GUI 庫包含一些可供開發(fā)者使用的類型,比如?Button?或?TextField。在此之上,gui?的用戶希望創(chuàng)建自定義的可以繪制于屏幕上的類型:比如,一個(gè)程序員可能會(huì)增加?Image,另一個(gè)可能會(huì)增加?SelectBox。

這個(gè)例子中并不會(huì)實(shí)現(xiàn)一個(gè)功能完善的 GUI 庫,不過會(huì)展示其中各個(gè)部分是如何結(jié)合在一起的。編寫庫的時(shí)候,我們不可能知曉并定義所有其他程序員希望創(chuàng)建的類型。我們所知曉的是?gui?需要記錄一系列不同類型的值,并需要能夠?qū)ζ渲忻恳粋€(gè)值調(diào)用?draw?方法。這里無需知道調(diào)用?draw?方法時(shí)具體會(huì)發(fā)生什么,只要該值會(huì)有那個(gè)方法可供我們調(diào)用。

在擁有繼承的語言中,可以定義一個(gè)名為?Component?的類,該類上有一個(gè)?draw?方法。其他的類比如?Button、Image?和?SelectBox?會(huì)從?Component?派生并因此繼承?draw?方法。它們各自都可以覆蓋?draw?方法來定義自己的行為,但是框架會(huì)把所有這些類型當(dāng)作是?Component?的實(shí)例,并在其上調(diào)用?draw。不過 Rust 并沒有繼承,我們得另尋出路。

定義通用行為的trait

為了實(shí)現(xiàn)?gui?所期望的行為,讓我們定義一個(gè)?Draw?trait,其中包含名為?draw?的方法。接著可以定義一個(gè)存放?trait 對(duì)象trait object) 的 vector。trait 對(duì)象指向一個(gè)實(shí)現(xiàn)了我們指定 trait 的類型的實(shí)例,以及一個(gè)用于在運(yùn)行時(shí)查找該類型的trait方法的表。我們通過指定某種指針來創(chuàng)建 trait 對(duì)象,例如?&?引用或?Box<T>?智能指針,還有?dyn?keyword, 以及指定相關(guān)的 trait)。我們可以使用 trait 對(duì)象代替泛型或具體類型。任何使用 trait 對(duì)象的位置,Rust 的類型系統(tǒng)會(huì)在編譯時(shí)確保任何在此上下文中使用的值會(huì)實(shí)現(xiàn)其 trait 對(duì)象的 trait。如此便無需在編譯時(shí)就知曉所有可能的類型。

之前提到過,Rust 刻意不將結(jié)構(gòu)體與枚舉稱為 “對(duì)象”,以便與其他語言中的對(duì)象相區(qū)別。在結(jié)構(gòu)體或枚舉中,結(jié)構(gòu)體字段中的數(shù)據(jù)和?impl?塊中的行為是分開的,不同于其他語言中將數(shù)據(jù)和行為組合進(jìn)一個(gè)稱為對(duì)象的概念中。trait 對(duì)象將數(shù)據(jù)和行為兩者相結(jié)合,從這種意義上說??其更類似其他語言中的對(duì)象。不過 trait 對(duì)象不同于傳統(tǒng)的對(duì)象,因?yàn)椴荒芟?trait 對(duì)象增加數(shù)據(jù)。trait 對(duì)象并不像其他語言中的對(duì)象那么通用:其(trait 對(duì)象)具體的作用是允許對(duì)通用行為進(jìn)行抽象。src/lib.rs

pub trait Draw {
    fn draw(&self);
}

因?yàn)榈谑乱呀?jīng)討論過如何定義 trait,其語法看起來應(yīng)該比較眼熟。接下來就是新內(nèi)容了:下面實(shí)例定義了一個(gè)存放了名叫?components?的 vector 的結(jié)構(gòu)體?Screen。這個(gè) vector 的類型是?Box<dyn Draw>,此為一個(gè) trait 對(duì)象:它是?Box?中任何實(shí)現(xiàn)了?Draw?trait 的類型的替身。src/lib.rs

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

在?Screen?結(jié)構(gòu)體上,我們將定義一個(gè)?run?方法,該方法會(huì)對(duì)其?components?上的每一個(gè)組件調(diào)用?draw?方法。src/lib.rs

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

這與定義使用了帶有 trait bound 的泛型類型參數(shù)的結(jié)構(gòu)體不同。泛型類型參數(shù)一次只能替代一個(gè)具體類型,而 trait 對(duì)象則允許在運(yùn)行時(shí)替代多種具體類型。例如,可以定義?Screen?結(jié)構(gòu)體來使用泛型和 trait bound。src/lib.rs

pub struct Screen<T: Draw> {
    pub components: Vec<T>,
}

impl<T> Screen<T>
    where T: Draw {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

這限制了?Screen?實(shí)例必須擁有一個(gè)全是?Button?類型或者全是?TextField?類型的組件列表。如果只需要同質(zhì)(相同類型)集合,則傾向于使用泛型和 trait bound,因?yàn)槠涠x會(huì)在編譯時(shí)采用具體類型進(jìn)行單態(tài)化。

另一方面,通過使用 trait 對(duì)象的方法,一個(gè)?Screen?實(shí)例可以存放一個(gè)既能包含?Box<Button>,也能包含?Box<TextField>?的?Vec<T>。讓我們看看它是如何工作的,接著會(huì)講到其運(yùn)行時(shí)性能影響。

實(shí)現(xiàn)trait

現(xiàn)在來增加一些實(shí)現(xiàn)了?Draw?trait 的類型。我們將提供?Button?類型。再一次重申,真正實(shí)現(xiàn) GUI 庫超出了本書的范疇,所以?draw?方法體中不會(huì)有任何有意義的實(shí)現(xiàn)。為了想象一下這個(gè)實(shí)現(xiàn)看起來像什么,一個(gè)?Button?結(jié)構(gòu)體可能會(huì)擁有?width、height?和?label?字段。src/lib.rs

pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}

impl Draw for Button {
    fn draw(&self) {
        // 實(shí)際繪制按鈕的代碼
    }
}

在?Button?上的?width、height?和?label?字段會(huì)和其他組件不同,比如?TextField?可能有?width、height、label?以及?placeholder?字段。每一個(gè)我們希望能在屏幕上繪制的類型都會(huì)使用不同的代碼來實(shí)現(xiàn)?Draw?trait 的?draw?方法來定義如何繪制特定的類型,像這里的?Button?類型(并不包含任何實(shí)際的 GUI 代碼,這超出了本章的范疇)。除了實(shí)現(xiàn)?Draw?trait 之外,比如?Button?還可能有另一個(gè)包含按鈕點(diǎn)擊如何響應(yīng)的方法的?impl?塊。這類方法并不適用于像?TextField?這樣的類型。

如果一些庫的使用者決定實(shí)現(xiàn)一個(gè)包含?width、height?和?options?字段的結(jié)構(gòu)體?SelectBox,并且也為其實(shí)現(xiàn)了?Draw?trait。?src/main.rs

use gui::Draw;

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}

庫使用者現(xiàn)在可以在他們的?main?函數(shù)中創(chuàng)建一個(gè)?Screen?實(shí)例。至此可以通過將?SelectBox?和?Button?放入?Box<T>?轉(zhuǎn)變?yōu)?trait 對(duì)象來增加組件。接著可以用?Screen?的?run?方法,它會(huì)調(diào)用每個(gè)組件的?draw?方法。?src/main.rs

use gui::{Screen, Button};

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No")
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                label: String::from("OK"),
            }),
        ],
    };

    screen.run();
}

當(dāng)編寫庫的時(shí)候,我們不知道何人會(huì)在何時(shí)增加?SelectBox?類型,不過?Screen?的實(shí)現(xiàn)能夠操作并繪制這個(gè)新類型,因?yàn)?SelectBox?實(shí)現(xiàn)了?Draw?trait,這意味著它實(shí)現(xiàn)了?draw?方法。

這個(gè)概念 —— 只關(guān)心值所反映的信息而不是其具體類型 —— 類似于動(dòng)態(tài)類型語言中稱為?鴨子類型duck typing)的概念:如果它走起來像一只鴨子,叫起來像一只鴨子,那么它就是一只鴨子!在示例中?Screen?上的?run?實(shí)現(xiàn)中,run?并不需要知道各個(gè)組件的具體類型是什么。它并不檢查組件是?Button?或者?SelectBox?的實(shí)例。通過指定?Box<dyn Draw>?作為?components?vector 中值的類型,我們就定義了?Screen?為需要可以在其上調(diào)用?draw?方法的值。

使用 trait 對(duì)象和 Rust 類型系統(tǒng)來進(jìn)行類似鴨子類型操作的優(yōu)勢(shì)是無需在運(yùn)行時(shí)檢查一個(gè)值是否實(shí)現(xiàn)了特定方法或者擔(dān)心在調(diào)用時(shí)因?yàn)橹禌]有實(shí)現(xiàn)方法而產(chǎn)生錯(cuò)誤。如果值沒有實(shí)現(xiàn) trait 對(duì)象所需的 trait 則 Rust 不會(huì)編譯這些代碼。

例如,下面示例展示了當(dāng)創(chuàng)建一個(gè)使用?String?做為其組件的?Screen?時(shí)發(fā)生的情況:?src/main.rs

use gui::Screen;

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(String::from("Hi")),
        ],
    };

    screen.run();
}

我們會(huì)遇到這個(gè)錯(cuò)誤,因?yàn)?String?沒有實(shí)現(xiàn)?rust_gui::Draw?trait:

【Rust】Rust學(xué)習(xí) 第十七章Rust 的面向?qū)ο筇匦?Rust,rust,學(xué)習(xí),開發(fā)語言,keep studying

這告訴了我們,要么是我們傳遞了并不希望傳遞給?Screen?的類型并應(yīng)該提供其他類型,要么應(yīng)該在?String?上實(shí)現(xiàn)?Draw?以便?Screen?可以調(diào)用其上的?draw。

trait對(duì)象執(zhí)行動(dòng)態(tài)分發(fā)

回憶一下第十章部分討論過的,當(dāng)對(duì)泛型使用 trait bound 時(shí)編譯器所進(jìn)行單態(tài)化處理:編譯器為每一個(gè)被泛型類型參數(shù)代替的具體類型生成了非泛型的函數(shù)和方法實(shí)現(xiàn)。單態(tài)化所產(chǎn)生的代碼進(jìn)行?靜態(tài)分發(fā)static dispatch)。靜態(tài)分發(fā)發(fā)生于編譯器在編譯時(shí)就知曉調(diào)用了什么方法的時(shí)候。這與?動(dòng)態(tài)分發(fā)?(dynamic dispatch)相對(duì),這時(shí)編譯器在編譯時(shí)無法知曉調(diào)用了什么方法。在動(dòng)態(tài)分發(fā)的情況下,編譯器會(huì)生成在運(yùn)行時(shí)確定調(diào)用了什么方法的代碼。

當(dāng)使用 trait 對(duì)象時(shí),Rust 必須使用動(dòng)態(tài)分發(fā)。編譯器無法知曉所有可能用于 trait 對(duì)象代碼的類型,所以它也不知道應(yīng)該調(diào)用哪個(gè)類型的哪個(gè)方法實(shí)現(xiàn)。為此,Rust 在運(yùn)行時(shí)使用 trait 對(duì)象中的指針來知曉需要調(diào)用哪個(gè)方法。動(dòng)態(tài)分發(fā)也阻止編譯器有選擇的內(nèi)聯(lián)方法代碼,這會(huì)相應(yīng)的禁用一些優(yōu)化。

Trait 對(duì)象要求對(duì)象安全

只有?對(duì)象安全object safe)的 trait 才可以組成 trait 對(duì)象。圍繞所有使得 trait 對(duì)象安全的屬性存在一些復(fù)雜的規(guī)則,不過在實(shí)踐中,只涉及到兩條規(guī)則。如果一個(gè) trait 中所有的方法有如下屬性時(shí),則該 trait 是對(duì)象安全的:

  • 返回值類型不為?Self
  • 方法沒有任何泛型類型參數(shù)

Self?關(guān)鍵字是我們要實(shí)現(xiàn) trait 或方法的類型的別名。對(duì)象安全對(duì)于 trait 對(duì)象是必須的,因?yàn)橐坏┯辛?trait 對(duì)象,就不再知曉實(shí)現(xiàn)該 trait 的具體類型是什么了。如果 trait 方法返回具體的?Self?類型,但是 trait 對(duì)象忘記了其真正的類型,那么方法不可能使用已經(jīng)忘卻的原始具體類型。同理對(duì)于泛型類型參數(shù)來說,當(dāng)使用 trait 時(shí)其會(huì)放入具體的類型參數(shù):此具體類型變成了實(shí)現(xiàn)該 trait 的類型的一部分。當(dāng)使用 trait 對(duì)象時(shí)其具體類型被抹去了,故無從得知放入泛型參數(shù)類型的類型是什么。

一個(gè) trait 的方法不是對(duì)象安全的例子是標(biāo)準(zhǔn)庫中的?Clone?trait。Clone?trait 的?clone?方法的參數(shù)簽名看起來像這樣:

pub trait Clone {
    fn clone(&self) -> Self;
}

String?實(shí)現(xiàn)了?Clone?trait,當(dāng)在?String?實(shí)例上調(diào)用?clone?方法時(shí)會(huì)得到一個(gè)?String?實(shí)例。類似的,當(dāng)調(diào)用?Vec<T>?實(shí)例的?clone?方法會(huì)得到一個(gè)?Vec<T>?實(shí)例。clone?的簽名需要知道什么類型會(huì)代替?Self,因?yàn)檫@是它的返回值。

如果嘗試做一些違反有關(guān) trait 對(duì)象的對(duì)象安全規(guī)則的事情,編譯器會(huì)提示你。

17.3?面向?qū)ο笤O(shè)計(jì)模式的實(shí)現(xiàn)

狀態(tài)模式(state pattern)是一個(gè)面向?qū)ο笤O(shè)計(jì)模式。該模式的關(guān)鍵在于一個(gè)值有某些內(nèi)部狀態(tài),體現(xiàn)為一系列的?狀態(tài)對(duì)象,同時(shí)值的行為隨著其內(nèi)部狀態(tài)而改變。狀態(tài)對(duì)象共享功能:當(dāng)然,在 Rust 中使用結(jié)構(gòu)體和 trait 而不是對(duì)象和繼承。每一個(gè)狀態(tài)對(duì)象代表負(fù)責(zé)其自身的行為和當(dāng)需要改變?yōu)榱硪粋€(gè)狀態(tài)時(shí)的規(guī)則的狀態(tài)。持有任何一個(gè)這種狀態(tài)對(duì)象的值對(duì)于不同狀態(tài)的行為以及何時(shí)狀態(tài)轉(zhuǎn)移毫不知情。

使用狀態(tài)模式意味著當(dāng)程序的業(yè)務(wù)需求改變時(shí),無需改變值持有狀態(tài)或者使用值的代碼。我們只需更新某個(gè)狀態(tài)對(duì)象中的代碼來改變其規(guī)則,或者是增加更多的狀態(tài)對(duì)象。讓我們看看一個(gè)有關(guān)狀態(tài)模式和如何在 Rust 中使用它的例子。

為了探索這個(gè)概念,我們將實(shí)現(xiàn)一個(gè)增量式的發(fā)布博文的工作流。這個(gè)博客的最終功能看起來像這樣:

  1. 博文從空白的草案開始。
  2. 一旦草案完成,請(qǐng)求審核博文。
  3. 一旦博文過審,它將被發(fā)表。
  4. 只有被發(fā)表的博文的內(nèi)容會(huì)被打印,這樣就不會(huì)意外打印出沒有被審核的博文的文本。

任何其他對(duì)博文的修改嘗試都是沒有作用的。例如,如果嘗試在請(qǐng)求審核之前通過一個(gè)草案博文,博文應(yīng)該保持未發(fā)布的狀態(tài)。

下面示例展示這個(gè)工作流的代碼形式:這是一個(gè)我們將要在一個(gè)叫做?blog?的庫 crate 中實(shí)現(xiàn)的 API 的示例。這段代碼還不能編譯,因?yàn)檫€未實(shí)現(xiàn)?blog。?src/main.rs

use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

我們希望允許用戶使用?Post::new?創(chuàng)建一個(gè)新的博文草案。接著希望能在草案階段為博文編寫一些文本。如果嘗試在審核之前立即打印出博文的內(nèi)容,什么也不會(huì)發(fā)生因?yàn)椴┪娜匀皇遣莅浮_@里增加的?assert_eq!?出于演示目的。一個(gè)好的單元測(cè)試將是斷言草案博文的?content?方法返回空字符串,不過我們并不準(zhǔn)備為這個(gè)例子編寫單元測(cè)試。

接下來,我們希望能夠請(qǐng)求審核博文,而在等待審核的階段?content?應(yīng)該仍然返回空字符串。最后當(dāng)博文審核通過,它應(yīng)該被發(fā)表,這意味著當(dāng)調(diào)用?content?時(shí)博文的文本將被返回。

注意我們與 crate 交互的唯一的類型是?Post。這個(gè)類型會(huì)使用狀態(tài)模式并會(huì)存放處于三種博文所可能的狀態(tài)之一的值 —— 草案,等待審核和發(fā)布。狀態(tài)上的改變由?Post?類型內(nèi)部進(jìn)行管理。狀態(tài)依庫用戶對(duì)?Post?實(shí)例調(diào)用的方法而改變,但是不能直接管理狀態(tài)變化。這也意味著用戶不會(huì)在狀態(tài)上犯錯(cuò),比如在過審前發(fā)布博文。

定義Post并新建一個(gè)草案狀態(tài)的實(shí)例

讓我們開始實(shí)現(xiàn)這個(gè)庫吧!我們知道需要一個(gè)公有?Post?結(jié)構(gòu)體來存放一些文本,所以讓我們從結(jié)構(gòu)體的定義和一個(gè)創(chuàng)建?Post?實(shí)例的公有關(guān)聯(lián)函數(shù)?new?開始,如下面示例所示。還需定義一個(gè)私有 trait?State。Post?將在私有字段?state?中存放一個(gè)?Option<T>?類型的 trait 對(duì)象?Box<dyn State>。稍后將會(huì)看到為何?Option<T>?是必須的。src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

State?trait 定義了所有不同狀態(tài)的博文所共享的行為,同時(shí)?DraftPendingReview?和?Published?狀態(tài)都會(huì)實(shí)現(xiàn)?State?狀態(tài)?,F(xiàn)在這個(gè) trait 并沒有任何方法,同時(shí)開始將只定義?Draft?狀態(tài)因?yàn)檫@是我們希望博文的初始狀態(tài)。

當(dāng)創(chuàng)建新的?Post?時(shí),我們將其?state?字段設(shè)置為一個(gè)存放了?Box?的?Some?值。這個(gè)?Box?指向一個(gè)?Draft?結(jié)構(gòu)體新實(shí)例。這確保了無論何時(shí)新建一個(gè)?Post?實(shí)例,它都會(huì)從草案開始。因?yàn)?Post?的?state?字段是私有的,也就無法創(chuàng)建任何其他狀態(tài)的?Post?了!。Post::new?函數(shù)中將?content?設(shè)置為新建的空?String。

存放博文內(nèi)容的文本

在上面示例中,展示了我們希望能夠調(diào)用一個(gè)叫做?add_text?的方法并向其傳遞一個(gè)?&str?來將文本增加到博文的內(nèi)容中。選擇實(shí)現(xiàn)為一個(gè)方法而不是將?content?字段暴露為?pub?。這意味著之后可以實(shí)現(xiàn)一個(gè)方法來控制?content?字段如何被讀取。add_text?方法是非常直觀的。

impl Post {
    // --snip--
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

add_text?獲取一個(gè)?self?的可變引用,因?yàn)樾枰淖冋{(diào)用?add_text?的?Post?實(shí)例。接著調(diào)用?content?中的?String?的?push_str?并傳遞?text?參數(shù)來保存到?content?中。這不是狀態(tài)模式的一部分,因?yàn)樗男袨椴⒉灰蕾嚥┪乃幍臓顟B(tài)。add_text?方法完全不與?state?狀態(tài)交互,不過這是我們希望支持的行為的一部分。

確保博文草案的內(nèi)容是空的

即使調(diào)用?add_text?并向博文增加一些內(nèi)容之后,我們?nèi)匀幌M?content?方法返回一個(gè)空字符串 slice,因?yàn)椴┪娜匀惶幱诓莅笭顟B(tài)?,F(xiàn)在讓我們使用能滿足要求的最簡(jiǎn)單的方式來實(shí)現(xiàn)?content?方法:總是返回一個(gè)空字符串 slice。當(dāng)實(shí)現(xiàn)了將博文狀態(tài)改為發(fā)布的能力之后將改變這一做法。但是目前博文只能是草案狀態(tài),這意味著其內(nèi)容應(yīng)該總是空的。?src/lib.rs

impl Post {
    // --snip--
    pub fn content(&self) -> &str {
        ""
    }
}

請(qǐng)求審核博文來改變其狀態(tài)

接下來需要增加請(qǐng)求審核博文的功能,這應(yīng)當(dāng)將其狀態(tài)由?Draft?改為?PendingReview。src/lib.rs

impl Post {
    // --snip--
    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

這里為?Post?增加一個(gè)獲取?self?可變引用的公有方法?request_review。接著在?Post?的當(dāng)前狀態(tài)下調(diào)用內(nèi)部的?request_review?方法,并且第二個(gè)?request_review?方法會(huì)消費(fèi)當(dāng)前的狀態(tài)并返回一個(gè)新狀態(tài)。

這里給?State?trait 增加了?request_review?方法;所有實(shí)現(xiàn)了這個(gè) trait 的類型現(xiàn)在都需要實(shí)現(xiàn)?request_review?方法。注意不同于使用?self、?&self?或者?&mut self?作為方法的第一個(gè)參數(shù),這里使用了?self: Box<Self>。這個(gè)語法意味著這個(gè)方法調(diào)用只對(duì)這個(gè)類型的?Box?有效。這個(gè)語法獲取了?Box<Self>?的所有權(quán),使老狀態(tài)無效化以便?Post?的狀態(tài)值可以將自身轉(zhuǎn)換為新狀態(tài)。

為了消費(fèi)老狀態(tài),request_review?方法需要獲取狀態(tài)值的所有權(quán)。這也就是?Post?的?state?字段中?Option?的來歷:調(diào)用?take?方法將?state?字段中的?Some?值取出并留下一個(gè)?None,因?yàn)?Rust 不允許在結(jié)構(gòu)體中存在空的字段。這使得我們將?state?值移動(dòng)出?Post?而不是借用它。接著將博文的?state?值設(shè)置為這個(gè)操作的結(jié)果。

這里需要將?state?臨時(shí)設(shè)置為?None,不同于像?self.state = self.state.request_review();?這樣的代碼直接設(shè)置?state?字段,來獲取?state?值的所有權(quán)。這確保了當(dāng)?Post?被轉(zhuǎn)換為新狀態(tài)后其不再能使用老的?state?值。

Draft?的方法?request_review?的實(shí)現(xiàn)返回一個(gè)新的,裝箱的?PendingReview?結(jié)構(gòu)體的實(shí)例,其用來代表博文處于等待審核狀態(tài)。結(jié)構(gòu)體?PendingReview?同樣也實(shí)現(xiàn)了?request_review?方法,不過它不進(jìn)行任何狀態(tài)轉(zhuǎn)換。相反它返回自身,因?yàn)檎?qǐng)求審核已經(jīng)處于?PendingReview?狀態(tài)的博文應(yīng)該保持?PendingReview?狀態(tài)。

現(xiàn)在開始能夠看出狀態(tài)模式的優(yōu)勢(shì)了:Post?的?request_review?方法無論?state?是何值都是一樣的。每個(gè)狀態(tài)只負(fù)責(zé)它自己的規(guī)則。

我們將繼續(xù)保持?Post?的?content?方法不變,返回一個(gè)空字符串 slice?,F(xiàn)在可以擁有?PendingReview?狀態(tài)而不僅僅是?Draft?狀態(tài)的?Post?了,不過我們希望在?PendingReview?狀態(tài)下其也有相同的行為。

增加改變content 行為的approve方法

approve?方法將與?request_review?方法類似:它會(huì)將?state?設(shè)置為審核通過時(shí)應(yīng)處于的狀態(tài)。src/lib.rs

impl Post {
    // --snip--
    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    // --snip--
    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    // --snip--
    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

這里為?State?trait 增加了?approve?方法,并新增了一個(gè)實(shí)現(xiàn)了?State?的結(jié)構(gòu)體,Published?狀態(tài)。

類似于?request_review,如果對(duì)?Draft?調(diào)用?approve?方法,并沒有任何效果,因?yàn)樗鼤?huì)返回?self。當(dāng)對(duì)?PendingReview?調(diào)用?approve?時(shí),它返回一個(gè)新的、裝箱的?Published?結(jié)構(gòu)體的實(shí)例。Published?結(jié)構(gòu)體實(shí)現(xiàn)了?State?trait,同時(shí)對(duì)于?request_review?和?approve?兩方法來說,它返回自身,因?yàn)樵谶@兩種情況博文應(yīng)該保持?Published?狀態(tài)。

現(xiàn)在更新?Post?的?content?方法:如果狀態(tài)為?Published?希望返回博文?content?字段的值;否則希望返回空字符串 slice。src/lib.rs

impl Post {
    // --snip--
    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }
    // --snip--
}

因?yàn)槟繕?biāo)是將所有像這樣的規(guī)則保持在實(shí)現(xiàn)了?State?的結(jié)構(gòu)體中,我們將調(diào)用?state?中的值的?content?方法并傳遞博文實(shí)例(也就是?self)作為參數(shù)。接著返回?state?值的?content?方法的返回值。

這里調(diào)用?Option?的?as_ref?方法是因?yàn)樾枰?Option?中值的引用而不是獲取其所有權(quán)。因?yàn)?state?是一個(gè)?Option<Box<State>>,調(diào)用?as_ref?會(huì)返回一個(gè)?Option<&Box<State>>。如果不調(diào)用?as_ref,將會(huì)得到一個(gè)錯(cuò)誤,因?yàn)椴荒軐?state?移動(dòng)出借用的?&self?函數(shù)參數(shù)。

接著調(diào)用?unwrap?方法,這里我們知道它永遠(yuǎn)也不會(huì) panic,因?yàn)?Post?的所有方法都確保在他們返回時(shí)?state?會(huì)有一個(gè)?Some?值。這就是一個(gè)第十二章?“當(dāng)我們比編譯器知道更多的情況”?部分討論過的我們知道?None?是不可能的而編譯器卻不能理解的情況。

接著我們就有了一個(gè)?&Box<State>,當(dāng)調(diào)用其?content?時(shí),解引用強(qiáng)制多態(tài)會(huì)作用于?&?和?Box?,這樣最終會(huì)調(diào)用實(shí)現(xiàn)了?State?trait 的類型的?content?方法。這意味著需要為?State?trait 定義增加?content,這也是放置根據(jù)所處狀態(tài)返回什么內(nèi)容的邏輯的地方。src/lib.rs

trait State {
    // --snip--
    fn content<'a>(&self, post: &'a Post) -> &'a str {
        ""
    }
}

// --snip--
struct Published {}

impl State for Published {
    // --snip--
    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}

這里增加了一個(gè)?content?方法的默認(rèn)實(shí)現(xiàn)來返回一個(gè)空字符串 slice。這意味著無需為?Draft?和?PendingReview?結(jié)構(gòu)體實(shí)現(xiàn)?content?了。Published?結(jié)構(gòu)體會(huì)覆蓋?content?方法并會(huì)返回?post.content?的值。

注意這個(gè)方法需要生命周期注解,如第十章所討論的。這里獲取?post?的引用作為參數(shù),并返回?post?一部分的引用,所以返回的引用的生命周期與?post?參數(shù)相關(guān)。

狀態(tài)模式的權(quán)衡取舍

我們展示了 Rust 是能夠?qū)崿F(xiàn)面向?qū)ο蟮臓顟B(tài)模式的,以便能根據(jù)博文所處的狀態(tài)來封裝不同類型的行為。Post?的方法并不知道這些不同類型的行為。通過這種組織代碼的方式,要找到所有已發(fā)布博文的不同行為只需查看一處代碼:Published?的?State?trait 的實(shí)現(xiàn)。

如果要?jiǎng)?chuàng)建一個(gè)不使用狀態(tài)模式的替代實(shí)現(xiàn),則可能會(huì)在?Post?的方法中,或者甚至于在?main?代碼中用到?match?語句,來檢查博文狀態(tài)并在這里改變其行為。這意味著需要查看很多位置來理解處于發(fā)布狀態(tài)的博文的所有邏輯!這在增加更多狀態(tài)時(shí)會(huì)變得更糟:每一個(gè)?match?語句都會(huì)需要另一個(gè)分支。

對(duì)于狀態(tài)模式來說,Post?的方法和使用?Post?的位置無需?match?語句,同時(shí)增加新狀態(tài)只涉及到增加一個(gè)新?struct?和為其實(shí)現(xiàn) trait 的方法。

這個(gè)實(shí)現(xiàn)易于擴(kuò)展增加更多功能。為了體會(huì)使用此模式維護(hù)代碼的簡(jiǎn)潔性,請(qǐng)嘗試如下一些建議:

  • 增加?reject?方法將博文的狀態(tài)從?PendingReview?變回?Draft
  • 在將狀態(tài)變?yōu)?Published?之前需要兩次?approve?調(diào)用
  • 只允許博文處于?Draft?狀態(tài)時(shí)增加文本內(nèi)容。提示:讓狀態(tài)對(duì)象負(fù)責(zé)什么可能會(huì)修改內(nèi)容而不負(fù)責(zé)修改?Post。

狀態(tài)模式的一個(gè)缺點(diǎn)是因?yàn)闋顟B(tài)實(shí)現(xiàn)了狀態(tài)之間的轉(zhuǎn)換,一些狀態(tài)會(huì)相互聯(lián)系。如果在?PendingReview?和?Published?之間增加另一個(gè)狀態(tài),比如?Scheduled,則不得不修改?PendingReview?中的代碼來轉(zhuǎn)移到?Scheduled。如果?PendingReview?無需因?yàn)樾略龅臓顟B(tài)而改變就更好了,不過這意味著切換到另一種設(shè)計(jì)模式。

另一個(gè)缺點(diǎn)是我們會(huì)發(fā)現(xiàn)一些重復(fù)的邏輯。為了消除他們,可以嘗試為?State?trait 中返回?self?的?request_review?和?approve?方法增加默認(rèn)實(shí)現(xiàn),不過這會(huì)違反對(duì)象安全性,因?yàn)?trait 不知道?self?具體是什么。我們希望能夠?qū)?State?作為一個(gè) trait 對(duì)象,所以需要其方法是對(duì)象安全的。

另一個(gè)重復(fù)是?Post?中?request_review?和?approve?這兩個(gè)類似的實(shí)現(xiàn)。他們都委托調(diào)用了?state?字段中?Option?值的同一方法,并在結(jié)果中為?state?字段設(shè)置了新值。如果?Post?中的很多方法都遵循這個(gè)模式,我們可能會(huì)考慮定義一個(gè)宏來消除重復(fù)(查看第十九章的?“宏”?部分)。

完全按照面向?qū)ο笳Z言的定義實(shí)現(xiàn)這個(gè)模式并沒有盡可能地利用 Rust 的優(yōu)勢(shì)。讓我們看看一些代碼中可以做出的修改,來將無效的狀態(tài)和狀態(tài)轉(zhuǎn)移變?yōu)榫幾g時(shí)錯(cuò)誤。

代碼:?src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }


}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
    fn content<'a>(&self, post: &'a Post) -> &'a str {
        ""
    }
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
     fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }
    
    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}

src/main.rs

// use blog::Post;
use test7::Post;            // 文件名稱是test7

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

將狀態(tài)和行為編碼為類型

我們將展示如何稍微反思狀態(tài)模式來進(jìn)行一系列不同的權(quán)衡取舍。不同于完全封裝狀態(tài)和狀態(tài)轉(zhuǎn)移使得外部代碼對(duì)其毫不知情,我們將狀態(tài)編碼進(jìn)不同的類型。如此,Rust 的類型檢查就會(huì)將任何在只能使用發(fā)布博文的地方使用草案博文的嘗試變?yōu)榫幾g時(shí)錯(cuò)誤。src/main.rs

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());
}

我們?nèi)匀幌M軌蚴褂?Post::new?創(chuàng)建一個(gè)新的草案博文,并能夠增加博文的內(nèi)容。不過不同于存在一個(gè)草案博文時(shí)返回空字符串的?content?方法,我們將使草案博文完全沒有?content?方法。這樣如果嘗試獲取草案博文的內(nèi)容,將會(huì)得到一個(gè)方法不存在的編譯錯(cuò)誤。這使得我們不可能在生產(chǎn)環(huán)境意外顯示出草案博文的內(nèi)容,因?yàn)檫@樣的代碼甚至就不能編譯。?src/lib.rs

pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
    pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

    pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

Post?和?DraftPost?結(jié)構(gòu)體都有一個(gè)私有的?content?字段來儲(chǔ)存博文的文本。這些結(jié)構(gòu)體不再有?state?字段因?yàn)槲覀儗顟B(tài)編碼改為結(jié)構(gòu)體類型。Post?將代表發(fā)布的博文,它有一個(gè)返回?content?的?content?方法。

仍然有一個(gè)?Post::new?函數(shù),不過不同于返回?Post?實(shí)例,它返回?DraftPost?的實(shí)例?,F(xiàn)在不可能創(chuàng)建一個(gè)?Post?實(shí)例,因?yàn)?content?是私有的同時(shí)沒有任何函數(shù)返回?Post。

DraftPost?上定義了一個(gè)?add_text?方法,這樣就可以像之前那樣向?content?增加文本,不過注意?DraftPost?并沒有定義?content?方法!如此現(xiàn)在程序確保了所有博文都從草案開始,同時(shí)草案博文沒有任何可供展示的內(nèi)容。任何繞過這些限制的嘗試都會(huì)產(chǎn)生編譯錯(cuò)誤。

實(shí)現(xiàn)狀態(tài)轉(zhuǎn)移為不同類型的轉(zhuǎn)換

那么如何得到發(fā)布的博文呢?我們希望強(qiáng)制執(zhí)行的規(guī)則是草案博文在可以發(fā)布之前必須被審核通過。等待審核狀態(tài)的博文應(yīng)該仍然不會(huì)顯示任何內(nèi)容。讓我們通過增加另一個(gè)結(jié)構(gòu)體?PendingReviewPost?來實(shí)現(xiàn)這個(gè)限制,在?DraftPost?上定義?request_review?方法來返回?PendingReviewPost,并在?PendingReviewPost?上定義?approve?方法來返回?Post。src/lib.rs

impl DraftPost {
    // --snip--

    pub fn request_review(self) -> PendingReviewPost {
        PendingReviewPost {
            content: self.content,
        }
    }
}

pub struct PendingReviewPost {
    content: String,
}

impl PendingReviewPost {
    pub fn approve(self) -> Post {
        Post {
            content: self.content,
        }
    }
}

request_review?和?approve?方法獲取?self?的所有權(quán),因此會(huì)消費(fèi)?DraftPost?和?PendingReviewPost?實(shí)例,并分別轉(zhuǎn)換為?PendingReviewPost?和發(fā)布的?Post。這樣在調(diào)用?request_review?之后就不會(huì)遺留任何?DraftPost?實(shí)例,后者同理。PendingReviewPost?并沒有定義?content?方法,所以嘗試讀取其內(nèi)容會(huì)導(dǎo)致編譯錯(cuò)誤,DraftPost?同理。因?yàn)槲ㄒ坏玫蕉x了?content?方法的?Post?實(shí)例的途徑是調(diào)用?PendingReviewPost?的?approve?方法,而得到?PendingReviewPost?的唯一辦法是調(diào)用?DraftPost?的?request_review?方法,現(xiàn)在我們就將發(fā)博文的工作流編碼進(jìn)了類型系統(tǒng)。

這也意味著不得不對(duì)?main?做出一些小的修改。因?yàn)?request_review?和?approve?返回新實(shí)例而不是修改被調(diào)用的結(jié)構(gòu)體,所以我們需要增加更多的?let post =?覆蓋賦值來保存返回的實(shí)例。也不再能斷言草案和等待審核的博文的內(nèi)容為空字符串了,我們也不再需要他們:不能編譯嘗試使用這些狀態(tài)下博文內(nèi)容的代碼。

use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");

    let post = post.request_review();

    let post = post.approve();

    assert_eq!("I ate a salad for lunch today", post.content());
}

不得不修改?main?來重新賦值?post?使得這個(gè)實(shí)現(xiàn)不再完全遵守面向?qū)ο蟮臓顟B(tài)模式:狀態(tài)間的轉(zhuǎn)換不再完全封裝在?Post?實(shí)現(xiàn)中。然而,得益于類型系統(tǒng)和編譯時(shí)類型檢查,我們得到了的是無效狀態(tài)是不可能的!這確保了某些特定的 bug,比如顯示未發(fā)布博文的內(nèi)容,將在部署到生產(chǎn)環(huán)境之前被發(fā)現(xiàn)。

即便 Rust 能夠?qū)崿F(xiàn)面向?qū)ο笤O(shè)計(jì)模式,也有其他像將狀態(tài)編碼進(jìn)類型這樣的模式存在。這些模式有著不同的權(quán)衡取舍。雖然你可能非常熟悉面向?qū)ο竽J剑匦滤伎歼@些問題來利用 Rust 提供的像在編譯時(shí)避免一些 bug 這樣有益功能。在 Rust 中面向?qū)ο竽J讲⒉豢偸亲詈玫慕鉀Q方案,因?yàn)?Rust 擁有像所有權(quán)這樣的面向?qū)ο笳Z言所沒有的功能。

總結(jié)

閱讀本章后,不管你是否認(rèn)為 Rust 是一個(gè)面向?qū)ο笳Z言,現(xiàn)在你都見識(shí)了 trait 對(duì)象是一個(gè) Rust 中獲取部分面向?qū)ο蠊δ艿姆椒ā?dòng)態(tài)分發(fā)可以通過犧牲少量運(yùn)行時(shí)性能來為你的代碼提供一些靈活性。這些靈活性可以用來實(shí)現(xiàn)有助于代碼可維護(hù)性的面向?qū)ο竽J?。Rust 也有像所有權(quán)這樣不同于面向?qū)ο笳Z言的功能。面向?qū)ο竽J讲⒉豢偸抢?Rust 優(yōu)勢(shì)的最好方式,但也是可用的選項(xiàng)。

參考:Rust 的面向?qū)ο缶幊烫匦?- Rust 程序設(shè)計(jì)語言 簡(jiǎn)體中文版 (bootcss.com)文章來源地址http://www.zghlxwxcb.cn/news/detail-666473.html

到了這里,關(guān)于【Rust】Rust學(xué)習(xí) 第十七章Rust 的面向?qū)ο筇匦缘奈恼戮徒榻B完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包