介面不能被繼承

閱讀時間約 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
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
測試介面大廠穎崴近期股價自先前低點上漲逾 30%,即使市場普遍認定公司Q4營運將持續低迷,但多數都已將目光看向2024年測試座將隨 AI 晶片出貨瓶頸解決需求快速成長,穎崴已成功卡位 NVIDIA 和 AMD 之主要供應商地位,預期可直接受惠,該如何解讀穎崴 2024 年營運狀況?
Thumbnail
前篇提要,介面功能主要會被分以下三個區塊介紹: 1. 左側設定區 (Part I) 2. 中間及右側預算設定區(Part II) 3. 帳戶內錢錢進出記錄功能 (Part III) 此篇終於來到尾聲,要介紹帳戶內錢錢建出紀錄功能。
Thumbnail
前篇提要,介面功能主要會被分以下三個區塊介紹: 1. 左側設定區 (Part I) 2. 中間及右側預算設定區(Part II) 3. 帳戶內錢錢進出記錄功能 (Part III) 上一篇已經介紹過左側設定區 (Part I),此篇要介紹中間及右側預算設定區。
Thumbnail
你已經註冊YNAB帳號了嗎? 還沒的話,可以先看這篇➡️開始你的YNAB體驗: 34天免費 我會將介面功能一一介紹,主要會分三個區塊,因內容多也會分別在三篇文章中闡述: 1. 左側設定區 (Part I) 2. 中間及右側預算設定區(Part II) 3. 帳戶內錢錢進出記錄功能 (Part I
Thumbnail
說到自己身為設計師,原來這裡也能置入自己的照片,看來我對這平台還有待研究(笑),為了從平面設計跳轉到使用者介面設計的領域,其實花了一年左右的時間在自學,甚至買課程做一些練習,大部分的作品都是自己摸索完成的,也藉由這先得一年期許能轉職成功,這麼一來又多了可以放自己作品集的地方了。不過通常自己的作品都會
Thumbnail
C# 介面 ( C# Interface ) – (C#教學) – 介面就是類別的接口, 就好像在電插一樣, 不同的電器有同一類與電力的接口. 要編程就像一個布局, 當引用一個class時, 會引用不同的method, property. 如果method的class可以轉換, 就大大簡化了編程.
Thumbnail
在科技翻譯中,使用者界面的翻譯,佔有相當的比重;這種翻譯在進行時,了解自己翻出來的文字,在用戶眼前會如何呈現出來,是非常重要的。如果在翻譯時沒有考慮到譯文呈現出來是什麼樣子,往往就會發生悲劇。
Thumbnail
【介面設計師與視覺設計師應該如何分工協作?】 你知道介面設計師與視覺設計師,在共同負責一個專案的時候,都是如何協作的嗎? 即使同樣都是為產品外觀所把關的設計師,但所負責的部分卻是大不相同喔!🧐 想知道介面設計與視覺設計互相合作的眉眉角角?趕快點進文章來看看吧!
Thumbnail
用了微軟的新瀏覽器「Edge」十分鐘,就看到幾個翻譯相關的問題。簡單講,就是做中文化的人/單位「會翻譯,但是不太用腦」。那麼,想做這類工作的人該注意些什麼呢?
Thumbnail
這篇文章會分享我平常關注的設計資訊來源以及淺談當初是如何自學Sketch,把自己當初從平面設計轉到介面設計時所收集的一些資源分享在這篇文章中,提供給初學及入門的人參考。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
測試介面大廠穎崴近期股價自先前低點上漲逾 30%,即使市場普遍認定公司Q4營運將持續低迷,但多數都已將目光看向2024年測試座將隨 AI 晶片出貨瓶頸解決需求快速成長,穎崴已成功卡位 NVIDIA 和 AMD 之主要供應商地位,預期可直接受惠,該如何解讀穎崴 2024 年營運狀況?
Thumbnail
前篇提要,介面功能主要會被分以下三個區塊介紹: 1. 左側設定區 (Part I) 2. 中間及右側預算設定區(Part II) 3. 帳戶內錢錢進出記錄功能 (Part III) 此篇終於來到尾聲,要介紹帳戶內錢錢建出紀錄功能。
Thumbnail
前篇提要,介面功能主要會被分以下三個區塊介紹: 1. 左側設定區 (Part I) 2. 中間及右側預算設定區(Part II) 3. 帳戶內錢錢進出記錄功能 (Part III) 上一篇已經介紹過左側設定區 (Part I),此篇要介紹中間及右側預算設定區。
Thumbnail
你已經註冊YNAB帳號了嗎? 還沒的話,可以先看這篇➡️開始你的YNAB體驗: 34天免費 我會將介面功能一一介紹,主要會分三個區塊,因內容多也會分別在三篇文章中闡述: 1. 左側設定區 (Part I) 2. 中間及右側預算設定區(Part II) 3. 帳戶內錢錢進出記錄功能 (Part I
Thumbnail
說到自己身為設計師,原來這裡也能置入自己的照片,看來我對這平台還有待研究(笑),為了從平面設計跳轉到使用者介面設計的領域,其實花了一年左右的時間在自學,甚至買課程做一些練習,大部分的作品都是自己摸索完成的,也藉由這先得一年期許能轉職成功,這麼一來又多了可以放自己作品集的地方了。不過通常自己的作品都會
Thumbnail
C# 介面 ( C# Interface ) – (C#教學) – 介面就是類別的接口, 就好像在電插一樣, 不同的電器有同一類與電力的接口. 要編程就像一個布局, 當引用一個class時, 會引用不同的method, property. 如果method的class可以轉換, 就大大簡化了編程.
Thumbnail
在科技翻譯中,使用者界面的翻譯,佔有相當的比重;這種翻譯在進行時,了解自己翻出來的文字,在用戶眼前會如何呈現出來,是非常重要的。如果在翻譯時沒有考慮到譯文呈現出來是什麼樣子,往往就會發生悲劇。
Thumbnail
【介面設計師與視覺設計師應該如何分工協作?】 你知道介面設計師與視覺設計師,在共同負責一個專案的時候,都是如何協作的嗎? 即使同樣都是為產品外觀所把關的設計師,但所負責的部分卻是大不相同喔!🧐 想知道介面設計與視覺設計互相合作的眉眉角角?趕快點進文章來看看吧!
Thumbnail
用了微軟的新瀏覽器「Edge」十分鐘,就看到幾個翻譯相關的問題。簡單講,就是做中文化的人/單位「會翻譯,但是不太用腦」。那麼,想做這類工作的人該注意些什麼呢?
Thumbnail
這篇文章會分享我平常關注的設計資訊來源以及淺談當初是如何自學Sketch,把自己當初從平面設計轉到介面設計時所收集的一些資源分享在這篇文章中,提供給初學及入門的人參考。