介面不能被繼承

更新於 2024/01/15閱讀時間約 8 分鐘

類似於trait/typeclass的特性系統能提供程式「延展性」,它能讓函式針對不同的類型做出不同的行為。這種機制與物件導向的繼承非常像,然而特性系統的彈性比較大一點,而且概念上也有一些差別。為了探討討論這些差異,我們必須深入了解繼承機制到底是什麼。


繼承並不是建立子類關係的唯一方法。所謂的子類關係代表存在一種單向的類型轉換,能把子類的變數無痛轉換成父類的變數,而這種轉換並不是建構新的變數,轉換前後都代表同一個變數,只有類型改變了。透過類型轉換能夠將子類的變數值設定給父類的變數,這代表這個變數的類型與變數值的實體類型可以不一樣。例如類別Cat的值可以設定給類別Animal的變數,因為Cat繼承自Animal。子類關係也可以透過協變和逆變產生,例如List<Cat>是List<Animal>的子類。有些語言例如TypeScript擁有匿名的結構類型,而這些結構自動擁有子類關係,例如 {x: number, y: number, z: number} 是 {x: number, y: number} 的子類。物件導向的(子類)多型就是透過子類關係讓一個函式能接受多種類型的參數,並根據實體類型的不同而做出不同的行為。但子類關係並不是只能靠繼承,因此子類多型並不一定直接由繼承完成,然而最底層還是透過繼承機制實現。


物件導向的類別定義由兩個部分組成,一個是定義資料(成員變數),另一個是定義介面(方法),而繼承機制同時具有建立子類資料和繼承介面的功能。因為類別同時具有介面的功能,當子類變數轉換成父類變數時,介面仍是使用子類所實作的介面方法(因為那些方法是屬於這個物件的),而子類多型正是依賴於這個機制實現的。然而介面不應該被繼承,也就是父類實作的介面子類不一定要實作。舉一個最明顯的例子,Equal介面定義了方法equals,用來描述物件的相等性。它必須符合一些規則,例如obj.equals(obj) == true。首先,這個方法應該只有比較相同的類型時有意義,但我們不能把它宣告成bool equals(Self obj),其中Self代表物件的類型,編譯器沒辦法幫你推斷這個類型,因為它會因繼承而改變。因此只能退而求其次,宣告成bool equals(T obj),其中T是類型參數。如果一個類別(例如User)想要實作這個介面,就必須明確指定要比較的類型(例如實作Equals<User>)。但是當繼承這個類別時,這個方法就變成比較不同類型的方法,或者可以說他應該是「比較User定義的成員變數」的介面。它的本意不是只比較部分資料,這可能會使繼承自User的類別(例如Admin)與User比較時得到非預期的結果:user.equals(admin) == true。事實上比較具有不同實體類別的物件本來就不是合理的,尤其是把它當作資料操作時。因此像是相等性、比較大小、複製等會自我參考類型的特性都不能被繼承或允許類型轉換,就算需要繼承也應該使用其他機制完成繼承,而非直接預設不覆寫方法。


之前的文章提過利用泛型可以做到擁有類似子類關係的資料結構,而特性系統則可以做到介面的功能。例如我們可以定義Display特性,它帶有方法fn show(T) -> String,所有實作這個特性的類型都可以當作是繼承這個介面(抽象類別)的類別。它並不是真的子類關係,類型資訊只是被參數化而並沒有被抹除,因此像是List<Display>的類型跟List<T> where T: Display是不同的,前者可以裝不同類型且都實作了Display的物件,而後者只能裝同類型的物件。具有子類關係的類型在進行類型轉換時會丟失類型資訊,因此只能在執行時期透過子類多型取得物件的實體類型,但它們能因此被當作同一個類型使用,這在一些時候是很有用的。rust可以使用trait object做到類似的事,因此上面的例子可以改寫成List<Box<dyn Display>>,其中dyn Display包含了所有實作Display特性的類型,而實際類型已經被抹除。dyn代表它利用dynamic dispatch呼叫方法,應該呼叫哪個方法是沒辦法在編譯時期推斷的,因為類型資訊已經丟失。這個特性在物件導向的程式語言是稀鬆平常的,因此在無意中浪費了一點時間與記憶體,而rust關注記憶體與執行效率,因此它把這個特性明確地用關鍵字標示起來。


若要在PureScript或是Haskell實現帶有介面的子類關係,可以使用動態方法把原本在編譯時期的資訊,也就是trait/typeclass的方法,帶到執行時期。這個方法很直觀,就是把特性的方法寫成成員變數,然後使用泛型預留延展空間,再使用exist把類型參數抹除。例如使用PureScript的Record可以寫成:


type Concrete r =

{ value :: int,

setValue :: Concrete r -> int -> Concrete r

| r }

type AbstractClass = exist r. Concrete r


這裡的 Concrete r 代表物件的實體類型,它沒有子類,但預留了類型參數r提供擴展。value是它的成員變數,而setValue是方法,這個方法接收實體類型並回傳同樣類型的物件。AbstractClass則透過存在量化把所有可能的類型參數r都包含進來,所有 Concrete r 都是它的子類。跟物件導向不同的是,它的方法類型可以帶有實體類型,因為我們先使用泛型定義實體類型,再把它們包成抽象類型,而不是直接定義抽象類型,因此可以很自然地使用物件的實體類型作為回傳值。相比之下,物件導向只有自己(this)可以是實體類型,因此它很依賴變數修改與改變狀態來傳遞訊息。


這種方法的缺點是它的方法可以被任意改寫,即使兩個物件具有相同的實體類型,因為它的方法實際上只是類型剛好是函式的成員變數而已。在這種模型下,要建構一個物件就必須先準備方法給他,再把其他資料作為參數讓使用者傳入。這某種程度上可以看作是一種原型(prototype)的機制,只要修改原型就能改變物件的行為,這使得類型不再能描述變數的行為,並且可能會因此造成非預期的結果。例如考慮兩個帶有compare :: Concrete r -> Concrete r -> Ordering方法的物件,但它們的實作是不同的,這時使用誰的方法比較順序就會影響排序結果。物件的方法不應被當作成員函式,方法是被類型或介面描述的特性,它應該要擁有一致的行為。若是要修正這個問題,就必須把方法另外包成獨立且有意義的類型,而正好這就是trait/typeclass所做的事。因此應該改成:


type Concrete r = { value :: int | r }

class SetValue r where

setValue :: Concrete r -> int -> Concrete r

type AbstractClass =

exist r. (SetValue r, Concrete r)


在AbstractClass的定義中,SetValue r 代表實作此特性的結構,它跟資料 Concrete r 綁定在一起形成物件。因為一個類型基本上只能有一個實作特定特性的結構,因此不會出現前面的情況。rust的trait object基本上就是這種東西,其中 SetValue r 就是vtable,而 Concrete r 則是資料的部分。


事實上Haskell和PureScript的類型系統不支援一般的子類關係,例如定義一個延伸實體類型type Extend s = Concrete ( … | r ),它雖然也是AbstractClass的子類,但依據此類型定義的抽象類型則不是,它並不能直接轉換成AbstractClass。在這裡量化類型與實體類型的機制是不同的,因此不應把它們當成是同等的概念。rust雖然有子類關係,但只支援基於lifetime的子類關係,因為它們的變數類型與實體類型必須一致。具有子類關係的類型基本上就是擁有無限種可能的加法類型,因為一般來說類別總可以被延展。然而大部分時候一般的加法類型就足夠使用了。相反地,有限可能的加法類型反而有好處,因為我們總可以確定所有可能的狀態。Java引入sealed/permit也是為了限制這個過強的特性。即使很複雜,函數式編程的確能在某種程度模擬繼承機制,但它並不是合適的編程範式,其中的困難點都顯示了物件導向機制的問題。泛型、特性、加法類型等都已經足夠實現類似的功能,甚至可以做的更好,還有什麼理由要使用繼承?


avatar-img
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
have bear的沙龍 的其他內容
所謂的多型是讓一個函式或是資料結構能擁有多個不同的類型,其中上一篇文章所談的就是參數多型(parametric polymorphism),這篇文章將繼續討論特設多型(ad hoc polymorphism)。特設多型跟泛型的差別在於:泛型函式對於所有的類型只能有一種實作,而特設多型會根據類型有不同
前幾篇文章討論了類型系統的合理性,而這會影響我們對於變數與函式是什麼的理解。其中泛型是當中很重要的一個元素,很多討論都是基於泛型的使用。泛型會大大地增加類型系統的複雜度,因此有些語言選擇不提供泛型(go),但缺少泛型又會使簡單的容器都無法用類型精確描述。泛型的強大必須結合有紀律的類型系統才能顯現,但
函式跟資料結構一樣都有類型,它不只是特定於函式的概念,而是跟int, tuple<bool,float>等類型同等的概念。在c++函式的類型可以寫做如std::function<int(float,float)>,它可以放在tuple, array等容器裡,當然也可以作為函式的參數或是傳回值,如st
前幾篇文章在討論類型時,只討論了乘法與加法類型,這只是最基礎的類型構造方式,另外還有函式類型和泛型等概念還沒討論。在討論函式的類型之前,必須先討論函式的正確用法。對於程序式編程來說,函式是一段可重複使用的執行代碼,輸入的參數是用來控制執行行為的,因此比起函式(function)更應該稱它為程序(pr
物件導向程式語言的類型系統總是不合理的,這些程式語言承襲至舊的語言不好的特性,而人們並沒有意識到它的問題,或者比起健全(soundness)它們更注重熟悉(familiarity)。多數的物件導向程式語言都源自c/c++,而c++有太多糟糕的設計,然而同一時期出來的Haskell卻很少有類似的問題。
寫上一篇文章時我意識到,類型,類別,型別這幾個詞在物件導向當道的現代變得有些模糊,常常會不小心當成是物件導向的類,但我指的其實是資料類型。在英文中,我常常這樣區分它們:物件導向的類是class,代表的是抽象的物件模型,而類型是type/data type,代表的是實際的資料結構。正如上一篇文章所說,
所謂的多型是讓一個函式或是資料結構能擁有多個不同的類型,其中上一篇文章所談的就是參數多型(parametric polymorphism),這篇文章將繼續討論特設多型(ad hoc polymorphism)。特設多型跟泛型的差別在於:泛型函式對於所有的類型只能有一種實作,而特設多型會根據類型有不同
前幾篇文章討論了類型系統的合理性,而這會影響我們對於變數與函式是什麼的理解。其中泛型是當中很重要的一個元素,很多討論都是基於泛型的使用。泛型會大大地增加類型系統的複雜度,因此有些語言選擇不提供泛型(go),但缺少泛型又會使簡單的容器都無法用類型精確描述。泛型的強大必須結合有紀律的類型系統才能顯現,但
函式跟資料結構一樣都有類型,它不只是特定於函式的概念,而是跟int, tuple<bool,float>等類型同等的概念。在c++函式的類型可以寫做如std::function<int(float,float)>,它可以放在tuple, array等容器裡,當然也可以作為函式的參數或是傳回值,如st
前幾篇文章在討論類型時,只討論了乘法與加法類型,這只是最基礎的類型構造方式,另外還有函式類型和泛型等概念還沒討論。在討論函式的類型之前,必須先討論函式的正確用法。對於程序式編程來說,函式是一段可重複使用的執行代碼,輸入的參數是用來控制執行行為的,因此比起函式(function)更應該稱它為程序(pr
物件導向程式語言的類型系統總是不合理的,這些程式語言承襲至舊的語言不好的特性,而人們並沒有意識到它的問題,或者比起健全(soundness)它們更注重熟悉(familiarity)。多數的物件導向程式語言都源自c/c++,而c++有太多糟糕的設計,然而同一時期出來的Haskell卻很少有類似的問題。
寫上一篇文章時我意識到,類型,類別,型別這幾個詞在物件導向當道的現代變得有些模糊,常常會不小心當成是物件導向的類,但我指的其實是資料類型。在英文中,我常常這樣區分它們:物件導向的類是class,代表的是抽象的物件模型,而類型是type/data type,代表的是實際的資料結構。正如上一篇文章所說,
你可能也想看
Google News 追蹤
Thumbnail
徵的就是你 🫵 超ㄅㄧㄤˋ 獎品搭配超瞎趴的四大主題,等你踹共啦!還有機會獲得經典的「偉士牌樂高」喔!馬上來參加本次的活動吧!
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
※ 單例模式介紹 ※ 定義:單例模式是一種設計模式,確保一個class(類)只有一個實例,並提供一個存取它的全域存取點。無論如何取值,皆只對這個實例取值。 ※ 目的:保證一個類別只會產生一個物件,而且提供存取該物件的統一方法。 ※ 講解:單例模式確保一個類無論怎麼 new 或 get,都只能拿
※ OPP第三大核心-多型 ※ 多型的基本定義: 多型是利用繼承的特性,讓不同的子類別可以實現相同的介面,但在呼叫這些介面的方法時會表現出不同的行為。這使得程式設計更具彈性和擴展性,避免了複雜的條件判斷式,同時促進了代碼的重用。 class Animal { makeSound() {
Thumbnail
過往的經驗,烙印在生命中。 當你願意看見並承認自己為此服務,便開始有了選擇。 那些銘刻與血親之中的,並不等同於你。 因此,是否要延續這樣的模式? 你仍擁有最終的決定權。
Thumbnail
本章節的目的是讓讀者瞭解C#的物件導向特性,包括類別、繼承、多型、封裝等基本概念,以及介面、抽象類別、靜態類別等進階主題。此外,本章節也將介紹如何使用列舉、委派、Lambda表達式、泛型及反射,這些都是C#中常見的強大功能。
多型性(polymorphism)是物件導向中的一個重要概念,它指的是同一個方法或函式在不同的物件類別中可以有不同的行為。在 Python 中,多型性通常是通過繼承和方法重寫(method overriding)來實現的。 主要是為了不同資料類型的實體提供統一的介面,我們藉由下面的程式範例來多理解
Thumbnail
最近對於"傳承"的意義反覆地思索... --------------------------------------------------- 生物性欲的本能驅使生物一代傳一代, "創造宇宙繼起之生命" 但為什麼當初生命造起之初 就下了有限的設定呢? 對於無限的生命,傳承無法存在意義
那些賦予、冠加在我們身上的任何特徵、屬性
Thumbnail
CSS 的繼承性是開發網頁樣式時的一個重要概念,它使得樣式設計更加靈活和高效,有助於提高程式碼的可讀性、一致性和可重用性,並加快開發速度,從而提供更好的開發體驗。
Thumbnail
代理模式通過封裝原始對象來實現對該對象的控制和管理,同時不改變原始對象的行為或客戶端與該對象互動的方式,以此介入或增強對該對象的訪問和操作。
Thumbnail
在物件導向程式設計的進階階段,學生將學習繼承、介面、抽象類別等核心概念。繼承允許類別共享屬性和方法,介面確保實現類別提供特定的方法實現,而抽象類別定義了基本結構供子類別擴展。這些知識點有助於提升程式碼的重用性、擴展性和維護性。
Thumbnail
徵的就是你 🫵 超ㄅㄧㄤˋ 獎品搭配超瞎趴的四大主題,等你踹共啦!還有機會獲得經典的「偉士牌樂高」喔!馬上來參加本次的活動吧!
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
※ 單例模式介紹 ※ 定義:單例模式是一種設計模式,確保一個class(類)只有一個實例,並提供一個存取它的全域存取點。無論如何取值,皆只對這個實例取值。 ※ 目的:保證一個類別只會產生一個物件,而且提供存取該物件的統一方法。 ※ 講解:單例模式確保一個類無論怎麼 new 或 get,都只能拿
※ OPP第三大核心-多型 ※ 多型的基本定義: 多型是利用繼承的特性,讓不同的子類別可以實現相同的介面,但在呼叫這些介面的方法時會表現出不同的行為。這使得程式設計更具彈性和擴展性,避免了複雜的條件判斷式,同時促進了代碼的重用。 class Animal { makeSound() {
Thumbnail
過往的經驗,烙印在生命中。 當你願意看見並承認自己為此服務,便開始有了選擇。 那些銘刻與血親之中的,並不等同於你。 因此,是否要延續這樣的模式? 你仍擁有最終的決定權。
Thumbnail
本章節的目的是讓讀者瞭解C#的物件導向特性,包括類別、繼承、多型、封裝等基本概念,以及介面、抽象類別、靜態類別等進階主題。此外,本章節也將介紹如何使用列舉、委派、Lambda表達式、泛型及反射,這些都是C#中常見的強大功能。
多型性(polymorphism)是物件導向中的一個重要概念,它指的是同一個方法或函式在不同的物件類別中可以有不同的行為。在 Python 中,多型性通常是通過繼承和方法重寫(method overriding)來實現的。 主要是為了不同資料類型的實體提供統一的介面,我們藉由下面的程式範例來多理解
Thumbnail
最近對於"傳承"的意義反覆地思索... --------------------------------------------------- 生物性欲的本能驅使生物一代傳一代, "創造宇宙繼起之生命" 但為什麼當初生命造起之初 就下了有限的設定呢? 對於無限的生命,傳承無法存在意義
那些賦予、冠加在我們身上的任何特徵、屬性
Thumbnail
CSS 的繼承性是開發網頁樣式時的一個重要概念,它使得樣式設計更加靈活和高效,有助於提高程式碼的可讀性、一致性和可重用性,並加快開發速度,從而提供更好的開發體驗。
Thumbnail
代理模式通過封裝原始對象來實現對該對象的控制和管理,同時不改變原始對象的行為或客戶端與該對象互動的方式,以此介入或增強對該對象的訪問和操作。
Thumbnail
在物件導向程式設計的進階階段,學生將學習繼承、介面、抽象類別等核心概念。繼承允許類別共享屬性和方法,介面確保實現類別提供特定的方法實現,而抽象類別定義了基本結構供子類別擴展。這些知識點有助於提升程式碼的重用性、擴展性和維護性。