Rust 的面向對象特性

来源:https://www.cnblogs.com/timefiles/p/18040752
-Advertisement-
Play Games

在原文上有刪減,原文鏈接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());
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 現在免費證書只能申請三個月(之前還能申請十二個月),擁有acme能力對於小的站點來說就比較需要,可以比較好的部署也不用關心TLS帶來的煩惱。 ...
  • ORM,全稱為Object-Relational Mapping,即對象關係映射,是一種程式技術,用於實現面向對象編程語言里不同類型系統的數據之間的轉換。從效果上說,它其實是創建了一個可在編程語言里使用的“虛擬對象資料庫”。 ORM技術位於應用和資料庫之間,作為一層中間件,用於實體對象(例如 POJ ...
  • Java 包和 API Java 中的包 用於將相關的類分組在一起。可以將其視為文件目錄中的一個文件夾。我們使用包來避免名稱衝突,並編寫更易於維護的代碼。 包分為兩類: 內置包(來自 Java API 的包) 用戶定義的包(創建自己的包) 內置包 Java API 是一個預先編寫的類庫,可以在 Ja ...
  • Rust的智能指針有哪些?大多數人都能馬上答出Box<T>、Rc<T>和Arc<T>、Ref<T>和在非同步編程中很常見的Pin<P>等等。不過,有一個可能經常被大多數人遺忘的類型,它功能強大,利用好了可以節省很多複製開銷;它就是這篇文章的主角:Cow<B>。 什麼是COW(Copy-On-Write ...
  • 1.pom.xml引入依賴 <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.11</version> </dependency> 2.myba ...
  • 1.問題: 有如下代碼: public class Test { static { i = 0;// 給變數賦值可以正常編譯通過 System.out.print(i);// 編譯器會提示“非法向前引用”(illegal forward reference) } static int i = 1; ...
  • 線程安全 線程安全是多線程或多進程編程中的一個概念,在擁有共用數據的多條線程並行執行的程式中,線程安全的代碼會通過同步機制保證各個線程都可以正常且正確的執行,不會出現數據污染等意外情況。 線程安全的問題最主要還是由線程切換導致的,比如一個房間(進程)中有10顆糖(資源),除此之外還有3個小人(1個主 ...
  • 1.標準庫參考:shutil.rmtree。 根據設計,rmtree在包含只讀文件的文件夾樹上失敗。如果要刪除文件夾,不管它是否包含只讀文件,請使用 import shutil shutil.rmtree('/folder_name', ignore_errors=True) 2.從os.walk( ...
一周排行
    -Advertisement-
    Play Games
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...