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
  • PasteSpider是什麼? 一款使用.net編寫的開源的Linux容器部署助手,支持一鍵發佈,平滑升級,自動伸縮, Key-Value配置,項目網關,環境隔離,運行報表,差量升級,私有倉庫,集群部署,版本管理等! 30分鐘上手,讓開發也可以很容易的學會在linux上部署你得項目! [從需求角度介 ...
  • SQLSugar是什麼 **1. 輕量級ORM框架,專為.NET CORE開發人員設計,它提供了簡單、高效的方式來處理資料庫操作,使開發人員能夠更輕鬆地與資料庫進行交互 2. 簡化資料庫操作和數據訪問,允許開發人員在C#代碼中直接操作資料庫,而不需要編寫複雜的SQL語句 3. 支持多種資料庫,包括但 ...
  • 在C#中,經常會有一些耗時較長的CPU密集型運算,因為如果直接在UI線程執行這樣的運算就會出現UI不響應的問題。解決這類問題的主要途徑是使用多線程,啟動一個後臺線程,把運算操作放在這個後臺線程中完成。但是原生介面的線程操作有一些難度,如果要更進一步的去完成線程間的通訊就會難上加難。 因此,.NET類 ...
  • 一:背景 1. 講故事 前些天有位朋友在微信上丟了一個崩潰的dump給我,讓我幫忙看下為什麼出現了崩潰,在 Windows 的事件查看器上顯示的是經典的 訪問違例 ,即 c0000005 錯誤碼,不管怎麼說有dump就可以上windbg開幹了。 二:WinDbg 分析 1. 程式為誰崩潰了 在 Wi ...
  • CSharpe中的IO+NPOI+序列化 文件文件夾操作 學習一下常見的文件、文件夾的操作。 什麼是IO流? I:就是input O:就是output,故稱:輸入輸出流 將數據讀入記憶體或者記憶體輸出的過程。 常見的IO流操作,一般說的是[記憶體]與[磁碟]之間的輸入輸出。 作用 持久化數據,保證數據不再 ...
  • C#.NET與JAVA互通之MD5哈希V2024 配套視頻: 要點: 1.計算MD5時,SDK自帶的計算哈希(ComputeHash)方法,輸入輸出參數都是byte數組。就涉及到字元串轉byte數組轉換時,編碼選擇的問題。 2.輸入參數,字元串轉byte數組時,編碼雙方要統一,一般為:UTF-8。 ...
  • CodeWF.EventBus,一款靈活的事件匯流排庫,實現模塊間解耦通信。支持多種.NET項目類型,如WPF、WinForms、ASP.NET Core等。採用簡潔設計,輕鬆實現事件的發佈與訂閱。通過有序的消息處理,確保事件得到妥善處理。簡化您的代碼,提升系統可維護性。 ...
  • 一、基本的.NET框架概念 .NET框架是一個由微軟開發的軟體開發平臺,它提供了一個運行時環境(CLR - Common Language Runtime)和一套豐富的類庫(FCL - Framework Class Library)。CLR負責管理代碼的執行,而FCL則提供了大量預先編寫好的代碼, ...
  • 本章將和大家分享在ASP.NET Core中如何使用高級客戶端NEST來操作我們的Elasticsearch。 NEST是一個高級別的Elasticsearch .NET客戶端,它仍然非常接近原始Elasticsearch API的映射。所有的請求和響應都是通過類型來暴露的,這使得它非常適合快速上手 ...
  • 參考delphi的代碼更改為C# Delphi 檢測密碼強度 規則(仿 google) 仿 google 評分規則 一、密碼長度: 5 分: 小於等於 4 個字元 10 分: 5 到 7 字元 25 分: 大於等於 8 個字元 二、字母: 0 分: 沒有字母 10 分: 全都是小(大)寫字母 20 ...