在原文上有刪減,原文鏈接Rust 的面向對象特性。 目錄面向對象語言的特征對象包含數據和行為封裝隱藏了實現細節繼承,作為類型系統與代碼共用顧及不同類型值的 trait 對象定義通用行為的 trait實現 traittrait 對象執行動態分發麵向對象設計模式的實現定義 Post 並新建一個草案狀態的 ...
在原文上有刪減,原文鏈接Rust 的面向對象特性。
目錄面向對象語言的特征
對象包含數據和行為
The Gang of Four 中對象的定義:
Object-oriented programs are made up of objects. An object packages both data and the procedures that operate on that data. The procedures are typically called methods or operations.
面向對象的程式是由對象組成的。一個 對象 包含數據和操作這些數據的過程。這些過程通常被稱為 方法 或 操作。
在這個定義下,Rust 是面向對象的:結構體和枚舉包含數據而 impl 塊提供了在結構體和枚舉之上的方法。雖然帶有方法的結構體和枚舉並不被稱為對象,但是它們提供了與對象相同的功能。
封裝隱藏了實現細節
封裝(encapsulation)的思想:對象的實現細節不能被使用對象的代碼獲取到,封裝使得改變和重構對象的內部時無需改變使用對象的代碼。Rust 可以使用 pub 關鍵字來決定模塊、類型、函數和方法是公有的,而預設情況下其他一切都是私有的。
AveragedCollection 結構體維護了一個整型列表和集合中所有元素的平均值:
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
在 AveragedCollection 結構體上實現了 add、remove 和 average 公有方法:
//保證變數被增加到列表或者被從列表刪除時,也會同時更新平均值
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;
}
}
繼承,作為類型系統與代碼共用
繼承(Inheritance)是一個很多編程語言都提供的機制,一個對象可以定義為繼承另一個對象定義中的元素,這使其可以獲得父對象的數據和行為,而無需重新定義。
在 Rust 中,沒有巨集則無法定義一個結構體繼承父結構體的成員和方法,不過 Rust 也提供了其他的解決方案。
選擇繼承有兩個主要的原因:
-
重用代碼:一旦為一個類型實現了特定行為,繼承可以對一個不同的類型重用這個實現。Rust 代碼中可以使用預設 trait 方法實現來進行有限的共用。
-
多態:表現為子類型可以用於父類型被使用的地方,這意味著如果多種對象共用特定的屬性,則可以相互替代使用。
Rust 選擇了一個不同的途徑,使用 trait 對象而不是繼承。
顧及不同類型值的 trait 對象
定義通用行為的 trait
定義一個帶有 draw 方法的 trait Draw:
pub trait Draw {
fn draw(&self);
}
一個 Screen 結構體的定義,它帶有一個欄位 components,其包含實現了 Draw trait 的 trait 對象的 vector:
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
在 Screen 上實現一個 run 方法,該方法在每個 component 上調用 draw 方法:
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
這與定義使用了帶有 trait bound 的泛型類型參數的結構體不同:泛型類型參數一次只能替代一個具體類型,而 trait 對象則允許在運行時替代多種具體類型。
一種 Screen 結構體的替代實現,其 run 方法使用泛型和 trait bound:
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 實例必須擁有一個全是 Button 類型或者全是 TextField 類型的組件列表。如果只需要同質(相同類型)集合,則傾向於使用泛型和 trait bound,因為其定義會在編譯時採用具體類型進行單態化。
實現 trait
一個實現了 Draw trait 的 Button 結構體:
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// code to actually draw a button
}
}
另一個使用 gui 的 crate 中,在 SelectBox 結構體上實現 Draw trait:
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
}
}
使用 trait 對象來存儲實現了相同 trait 的不同類型的值:
use gui::{Button, Screen};
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();
}
這個概念 —— 只關心值所反映的信息而不是其具體類型 —— 類似於動態類型語言中稱為 鴨子類型(duck typing)的概念:如果它走起來像一隻鴨子,叫起來像一隻鴨子,那麼它就是一隻鴨子!
嘗試使用一種沒有實現 trait 對象的 trait 的類型:
//這段代碼無法通過編譯!
//因為 String 沒有實現 rust_gui::Draw trait
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
trait 對象執行動態分發
當對泛型使用 trait bound 時編譯器所執行的單態化處理:編譯器為每一個被泛型類型參數代替的具體類型生成了函數和方法的非泛型實現。單態化產生的代碼在執行 靜態分發(static dispatch)。靜態分發發生於編譯器在編譯時就知曉調用了什麼方法的時候。這與 動態分發 (dynamic dispatch)相對,這時編譯器在編譯時無法知曉調用了什麼方法。在動態分發的場景下,編譯器會生成負責在運行時確定該調用什麼方法的代碼。
當使用 trait 對象時,Rust 必須使用動態分發,編譯器無法知曉所有可能用於 trait 對象代碼的類型,Rust 在運行時使用 trait 對象中的指針來知曉需要調用哪個方法。
面向對象設計模式的實現
狀態模式(state pattern)是一個面向對象設計模式,用狀態模式增量式地實現一個發佈博文的工作流以探索這個概念,博客的最終功能如下:
- 博文從空白的草案開始。
- 一旦草案完成,請求審核博文。
- 一旦博文過審,它將被髮表。
- 只有被髮表的博文的內容會被列印
展示了 blog crate 期望行為的代碼,代碼還不能編譯:
//這段代碼無法通過編譯!
use blog::Post;
fn main() {
//使用 Post::new 創建一個新的博文草案
let mut post = Post::new();
//在草案階段為博文編寫一些文本
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
//請求審核博文,content 返回空字元串。
post.request_review();
assert_eq!("", post.content());
//博文審核通過被髮表,content 文本將被返回
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
定義 Post 並新建一個草案狀態的實例
Post 結構體的定義和新建 Post 實例的 new 函數,State trait 和結構體 Draft:
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
//確保了無論何時新建一個 Post 實例都會從草案開始
state: Some(Box::new(Draft {})),
//將 content 設置為新建的空 String
content: String::new(),
}
}
}
//定義了所有不同狀態的博文所共用的行為
trait State {}
//狀態對象:初始狀態
struct Draft {}
//實現 State 狀態
impl State for Draft {}
存放博文內容的文本
實現方法 add_text 來向博文的 content 增加文本:
impl Post {
// --snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
確保博文草案的內容是空的
增加一個 Post 的 content 方法的占位實現,它總是返回一個空字元串 slice:
impl Post {
// --snip--
pub fn content(&self) -> &str {
""
}
}
請求審核博文來改變其狀態
實現 Post 和 State trait 的 request_review 方法,將其狀態由 Draft 改為 PendingReview:
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
}
}
Draft 的 request_review 方法需要返回一個新的,裝箱的 PendingReview 結構體的實例,其用來代表博文處於等待審核狀態。結構體 PendingReview 同樣也實現了 request_review 方法,不過它不進行任何狀態轉換,它返回自身。
狀態模式的優勢:無論 state 是何值,Post 的 request_review 方法都是一樣的,每個狀態只負責它自己的規則。
增加改變 content 行為的 approve 方法
approve 方法將與 request_review 方法類似:它會將 state 設置為審核通過時應處於的狀態。
為 Post 和 State trait 實現 approve 方法:
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
}
}
如果對 Draft 調用 approve 方法,並沒有任何效果,因為它會返回 self。當對 PendingReview 調用 approve 時,它返回一個新的、裝箱的 Published 結構體的實例。
更新 Post 的 content 方法來委托調用 State 的 content 方法:
//這段代碼無法通過編譯!
impl Post {
// --snip--
pub fn content(&self) -> &str {
//調用 as_ref 會返回一個 Option<&Box<dyn State>>
self.state.as_ref().unwrap().content(self)
}
// --snip--
}
為 State trait 增加 content 方法:
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
}
}
示例完成,通過發佈博文工作流的規則實現了狀態模式,圍繞這些規則的邏輯都存在於狀態對象中而不是分散在 Post 之中。
不使用枚舉是因為每一個檢查枚舉值的地方都需要一個 match 表達式或類似的代碼來處理所有可能的成員,這相比 trait 對象模式可能顯得更重覆。
狀態模式的權衡取捨
對於狀態模式來說,Post 的方法和使用 Post 的位置無需 match 語句,同時增加新狀態只涉及到增加一個新 struct 和為其實現 trait 的方法。
完全按照面向對象語言的定義實現這個模式並沒有儘可能地利用 Rust 的優勢,可以做一些修改將無效的狀態和狀態轉移變為編譯時錯誤。
將狀態和行為編碼為類型
將狀態編碼進不同的類型,Rust 的類型檢查就會將任何在只能使用發佈博文的地方使用草案博文的嘗試變為編譯時錯誤:
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
}
帶有 content 方法的 Post 和沒有 content 方法的 DraftPost:
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
}
}
//註意 DraftPost 並沒有定義 content 方法
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
實現狀態轉移為不同類型的轉換
PendingReviewPost 通過調用 DraftPost 的 request_review 創建,approve 方法將 PendingReviewPost 變為發佈的 Post:
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,
}
}
}
main 中使用新的博文工作流實現的修改:
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());
}