面向?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)槲覀兿MWC變量被增加到列表或者被從列表刪除時(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
?字段的類型。只要?add
、remove
?和?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:
這告訴了我們,要么是我們傳遞了并不希望傳遞給?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è)博客的最終功能看起來像這樣:
- 博文從空白的草案開始。
- 一旦草案完成,請(qǐng)求審核博文。
- 一旦博文過審,它將被發(fā)表。
- 只有被發(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í)?Draft
、PendingReview
?和?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)。文章來源:http://www.zghlxwxcb.cn/news/detail-666473.html
參考: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)!