Rust 從 0 到 1 - 面向對象編程 - 概念

 根據某些定義,Rust 是面向對象的;而在其它一些定義下,Rust 又不是。

面向對象編程(Object-Oriented Programming,OOP)是一種編程範式。對象(Object)的概念最早出現在 1960 年代的 Simula 語言中,其影響了 Alan Kay (Smalltalk 語言發明者之一),他在 1967 年提出了術語 “面向對象編程” 來描述其所發明的語言。對於 OOP 是什麼,在社區並未達成一致。根據某些定義,Rust 是面向對象的;而在其它一些定義下,Rust 又不是。本章中,我們會討論一些被普遍認同的面向對象特性以及 Rust 語言如何對這些特性提供支持的。接着,我們會展示如何在 Rust 中實現這些面向對象特性,並討論其和利用 Rust 語言的優勢實現的方案的利弊。下面我們先介紹這些普遍認同的面向對象編程特性。

對於面向對象必須包含哪些特性,在編程內並未達成一致意見。Rust 受很多不同的編程範式影響,包括面向對象編程,還有前面我們介紹過的函數式編程等等。面向對象編程語言被普遍認爲包含的特性是對象、封裝和繼承。讓我們看一下這些概念的含義以及 Rust 是否支持。

01

包含數據和行爲的對象

由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(Addison-Wesley Professional, 1994)編寫的書 Design Patterns: Elements of Reusable Object-Oriented Software 俗稱 “四人幫”(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 爲結構體和枚舉提供了方法。儘管帶有方法的結構體和枚舉並不被稱爲對象,但是根據上面的定義,他們提供的功能與對象一樣。

02

通過封裝隱藏實現細節

另一個 OOP 相關的概念是封裝(encapsulation)思想:使用對象的代碼無法訪問其實現細節。因此,唯一與對象交互的方式是通過其提供的公開 API;使用對象的代碼無法直接改變對象內部的數據或者行爲。這讓我們可以改變或重構對象內部代碼實現,而使用者無需改變其代碼。

我們前面的章節中如何進行封裝:我們可以在代碼中利用 pub 關鍵字來決定哪些模塊、類型、函數和方法是公有的(而默認情況下它們都是私有的)。譬如,我們可以定義一個包含 Vec 類型列表的結構體 AveragedCollection ;結構體中還有 1 個字段,他保存列表中所有值的平均值,這樣在需要獲得列表的平均值是可以隨時獲取它,而不用重新計算。也就是說,AveragedCollection 會緩存列表的平均值計算結果。參考下面的例子:

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

在上面的例子中,結構體被標記爲 pub,這樣其他代碼就可以使用它,但是在結構體內部的字段仍然是私有的。這一點非常重要,因爲我們希望平均值在列表發生改變時,會同時被更新。我們可以通過在結構體上實現 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;
    }
}

公有方法 add、remove 和 average 是訪問或修改 AveragedCollection 實例中數據的唯一方式。當使用 add 方法爲列表增加元素或使用 remove 方法從列表刪除元素時,這些方法會調用私有的 update_average 方法更新 average 字段。我們保持 list 和 average 字段是私有的,因此外部代碼無法直接增加或者刪除列表中的元素,否則當列表改變時, average 字段可能並未更新。

因爲我們已經封裝了 AveragedCollection 的實現細節,將來可以比較容易進行修改或重構,譬如,改變列表的數據結構:我們可以將列表的類型改爲 HashSet 。只要 add、remove 和 average 方法的定義保持不變,使用 AveragedCollection 的外部代碼就無需改變。相反,如果列表字段是公有的,並且外部代碼直接使用了這個列表,那麼使用者可能不得不做出修改。

綜上, Rust 也滿足封裝的特性。我們可以通過在代碼中選擇是否使用 pub 關鍵字來管理封裝。

03

繼承

繼承(Inheritance)是指一個對象可以繼承另一個對象,這使其可以獲得(繼承)其父對象的數據和行爲,而無需再重新定義。

如果面嚮對象語言必須要支持繼承的話,那麼 Rust 就不是面向對象的。在 Rust 中我們無法定義一個結構體繼承另一個結構體(父結構體)的成員和方法。然而,Rust 也提供了其它解決方案作爲替代。

我們選擇繼承有兩個主要的原因。第一個是爲了重用代碼:我們可以通過繼承重用另一個類型中實現的特定行爲。在 Rust 中我們可以通過 trait 方法的默認實現來共享代碼,譬如,在前面章節的例子中我們在 Summary trait 上增加的 summarize 方法的默認實現。任何實現了 Summary trait 的類型都可以直接使用 summarize 方法的默認實現。這和子類可以複用父類的方法實現類似。當實現 Summary trait 時我們也可以選擇覆蓋(override )默認實現,重新實現 summarize 方法,這類似於在子類中覆蓋從父類繼承的方法。

第二個使用繼承的原因與類型系統有關:我們可以在使用父類型的地方使用其子類型,即多態(polymorphism),具備某些相同特性的多個對象可以在運行時互相替代。


To many people, polymorphism is synonymous with inheritance. But it’s actually a more general concept that refers to code that can work with data of multiple types. For inheritance, those types are generally subclasses.

Rust instead uses generics to abstract over different possible types and trait bounds to impose constraints on what those types must provide. This is sometimes called bounded parametric polymorphism.

_很多人將多態等同於繼承。不過它是一個更爲通用的概念,指代碼可以用於可能包含不同數據的多種類型。對於繼承來說,這些類型通常是某個類型的子類。 _

Rust 則通過泛型來抽象不同的類型,並通過 trait bounds 約束類型所必須包含的行爲。這有時被稱爲 “有界參數多態


最近,在很多語言中繼承不再受到青睞,因爲其共享的內容超出所需,帶來的便利多於風險。子類不應總是共享其父類的所有特性,如此導致程序設計缺少靈活性,並可能導致某些方法調用對於子類沒有任何意義,或由於方法不適用於子類而造成錯誤。某些語言還限制了子類只能繼承一個父類,這進一步限制了程序設計的靈活性。

出於這些考慮,Rust 選擇了另一條路,即,使用 trait 對象(trait objects)而不是繼承。在下面的章節中,我們將討論在 Rust 中如何利用 trait 對象實現多態。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3dk1sCI84fMr2OcIHk7h_A