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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...