閱讀本篇文章前,仔細想想看
- 泛用型別化名的如何進行宣告?
- 泛用化名註記在變數時的注意事項為何?
- 泛用函式的特點為何?
如果還不清楚可以看一下前一篇文章喔~
這一次在第四篇章又新增了泛型的機制,想當然,筆者還是得點出泛用介面與類別的特點。到底會擦出什麼樣的火花呢?
以下正文開始!
筆者為了好探討介面與類別結合,今天就以鏈結串列(Linked List)這個資料結構的實踐舉例,以下預設讀者已認識此資料結構,繼續講下去。
首先,普通情況下,我們可以訂立 LinkedList
與 LinkedListNode
這兩種介面:
以上的 LinkedList
介面有六個規格:
head
代表鏈結串列的首個元素 LinkedListNode
,由於鏈結串列可為空的狀態,因此不排除 head
為 null
型別的可能性length
是一個方法,輸出的是鏈結串列的長度(當然也可以使用普通的 number
型別而不採用 (): number
函式型別,但你可能必須要監控好 insert
或 remove
鏈結串列的值時,更新 length
的大小)at
是一個方法,輸入為 index
代表鏈結串列的位置,但是輸出既可以為 LinkedListNode
外也可以為 null
值insert
代表將某任意值 value
(型別為 any
)插入進鏈結串列,位置由 index
指定remove
則是根據 index
指名的位置移除連結串列裡的元素而代表鏈結串列的元素是 LinkedListNode
介面,裡面的結構很單純:
value
代表該元素所存的值next
則是取得下一個鏈結串列的元素,但結果也可以為 null
代表該元素可能是鏈結串列裡的最後一個元素不過想也知道,這個連結串列可以存取的值是任意型別的值 —— value
屬性對應到的是 any
型別,因此我們可以將其改成泛用介面的模式,使得彈性增大:
另外,泛用參數的命名是什麼其實不重要,所以你取 T
、U
或 V
甚至是口語化的名稱都無所謂,重點是看得懂就好。
從以上的程式碼得知,譬如使用 LinkedList<number>
(U
被取代為 number
)代表該鏈結串列存的元素必須符合 LinkedListNode<number>
這個介面下的規格;也就是說 LinkedListNode<number>
存的 value
必須為 number
型別。
泛用介面的宣告其實很簡單,就是這樣而已。
另外,有些讀者可能會疑惑:
為何不討論直接將介面的型別化名註記在變數上的案例?
筆者一開始有想要多做說明,不過後來想想,筆者認為這樣的討論結果不如讓讀者參考介面的推論篇章還比較快,差別就是多了泛用的機制,但是型別參數被顯性註記過後,跟普通型別差不了多少。
此外,讀者如果會了本系列自立的物件完整性理論,延伸推論出泛用介面的註記機制也是可以的。
通常泛用的介面會和類別結合在一起使用,因此筆者認為討論泛用類別的型別推論與註記重要性比起泛用介面還要大。
泛用類別部分,筆者快速帶過跟類別相關的功能。
以下就舉一些很蠢又很簡單的例子,為了展示泛用類別的機制。譬如說我們有 C
類別:
貼心小提示
讀者若是跳到本篇章然後不曉得成員變數、成員方法(Member Variables/Methods)等類別相關的東西,請記得參見《機動藍圖》篇章系列,筆者已經懶到不想貼哪一篇文章連結了QQ。
以上的 C
類別有:
memberProp
的成員變數,對應型別為 C
所宣告出的型別參數 T
memberFunc
則是輸出 memberProp
的值value
,分別覆寫或者輸出 memberProp
還記得前一篇講過的 —— 泛用的機制可以輔助 TypeScript 型別推論的功能,使得開發上能夠更靈活。
以下繼續展示泛用類別,或者乾脆說泛用機制超好用超變態的地方,以下筆者分幾個案例討論。
C
物件時填入型別參數以上的程式碼,筆者不對變數 instanceOfC1
註記任何型別,並且建構物件時使用 new C<number>
,代表 T
這個型別參數備取代為 number
型別。(圖)
圖一:單純建立 C<number>
,instanceOfC1
會被自動推論為 C<number>
圖二:由於成員變數與型別參數 T
進行綁定,理應出現的推論結果為 number
,結果確實也是 number
圖三:雖然成員方法沒有被顯性註記輸出的型別,但根據函式型別篇章,TypeScript 聰明地根據輸出判斷推論結果為 number
型別,因為輸出為成員變數 memberProp
,而該成員被綁定 number
型別
圖四:呼叫 Getter 方法時,因為輸出為 memberProp
,然後該成員綁定 number
型別,因此推論也是 number
型別
圖五:呼叫 Setter 方法時,因為該方法指定輸入的值 input
為型別參數 T
,而 T
在本案例被綁定為 number
型別,因此可以被數字代入
另外,根據類別存取方法篇章,Setter 方法接收錯誤型別的值就會發出警訊,而本案例被綁定的型別為 number
,代表如果代入非 number
型別的值就會輸出警訊唷~
圖六:被 TypeScript 打臉的感覺如何~?(請筆者不要唱衰讀者)
首先,筆者在講這個案例前必須補充,根據前一篇討論到的某個重點:
除非型別參數有預設值(又可稱作預設型別參數 Default Type Parameter),否則本身為泛用形式的型別化名,少掉任何一個型別參數的值就會出錯。
也就是說,你如果選擇這樣註記變數,一定會出錯。(如圖七)
圖七:就算筆者後面建構物件時亂填 Non-sense 的東西,TypeScript 讀到變數的註記時 —— 遇到泛用型別化名,少了任何一個型別參數就會跟你槓上(聽起來蠻有義氣的)
所以不可能會有變數註記泛用型別卻不填上型別參數的狀況 —— 變數的型別註記要的是確切的型別,而非泛用的形式。
一但泛用的型別形式確立了內部型別參數的值,它就會退化為普通的型別。
但若是該型別參數有預設型別,你才可以省掉指定型別參數的部分。
回過頭來,我們繼續探討情形 2,以下是測試的範例程式碼。
事實上,不是筆者偷懶,但是筆者真的認為情形 2 的推論結果跟情形 1 符合(事實上筆者親手測過 XDDD),所以打算讓讀者或者是推卸給讀者自行試試看,不然要張貼情形 2 推論每個單元的測試結果 — 一貼就六張圖了,佔太多版面,讀者應該也會覺得筆者廢話很多。(事實上筆者在本系列的廢話也不是一次兩次的事情了)
所以呢~ 請:
讀者試試看
試著將情形 2 的範例程式碼中的每個單元的推論結果驗證看看是不是跟情形 1 相符合?
另外,筆者雞婆的一定要測給讀者看 —— 泛用類別的型別推論的每種案例的主要原因是:以下描述的情形 3 的行為是很重要的!
其實對於使用 TypeScript 已成習慣的人應該很少察覺到這一回事,或者覺得應該理所當然;然而,這個機制可是在型別系統的推論(Type Inference)上,佔了很大的功勞:
有時你不需要在類別上顯性註記型別參數之型別,TypeScript 仍然可以藉由你填入的參數反向推論出泛用到的型別參數對應的型別值
筆者也是懶惰到想丟給讀者自己 Try Try 看,推論結果絕對都是跟 number
型別有關,筆者就貼變數被推論的結果。(如圖八)
圖八:就算你只有用 new C(...)
建構物件,TypeScript 可以照樣推論出 C<number>
型別
讀者試試看
試著將情形 3 的範例程式碼中的每個單元的推論結果驗證看看是不是跟情形 1 相符合?
重點 1. 泛用類別的推論行為
若
C
這個型別化名為類別宣告而成的,並且C
有一個型別參數T
(也就是型別化名C<T>
)。將
C<T>
註記到變數時,必須要填入T
代表的型別值,符合前一篇的重點 1 所講述的概念。但是建構物件時,建構子函式
new C(...)
不一定需要註記型別參數T
的值,就可以藉由型別系統間接推論出T
所代表的型別。然而,通常你會想要註記為
new C<T>(...)
的情形,除了是為了增加程式碼的可讀性外,註記行為可以輔助 TypeScript 的型別推論系統,直接闡明T
所代表之型別 —— 因此也可以選擇積極註記型別參數T
。
筆者還沒討論完泛用類別,因為我們還有類別的繼承。(汗水直流)
不過應該不會花太多篇幅講這個子類別繼承父類別的推論機制,請看類別的推論與註記篇章,裡面就有討論到大致上子類別與父類別的推論註記行為,相信泛用類別就只是多了泛用的概念外,也符合重點 1 所描述的特點。
筆者這邊要講的是一些泛用類別繼承上的宣告方式差異,這裡只會討論子類別繼承父類別,且父類別為泛用類別的狀況;如果反過來是子類別為泛用類別而父類別不是的話,討論起來根本沒意義,會跟前一節講到的普通泛用類別的推論註記機制差不多,只是多繼承一個非泛用的普通類別罷了。
首先,你能夠分辨出來這兩種格式差別在哪裡嗎?
首先,如果直接這樣宣告 D
類別,而且 T
型別不為某個型別化名的話,一定會出現錯誤。(如圖九)
圖九:它說,T
這個名稱找不到對應的型別
其實仔細看應該也會猜到 D
跟 E
差別在哪。
首先,D
本身沒有型別參數,因此它不是泛用類別,但是它繼承了 C
—— 不過 D
必須要確定你繼承的 C
對應的確切型別,這概念就很像是你要註記一個特定型別給一個特定變數 —— 因此筆者再次重申,前一節的重點 1 已經講過:如果某變數必須註記某個泛用型別,除非該泛型的型別參數有預設值,否則缺少型別參數對應的型別值就會出現錯誤警告。
頂多以 D
子類別為例,這一次 D
因為想要直接繼承某個類別,它也必須很確定它到底要繼承的確切型別是什麼 —— 也就是說,如果你選擇忽略類別 C
之 T
型別參數對應的型別值也會被 TypeScript 警告。(如圖十)
圖十:跟變數註記某個泛用型別的概念很像,差別在於這一次繼承的東西也必須是確切的型別而非為指定型別值的泛用類別狀態
因此你可能必須要改成:
class D extends C<number> {}
等等諸如此類的形式。
另外,E<T>
由於本身就是泛用類別的宣告,因此它有型別參數 T
,而且它還可以將它的型別參數代入到它要繼承的 C
類別的型別參數部分。也就是說,想要建構 E<number>
這種型別的物件,就等同於模擬 E
繼承 C<number>
的情形。
重點 2. 子類別繼承泛用類別的情形 Child Class Inherit Generic Class
分成兩種形式,若子類別為普通類別時,繼承到的泛用父類別必須確切指名該型別參數的確切型別值。
然而若子類別也是泛用類別時,則繼承到的泛用父類別除了可以指定特定的型別外,也可以填入子類別所宣告的型別參數建立型別上的連結。
讀者試試看
這應該算是筆者的整人時間,不過筆者倒是有在網路上的某個角落看到有人在討論以下這些案例會發生什麼樣的奇葩情形,筆者就不多做說明,留給讀者去玩玩看吧~
由於篇幅問題,筆者認為如果可以到出書的程度,再來認真補這些坑,儘管以上的程式碼看起來很愚蠢,但是可以考驗到底讀者對於預設型別(Default Type)跟型別限制(Type Constraint)的概念。
不過認真地想,筆者還真的不知道會不會用到以上的奇葩情形來為難自己,難道讀者會想要嗎?XD
但既然別人都問了這個問題,勢必可能在少數地方會用到,所以筆者也不能保證你不會遇到類似的狀況,因此還是泡出來給讀者
無聊或想不開時試試看。
筆者大致上講完了介面與類別結合泛型機制時大致上需要注意的方向,介面應該覺得還好,但主要是泛用類別的重點比較多。
並且藉由前一篇得出的結論 —— 也就是泛用型別可以間接推論程式碼未來動作時在型別推論方面的能力 —— 以此重點為基礎,闡述泛用類別也運用了類似的優勢,達到更完美的型別推論境界。
下一篇要講的是泛用類別與泛用介面結合的終極 Combo 第二彈~ 敬請期待啦~~~