Go 語言嵌入和多態機制對比
最近花了很多時間在學習 Go 語言及高級語言通用的語言特性和機制,比如說高級語言的內存分配和垃圾回收 (有垃圾回收特性的語言),類型系統,反射和泛型等等。
大多數高級語言之間都有想通的語言特性和機制,如果掌握了這些通用的知識,不僅可以對語言有更深刻的理解,還能舉一反三,快速學習其他編程語言。
簡介
面嚮對象語言設計最早可以追溯到 SIMULA 67,但直到 1980 年的 Smalltalk80 將其完善,並隨着 Java 的崛起而全面流行起來。面向對象設計的語言大多都支持三個關鍵的語言特性:抽象數據類型、繼承以及多態(方法動態派發)。
本文將以 Go 語言爲主,講解一下 Go 語言在面向對象領域的一些特性以及高級編程語言的一些通用領域知識。
繼承
在面嚮對象語言中,繼承是代碼複用和部分修改的常見手段之一。通過繼承,子類型可以獲得父類型的屬性和行爲,並且子類型的實例可以被當做父類的實例使用。
繼承一般分爲基於原型的繼承(prototype-based inheritance)和基於類的繼承(class-based inheritance),Javascript 語言就實現了基於原型的繼承,而其他場景後端語言多是實現了基於類的繼承。
Go 語言並未實現上述兩種意義上的繼承,而是提供嵌入機制。嵌入可以理解爲一種組合或者代理模式的自動語法糖。
如上代碼所示,Animal 類型實現了 Eat 函數,而在 Cat 類型的定義中,嵌入了 Animal 類型。上述代碼基本上等同於下面這段使用組合形式創建的 Cat 結構體
被嵌入的 Animal 類型被稱爲內部類型,而 Cat 被稱爲外部類型。外部類型會持有一個內部類型的實例,並對外提供所有內部類型的函數,而這些函數的內部實現則是直接調用了內部類型實例的對應函數。
下面代碼展示了外部類型初始化和調用其對應函數。
可見,初始化 Cat 類型需要傳入一個 Animal 類型,並且可以直接調用其 Eat 函數。不過需要注意的是,不同與一般的繼承機制,Cat 並不是 Animal 的子類型,所以並不能作爲 Animal 類型使用。
而在基於類的繼承中,子類是可以充當父類的實例使用的,比如 Java 通過 extend 關鍵字來實現繼承,繼承類被稱爲子類,被繼承類被稱爲父類或超類,凡是接收父類的地方都可以使用子類。在 Go 語言中沒有父子類替換機制。
此外,繼承並不是面嚮對象語言專屬的概念,C 語言早在面嚮對象語言發明之前就提供了類似的機制來實現將數據結構僞裝成另一種數據結構的特性,具體如下代碼所示。
如上述代碼的 main 函數實現所示,通過指針將 Cat 被僞裝成 Animal 來使用,是因爲 Cat 是 Animal 結構體的一個超集,二者共同擁有成員變量的順序也是一樣的,二者的結構體如下圖所示。
使用 Animal 指針指向一個 Cat 類型的結構體,並且可以將作爲參數傳遞給 eat 函數,此時會調用以 Animal 類型指針爲參數的 eat 函數,而不是以 Cat 類型指針爲參數的 eat 函數。
同時需要注意的是,在 C 語言例子中,開發者必須強制將 Cat 向上轉型爲 Animal,而在真正的面向對象編程語言中,這種類型的向上轉換通常是隱式的,語言運行時或者編譯器爲我們自動做了類型轉換。
多態
在編程語言和類型系統中,多態(Polymorphism) 能爲不同數據類型的實體提供統一的接口,或使用一個單一的符號來表示多個不同的類型。
也就是說,多態能夠允許同一段代碼在不同上下文中擁有不同的類型,進行不同的實現綁定,從而在不影響類型檢查的情況下,爲不同類型編寫通用的代碼。如下代碼就體現了多態。
getChars 函數接收 IO 接口作爲參數,然後調用其 read 函數讀取數據。不同的 IO 其 read 函數是不同的,比如說從標準輸入讀取和從文件讀取。所以,傳入不同的 IO 接口實現類,則會調用其不同的 read 函數實現,也就是 read 函數綁定了不同的實現,也就是所謂的多態。
不同的語言有着不同的多態實現方式,目前常見的多態實現方式一共有三類,分別是:參數多態 (Parametric Polymorphism)、特定多態 ( Ad-hoc Polymorphism ) 和子類型多態 ( Subtype Polymorphism )。
類似於類型系統,按照代碼進行綁定的時間,多態可以分爲靜態多態(static polymorphism)和動態多態(Dynamic Polymorphism)。靜態多態的代碼綁定發生在編譯期,而動態多態的代碼綁定發生在運行時。靜多態犧牲靈活性獲取性能,是零成本抽象,而動多態犧牲性能獲取靈活性。
動多態在運行時需要額外讀取類信息等數據,花費更多時間,並佔用較多空間,所以一般情況下都使用靜多態。上述三種多態實現方式中,參數化多態和特定多態一般是靜多態,子類型多態一般是動態多態。
Go 語言只支持子類型多態 (1.18 版本將支持參數多態),Rust 語言支持參數多態和特定多態,而 Java 語言則支持參數多態和子類型多態。
參數化多態
參數化多態實際上是指定義複合類型的成員變量和函數的參數時不指定其具體的類型,而是在真正使用時將其類型作爲參數傳入,從而使得複合類型和函數對各種具體類型都適用,從而避免大量重複性的工作,多用於隊列,堆棧等容器類型和通用算法函數。
參數化多態是泛型 (generic programming) 的一種實現方式,Go 語言預計在 1.18 版本引入參數化多態實現泛型編程,從而將一直被人所詬病的缺乏泛型編程導致代碼重複的短板補齊。下述代碼就是 Go 語言將來參數化多態的表現。
如上代碼所示,定義 Vector 類型時聲明瞭一個類型參數 T,並標明它可以是任意類型 (any 關鍵字),然後在真正初始化 Vector 類型變量時,傳入類型 int,標明其實際上是一個 int 類型數組。
特定多態
特定多態是針對函數和操作符重載等特定問題的多態實現方案。它不像參數化泛型一樣,並不是一種通用多態方案,也不是編程語言類型系統的基礎特性。
函數重載(overloading)指的是多個函數擁有相同的名稱,但是擁有不同的參數和實現。而操作符重載類似於函數重載,針對不同的參數具有不同的實現。Go 語言中只有參數不同的函數會被判定爲命名重複,自然無法支持函數重載和特定多態。Java 代碼爲例講述函數重載和操作符重載。
上述代碼分別定義了參數爲 string 和 int 類型的 Add 函數,在實際調用時,會根據傳入的參數,調用不同的 Add 函數實現。
子類型多態
子類型多態是指一種父子類型的包含關係,子類型可以替代父類型作爲參數進行傳遞,當調用父類型函數時,運行時會根據調用對象的實際類型來調用不同的函數實現。
子類型多態多存在於 Java 等面嚮對象語言中,Go 因其 Structural Typing 類型系統也可以實現子類型多態。
在 Go 語言中, 如果類型實現了接口定義的所有函數,該類型就被認爲實現了該接口。當然,鴨子類型系統並不能精確的表達語言抽象數據類型的全部特性,因爲鴨子類型系統一般不進行靜態類型檢查,而語言會在編譯期進行類型檢查,所以語言的創造者們更喜歡用結構類型(Structural typing)一詞來表述語言抽象類型系統。
需要注意的是,這裏的子類型和繼承並不是同一個概念,子類型反應的是類型 (接口) 實現的關係,而繼承則是兩個對象之間的關係,所以 Go 語言並沒有繼承特性也能實現子類型多態。
如上代碼所示,File 實現了 IO 類型的 read 函數,從而被認爲是 IO 類型的子類型,所以就可以將類型爲 File 的變量 f 傳入參數爲 IO 類型的 getChars 函數中。getChars 函數中會調用參數的 read 方法,Go 語言運行時會根據參數的實際類型,進行函數綁定,調用 File 類型的 read 函數。這也體現了子類型多態屬於動態多態,因爲上述函數綁定發生在運行時。
C 語言也可以實現類似多態的代碼機制,瞭解其具體實現方式有利於我們對多態和接口實現的本質有更好地理解。
Linux 中的驅動 IO 設備正是使用了這一機制,每個 IO 設備都提供 open、close、read、write 和 seek 五個函數,在其他語言中可以將其定義爲接口或抽象類,而在 C 語言中的定義如下所示。
FILE 結構體中有五個函數指針類型成員變量,分別對應上述五個函數。而不同的 IO 設備代碼都需要各自實現自己版本的五個函數,並且將 FILE 結構體的函數指針變量指向對應的實現函數。
如下代碼所示,聲明瞭 FILE 類型的 console 變量,將對應的五個函數的指針傳入結構體中,作爲其成員變量。
最後,getchar 函數接收 FILE 類型的數據作爲參數,然後通過結構體的函數指針,調用對應的函數,傳入的 FILE 類型的數據不同,則函數指針不同,也就調動了不同的函數實現,從而展示了多態能力。
C 語言的多態能力也在 Redis 的 dict 實現中有所體現,Redis 中很多數據結構都是依賴哈希表 dictType 實現的,所以其定義了 dictType 結構體,其成員變量都是所需函數的函數指針。
然後其具體數據結構諸如 Set 則需要實現自己版本的函數,並將其函數指針填充到對應的參數上。
通過這兩個 C 語言的案例,我們可以發現多態是函數指針的一種應用,C 語言可以使用函數指針來模擬多態,而面向編程語言將危險的函數指針隱藏掉,內化成語言本身的特性,提供了更加安全和方便的多態實現機制。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/PdskjvyTPtkGP53VwCmLHw