介面不能被繼承

更新於 發佈於 閱讀時間約 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
留言分享你的想法!
avatar-img
have bear的沙龍
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
have bear的沙龍的其他內容
2024/02/15
這個系列的文章主要專注於物件導向到函數式編程的差異與分析,並針對概念與機制上的不同進行比較。很多人說物件導向和函數式編程沒有哪個比較好的問題,只有哪個比較適合的問題,然而我並不這麼認為,我透過這一系列的文章從各個角度討論它們之間的優缺點就是為了闡述我的觀點。物件導向錯在沒有理論基礎,但它贏在熟悉性,
2024/02/15
這個系列的文章主要專注於物件導向到函數式編程的差異與分析,並針對概念與機制上的不同進行比較。很多人說物件導向和函數式編程沒有哪個比較好的問題,只有哪個比較適合的問題,然而我並不這麼認為,我透過這一系列的文章從各個角度討論它們之間的優缺點就是為了闡述我的觀點。物件導向錯在沒有理論基礎,但它贏在熟悉性,
2024/02/08
前一篇文章中所提的函數式的三個機制明確說明了它關注的規則與能力具體是什麼。然而這套對於函數式編程的定義主要基於特定的類型系統,作為一個編程範式來說過於狹隘(物件導向的定義也是這樣)。更廣義的,我認為函數式編程主要依循三個原則,它們可以應用於任何程式語言,就算沒有靜態類型系統的支援也可以。例如在不管類
2024/02/08
前一篇文章中所提的函數式的三個機制明確說明了它關注的規則與能力具體是什麼。然而這套對於函數式編程的定義主要基於特定的類型系統,作為一個編程範式來說過於狹隘(物件導向的定義也是這樣)。更廣義的,我認為函數式編程主要依循三個原則,它們可以應用於任何程式語言,就算沒有靜態類型系統的支援也可以。例如在不管類
2024/02/04
前面談了那麽多函數式編程與物件導向的差異,但我們還沒定義函數式編程。就像物件導向,函數式編程沒有明確的定義,每個人對於什麼是函數式編程都有不同的看法。在這裡我會總結前面的討論,給出我對於函數式編程的觀點。 物件導向注重封裝與延展性,因此一般基於三個機制:繼承、多型、封裝,它們代表了物件導向所重
2024/02/04
前面談了那麽多函數式編程與物件導向的差異,但我們還沒定義函數式編程。就像物件導向,函數式編程沒有明確的定義,每個人對於什麼是函數式編程都有不同的看法。在這裡我會總結前面的討論,給出我對於函數式編程的觀點。 物件導向注重封裝與延展性,因此一般基於三個機制:繼承、多型、封裝,它們代表了物件導向所重
看更多
你可能也想看
Thumbnail
孩子寫功課時瞇眼?小心近視!這款喜光全光譜TIONE⁺光健康智慧檯燈,獲眼科院長推薦,網路好評不斷!全光譜LED、180cm大照明範圍、5段亮度及色溫調整、350度萬向旋轉,讓孩子學習更舒適、保護眼睛!
Thumbnail
孩子寫功課時瞇眼?小心近視!這款喜光全光譜TIONE⁺光健康智慧檯燈,獲眼科院長推薦,網路好評不斷!全光譜LED、180cm大照明範圍、5段亮度及色溫調整、350度萬向旋轉,讓孩子學習更舒適、保護眼睛!
Thumbnail
創作者營運專員/經理(Operations Specialist/Manager)將負責對平台成長及收入至關重要的 Partnership 夥伴創作者開發及營運。你將發揮對知識與內容變現、影響力變現的精準判斷力,找到你心中的潛力新星或有聲量的中大型創作者加入 vocus。
Thumbnail
創作者營運專員/經理(Operations Specialist/Manager)將負責對平台成長及收入至關重要的 Partnership 夥伴創作者開發及營運。你將發揮對知識與內容變現、影響力變現的精準判斷力,找到你心中的潛力新星或有聲量的中大型創作者加入 vocus。
Thumbnail
本章節是一個初級的 TypeScript 教學,主要介紹了 TypeScript 中物件導向程式設計的各種核心概念,包括類別、建構子、存取修飾子、繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda 表達式、泛型和反射等。每個概念都通過詳細的解釋和實例代碼來進行深入的介紹。
Thumbnail
本章節是一個初級的 TypeScript 教學,主要介紹了 TypeScript 中物件導向程式設計的各種核心概念,包括類別、建構子、存取修飾子、繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda 表達式、泛型和反射等。每個概念都通過詳細的解釋和實例代碼來進行深入的介紹。
Thumbnail
本章節旨在介紹JavaScript中的物件導向編程。內容包括類別(Class)的定義和使用,建構子的作用,以及公開,私有,受保護(Protected)等不同訪問修飾符的概念。此外,還涵蓋了繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda表達式、泛型、反射等物件導向的主要觀念。
Thumbnail
本章節旨在介紹JavaScript中的物件導向編程。內容包括類別(Class)的定義和使用,建構子的作用,以及公開,私有,受保護(Protected)等不同訪問修飾符的概念。此外,還涵蓋了繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda表達式、泛型、反射等物件導向的主要觀念。
Thumbnail
本章節的目的是讓讀者瞭解C#的物件導向特性,包括類別、繼承、多型、封裝等基本概念,以及介面、抽象類別、靜態類別等進階主題。此外,本章節也將介紹如何使用列舉、委派、Lambda表達式、泛型及反射,這些都是C#中常見的強大功能。
Thumbnail
本章節的目的是讓讀者瞭解C#的物件導向特性,包括類別、繼承、多型、封裝等基本概念,以及介面、抽象類別、靜態類別等進階主題。此外,本章節也將介紹如何使用列舉、委派、Lambda表達式、泛型及反射,這些都是C#中常見的強大功能。
Thumbnail
上篇我們已經把風格融入在一個網路之中,實現了訓練一次就可以轉換不同的圖片成我們訓練的風格,但是這樣還不夠,因為這樣每個風格都得訓練一個網路來轉換,太浪費了,那麼,我們有沒有辦法在同一個網路中訓練多個風格呢?
Thumbnail
上篇我們已經把風格融入在一個網路之中,實現了訓練一次就可以轉換不同的圖片成我們訓練的風格,但是這樣還不夠,因為這樣每個風格都得訓練一個網路來轉換,太浪費了,那麼,我們有沒有辦法在同一個網路中訓練多個風格呢?
Thumbnail
CSS 的繼承性是開發網頁樣式時的一個重要概念,它使得樣式設計更加靈活和高效,有助於提高程式碼的可讀性、一致性和可重用性,並加快開發速度,從而提供更好的開發體驗。
Thumbnail
CSS 的繼承性是開發網頁樣式時的一個重要概念,它使得樣式設計更加靈活和高效,有助於提高程式碼的可讀性、一致性和可重用性,並加快開發速度,從而提供更好的開發體驗。
Thumbnail
default methods 似乎也引起不小的討論,因為 default methods 加上可以實作多個介面,已經有點像 C++ 的多重繼承了,只差在沒辦法繼承成員變數而已,是好是壞就看怎麼使用了。我個人覺得還蠻方便的
Thumbnail
default methods 似乎也引起不小的討論,因為 default methods 加上可以實作多個介面,已經有點像 C++ 的多重繼承了,只差在沒辦法繼承成員變數而已,是好是壞就看怎麼使用了。我個人覺得還蠻方便的
Thumbnail
代理模式通過封裝原始對象來實現對該對象的控制和管理,同時不改變原始對象的行為或客戶端與該對象互動的方式,以此介入或增強對該對象的訪問和操作。
Thumbnail
代理模式通過封裝原始對象來實現對該對象的控制和管理,同時不改變原始對象的行為或客戶端與該對象互動的方式,以此介入或增強對該對象的訪問和操作。
Thumbnail
在物件導向程式設計的進階階段,學生將學習繼承、介面、抽象類別等核心概念。繼承允許類別共享屬性和方法,介面確保實現類別提供特定的方法實現,而抽象類別定義了基本結構供子類別擴展。這些知識點有助於提升程式碼的重用性、擴展性和維護性。
Thumbnail
在物件導向程式設計的進階階段,學生將學習繼承、介面、抽象類別等核心概念。繼承允許類別共享屬性和方法,介面確保實現類別提供特定的方法實現,而抽象類別定義了基本結構供子類別擴展。這些知識點有助於提升程式碼的重用性、擴展性和維護性。
Thumbnail
針對 JavaScript 中的原始型別和隱性轉型進行了詳細的探討
Thumbnail
針對 JavaScript 中的原始型別和隱性轉型進行了詳細的探討
Thumbnail
[Python基礎]淺談類別 先前淺談了類別的用法,這次要在來研究一下類別繼承的概念。 延續蛋糕的案例的概念,同樣為食物,所以可以由食物當作父類別來延伸,蛋糕則是食物的子類別,若同樣為食物一定有相同的方法(函式)是固定的跟名字(屬性),可以讓子類別(蛋糕)來繼承沿用,然後也有其他子類別也可以繼承
Thumbnail
[Python基礎]淺談類別 先前淺談了類別的用法,這次要在來研究一下類別繼承的概念。 延續蛋糕的案例的概念,同樣為食物,所以可以由食物當作父類別來延伸,蛋糕則是食物的子類別,若同樣為食物一定有相同的方法(函式)是固定的跟名字(屬性),可以讓子類別(蛋糕)來繼承沿用,然後也有其他子類別也可以繼承
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News