iT邦幫忙

第 11 屆 iThome 鐵人賽

1
Modern Web

讓 TypeScript 成為你全端開發的 ACE!系列 第 44

Day 44. 通用武裝・介面與類別 X 泛型註記機制 - TypeScript Generic Class & Interface

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20191010/20120614lfJ5V0I4wA.png

閱讀本篇文章前,仔細想想看

  1. 泛用型別化名的如何進行宣告?
  2. 泛用化名註記在變數時的注意事項為何?
  3. 泛用函式的特點為何?

如果還不清楚可以看一下前一篇文章喔~

這一次在第四篇章又新增了泛型的機制,想當然,筆者還是得點出泛用介面與類別的特點。到底會擦出什麼樣的火花呢?

以下正文開始

泛用介面與泛用類別的型別註記機制

泛用介面 Generic Interface

筆者為了好探討介面與類別結合,今天就以鏈結串列(Linked List)這個資料結構的實踐舉例,以下預設讀者已認識此資料結構,繼續講下去。

首先,普通情況下,我們可以訂立 LinkedListLinkedListNode 這兩種介面:

https://ithelp.ithome.com.tw/upload/images/20191010/20120614QZEtBvmJPv.png

以上的 LinkedList 介面有六個規格:

  • head 代表鏈結串列的首個元素 LinkedListNode,由於鏈結串列可為空的狀態,因此不排除 headnull 型別的可能性
  • length 是一個方法,輸出的是鏈結串列的長度(當然也可以使用普通的 number 型別而不採用 (): number 函式型別,但你可能必須要監控好 insertremove 鏈結串列的值時,更新 length 的大小)
  • at 是一個方法,輸入為 index 代表鏈結串列的位置,但是輸出既可以為 LinkedListNode 外也可以為 null
  • insert 代表將某任意值 value(型別為 any)插入進鏈結串列,位置由 index 指定
  • remove 則是根據 index 指名的位置移除連結串列裡的元素

而代表鏈結串列的元素是 LinkedListNode 介面,裡面的結構很單純:

  • value 代表該元素所存的值
  • next 則是取得下一個鏈結串列的元素,但結果也可以為 null 代表該元素可能是鏈結串列裡的最後一個元素

不過想也知道,這個連結串列可以存取的值是任意型別的值 —— value 屬性對應到的是 any 型別,因此我們可以將其改成泛用介面的模式,使得彈性增大:

https://ithelp.ithome.com.tw/upload/images/20191010/20120614tsw8rPogxP.png

另外,泛用參數的命名是什麼其實不重要,所以你取 TUV 甚至是口語化的名稱都無所謂,重點是看得懂就好。

從以上的程式碼得知,譬如使用 LinkedList<number>U 被取代為 number)代表該鏈結串列存的元素必須符合 LinkedListNode<number> 這個介面下的規格;也就是說 LinkedListNode<number> 存的 value 必須為 number 型別。

泛用介面的宣告其實很簡單,就是這樣而已。

另外,有些讀者可能會疑惑:

為何不討論直接將介面的型別化名註記在變數上的案例?

筆者一開始有想要多做說明,不過後來想想,筆者認為這樣的討論結果不如讓讀者參考介面的推論篇章還比較快,差別就是多了泛用的機制,但是型別參數被顯性註記過後,跟普通型別差不了多少。

此外,讀者如果會了本系列自立的物件完整性理論,延伸推論出泛用介面的註記機制也是可以的。

通常泛用的介面會和類別結合在一起使用,因此筆者認為討論泛用類別的型別推論與註記重要性比起泛用介面還要大。

泛用類別 Generic Class

泛用類別部分,筆者快速帶過跟類別相關的功能。

以下就舉一些很蠢又很簡單的例子,為了展示泛用類別的機制。譬如說我們有 C 類別:

https://ithelp.ithome.com.tw/upload/images/20191010/20120614r79vglxhPD.png

貼心小提示

讀者若是跳到本篇章然後不曉得成員變數、成員方法(Member Variables/Methods)等類別相關的東西,請記得參見《機動藍圖》篇章系列,筆者已經懶到不想貼哪一篇文章連結了QQ。

以上的 C 類別有:

  • 名為 memberProp 的成員變數,對應型別為 C 所宣告出的型別參數 T
  • memberFunc 則是輸出 memberProp 的值
  • 存取方法 value,分別覆寫或者輸出 memberProp

還記得前一篇講過的 —— 泛用的機制可以輔助 TypeScript 型別推論的功能,使得開發上能夠更靈活。

以下繼續展示泛用類別,或者乾脆說泛用機制超好用超變態的地方,以下筆者分幾個案例討論。

情形 1. 不註記變數,建構 C 物件時填入型別參數

https://ithelp.ithome.com.tw/upload/images/20191010/20120614DnAw0Qc69H.png

以上的程式碼,筆者不對變數 instanceOfC1 註記任何型別,並且建構物件時使用 new C<number>,代表 T 這個型別參數備取代為 number 型別。(圖)

https://ithelp.ithome.com.tw/upload/images/20191010/20120614kB8XYc8mog.png
圖一:單純建立 C<number>instanceOfC1 會被自動推論為 C<number>

https://ithelp.ithome.com.tw/upload/images/20191010/20120614C38duLV7eL.png
圖二:由於成員變數與型別參數 T 進行綁定,理應出現的推論結果為 number,結果確實也是 number

https://ithelp.ithome.com.tw/upload/images/20191010/20120614ekqiPM0bjk.png
圖三:雖然成員方法沒有被顯性註記輸出的型別,但根據函式型別篇章,TypeScript 聰明地根據輸出判斷推論結果為 number 型別,因為輸出為成員變數 memberProp,而該成員被綁定 number 型別

https://ithelp.ithome.com.tw/upload/images/20191010/20120614ZoUGD5btTj.png
圖四:呼叫 Getter 方法時,因為輸出為 memberProp,然後該成員綁定 number 型別,因此推論也是 number 型別

https://ithelp.ithome.com.tw/upload/images/20191010/20120614luLF4C7ouR.png
圖五:呼叫 Setter 方法時,因為該方法指定輸入的值 input 為型別參數 T,而 T 在本案例被綁定為 number 型別,因此可以被數字代入

另外,根據類別存取方法篇章,Setter 方法接收錯誤型別的值就會發出警訊,而本案例被綁定的型別為 number,代表如果代入非 number 型別的值就會輸出警訊唷~

https://ithelp.ithome.com.tw/upload/images/20191010/20120614rYYx9Zwl1V.png
圖六:被 TypeScript 打臉的感覺如何~?(請筆者不要唱衰讀者

情形 2. 註記型別在變數上,不註記在類別建構子旁的型別參數裡

首先,筆者在講這個案例前必須補充,根據前一篇討論到的某個重點:

除非型別參數有預設值(又可稱作預設型別參數 Default Type Parameter),否則本身為泛用形式的型別化名,少掉任何一個型別參數的值就會出錯

也就是說,你如果選擇這樣註記變數,一定會出錯。(如圖七)

https://ithelp.ithome.com.tw/upload/images/20191010/20120614NpYuHeJV65.png
圖七:就算筆者後面建構物件時亂填 Non-sense 的東西,TypeScript 讀到變數的註記時 —— 遇到泛用型別化名,少了任何一個型別參數就會跟你槓上(聽起來蠻有義氣的

所以不可能會有變數註記泛用型別卻不填上型別參數的狀況 —— 變數的型別註記要的是確切的型別,而非泛用的形式。

一但泛用的型別形式確立了內部型別參數的值,它就會退化為普通的型別

若是該型別參數有預設型別,你才可以省掉指定型別參數的部分

回過頭來,我們繼續探討情形 2,以下是測試的範例程式碼。

https://ithelp.ithome.com.tw/upload/images/20191010/20120614T3p30SzEjo.png

事實上,不是筆者偷懶,但是筆者真的認為情形 2 的推論結果跟情形 1 符合(事實上筆者親手測過 XDDD),所以打算讓讀者或者是推卸給讀者自行試試看,不然要張貼情形 2 推論每個單元的測試結果 — 一貼就六張圖了,佔太多版面,讀者應該也會覺得筆者廢話很多。(事實上筆者在本系列的廢話也不是一次兩次的事情了

所以呢~ 請:

讀者試試看

試著將情形 2 的範例程式碼中的每個單元的推論結果驗證看看是不是跟情形 1 相符合?

另外,筆者雞婆的一定要測給讀者看 —— 泛用類別的型別推論的每種案例的主要原因是:以下描述的情形 3 的行為是很重要的!

情形 3. 不註記在變數上,但指派建構之物件時,你甚至不需要填入型別參數在建構子函式上

其實對於使用 TypeScript 已成習慣的人應該很少察覺到這一回事,或者覺得應該理所當然;然而,這個機制可是在型別系統的推論(Type Inference)上,佔了很大的功勞:

有時你不需要在類別上顯性註記型別參數之型別,TypeScript 仍然可以藉由你填入的參數反向推論出泛用到的型別參數對應的型別值

https://ithelp.ithome.com.tw/upload/images/20191010/20120614sMStwhMU5D.png

筆者也是懶惰到想丟給讀者自己 Try Try 看,推論結果絕對都是跟 number 型別有關,筆者就貼變數被推論的結果。(如圖八)

https://ithelp.ithome.com.tw/upload/images/20191010/2012061451btuN8Mos.png
圖八:就算你只有用 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

泛用類別的繼承 Generic Class Inheritance

筆者還沒討論完泛用類別,因為我們還有類別的繼承。(汗水直流)

不過應該不會花太多篇幅講這個子類別繼承父類別的推論機制,請看類別的推論與註記篇章,裡面就有討論到大致上子類別與父類別的推論註記行為,相信泛用類別就只是多了泛用的概念外,也符合重點 1 所描述的特點。

筆者這邊要講的是一些泛用類別繼承上的宣告方式差異,這裡只會討論子類別繼承父類別,且父類別為泛用類別的狀況;如果反過來是子類別為泛用類別而父類別不是的話,討論起來根本沒意義,會跟前一節講到的普通泛用類別的推論註記機制差不多,只是多繼承一個非泛用的普通類別罷了。

首先,你能夠分辨出來這兩種格式差別在哪裡嗎?

https://ithelp.ithome.com.tw/upload/images/20191010/20120614r8Q66rsHm4.png

首先,如果直接這樣宣告 D 類別,而且 T 型別不為某個型別化名的話,一定會出現錯誤。(如圖九)

https://ithelp.ithome.com.tw/upload/images/20191010/20120614L4FaRrV7yu.png
圖九:它說,T 這個名稱找不到對應的型別

其實仔細看應該也會猜到 DE 差別在哪。

首先,D 本身沒有型別參數,因此它不是泛用類別,但是它繼承了 C —— 不過 D 必須要確定你繼承的 C 對應的確切型別,這概念就很像是你要註記一個特定型別給一個特定變數 —— 因此筆者再次重申,前一節的重點 1 已經講過:如果某變數必須註記某個泛用型別,除非該泛型的型別參數有預設值,否則缺少型別參數對應的型別值就會出現錯誤警告。

頂多以 D 子類別為例,這一次 D 因為想要直接繼承某個類別,它也必須很確定它到底要繼承的確切型別是什麼 —— 也就是說,如果你選擇忽略類別 CT 型別參數對應的型別值也會被 TypeScript 警告。(如圖十)

https://ithelp.ithome.com.tw/upload/images/20191010/20120614HXq9cNOvvK.png
圖十:跟變數註記某個泛用型別的概念很像,差別在於這一次繼承的東西也必須是確切的型別而非為指定型別值的泛用類別狀態

因此你可能必須要改成:

class D extends C<number> {}

等等諸如此類的形式。

另外,E<T> 由於本身就是泛用類別的宣告,因此它有型別參數 T,而且它還可以將它的型別參數代入到它要繼承的 C 類別的型別參數部分。也就是說,想要建構 E<number> 這種型別的物件,就等同於模擬 E 繼承 C<number> 的情形。

重點 2. 子類別繼承泛用類別的情形 Child Class Inherit Generic Class

分成兩種形式,若子類別為普通類別時,繼承到的泛用父類別必須確切指名該型別參數的確切型別值

然而若子類別也是泛用類別時,則繼承到的泛用父類別除了可以指定特定的型別外,也可以填入子類別所宣告的型別參數建立型別上的連結

讀者試試看

這應該算是筆者的整人時間,不過筆者倒是有在網路上的某個角落看到有人在討論以下這些案例會發生什麼樣的奇葩情形,筆者就不多做說明,留給讀者去玩玩看吧~

https://ithelp.ithome.com.tw/upload/images/20191010/20120614tZTs9KyDgj.png

由於篇幅問題,筆者認為如果可以到出書的程度,再來認真補這些坑,儘管以上的程式碼看起來很愚蠢,但是可以考驗到底讀者對於預設型別(Default Type)跟型別限制(Type Constraint)的概念。

不過認真地想,筆者還真的不知道會不會用到以上的奇葩情形來為難自己,難道讀者會想要嗎?XD

但既然別人都問了這個問題,勢必可能在少數地方會用到,所以筆者也不能保證你不會遇到類似的狀況,因此還是泡出來給讀者無聊或想不開時試試看。

小結

筆者大致上講完了介面與類別結合泛型機制時大致上需要注意的方向,介面應該覺得還好,但主要是泛用類別的重點比較多。

並且藉由前一篇得出的結論 —— 也就是泛用型別可以間接推論程式碼未來動作時在型別推論方面的能力 —— 以此重點為基礎,闡述泛用類別也運用了類似的優勢,達到更完美的型別推論境界。

下一篇要講的是泛用類別與泛用介面結合的終極 Combo 第二彈~ 敬請期待啦~~~


上一篇
Day 43. 通用武裝・泛型註記 X 推論未來 - TypeScript Generic Declaration & Annotation
下一篇
Day 45. 通用武裝・泛用類別與介面 X 終極組合第二彈 - Ultimate Combo of Generic Class & Interface
系列文
讓 TypeScript 成為你全端開發的 ACE!51
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言