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 微服務框架,幫助我們輕鬆構建和管理微服務應用。 本框架不僅支持 Consul 服務註 ...
  • 先看一下效果吧: 如果不會寫動畫或者懶得寫動畫,就直接交給Blend來做吧; 其實Blend操作起來很簡單,有點類似於在操作PS,我們只需要設置關鍵幀,滑鼠點來點去就可以了,Blend會自動幫我們生成我們想要的動畫效果. 第一步:要創建一個空的WPF項目 第二步:右鍵我們的項目,在最下方有一個,在B ...
  • Prism:框架介紹與安裝 什麼是Prism? Prism是一個用於在 WPF、Xamarin Form、Uno 平臺和 WinUI 中構建鬆散耦合、可維護和可測試的 XAML 應用程式框架 Github https://github.com/PrismLibrary/Prism NuGet htt ...
  • 在WPF中,屏幕上的所有內容,都是通過畫筆(Brush)畫上去的。如按鈕的背景色,邊框,文本框的前景和形狀填充。藉助畫筆,可以繪製頁面上的所有UI對象。不同畫筆具有不同類型的輸出( 如:某些畫筆使用純色繪製區域,其他畫筆使用漸變、圖案、圖像或繪圖)。 ...
  • 前言 嗨,大家好!推薦一個基於 .NET 8 的高併發微服務電商系統,涵蓋了商品、訂單、會員、服務、財務等50多種實用功能。 項目不僅使用了 .NET 8 的最新特性,還集成了AutoFac、DotLiquid、HangFire、Nlog、Jwt、LayUIAdmin、SqlSugar、MySQL、 ...
  • 本文主要介紹攝像頭(相機)如何採集數據,用於類似攝像頭本地顯示軟體,以及流媒體數據傳輸場景如傳屏、視訊會議等。 攝像頭採集有多種方案,如AForge.NET、WPFMediaKit、OpenCvSharp、EmguCv、DirectShow.NET、MediaCaptre(UWP),網上一些文章以及 ...
  • 前言 Seal-Report 是一款.NET 開源報表工具,擁有 1.4K Star。它提供了一個完整的框架,使用 C# 編寫,最新的版本採用的是 .NET 8.0 。 它能夠高效地從各種資料庫或 NoSQL 數據源生成日常報表,並支持執行複雜的報表任務。 其簡單易用的安裝過程和直觀的設計界面,我們 ...
  • 背景需求: 系統需要對接到XXX官方的API,但因此官方對接以及管理都十分嚴格。而本人部門的系統中包含諸多子系統,系統間為了穩定,程式間多數固定Token+特殊驗證進行調用,且後期還要提供給其他兄弟部門系統共同調用。 原則上:每套系統都必須單獨接入到官方,但官方的接入複雜,還要官方指定機構認證的證書 ...
  • 本文介紹下電腦設備關機的情況下如何通過網路喚醒設備,之前電源S狀態 電腦Power電源狀態- 唐宋元明清2188 - 博客園 (cnblogs.com) 有介紹過遠程喚醒設備,後面這倆天瞭解多了點所以單獨加個隨筆 設備關機的情況下,使用網路喚醒的前提條件: 1. 被喚醒設備需要支持這WakeOnL ...
  • 前言 大家好,推薦一個.NET 8.0 為核心,結合前端 Vue 框架,實現了前後端完全分離的設計理念。它不僅提供了強大的基礎功能支持,如許可權管理、代碼生成器等,還通過採用主流技術和最佳實踐,顯著降低了開發難度,加快了項目交付速度。 如果你需要一個高效的開發解決方案,本框架能幫助大家輕鬆應對挑戰,實 ...