iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

1
Modern Web

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

Day 45. 通用武裝・泛用類別與介面 X 終極組合第二彈 - Ultimate Combo of Generic Class & Interface

https://ithelp.ithome.com.tw/upload/images/20191011/20120614lx0a651BsV.png

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

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

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

這段寫作過程真是很神奇,不過筆者還是就貼一下在《機動藍圖》篇章探討過的介面與類別的結合篇章 —— 該篇重點在於探討類別 implements 介面種種的型別推論與註記的機制。

本篇則是多增加了泛型(Generics)的機制,但是就跟前一篇的調性差不多,也就是說 —— 筆者不會再度探討泛型的類別與介面結合時的推論與註記機制,因為這跟《機動藍圖》篇章討論的沒差多少。

而本篇重點在於強調泛型的宣告下類別與介面綁定時的規則與特點

以下正文開始

泛用類別與介面的終極組合 Ultimate Combo of Generic Class & Interface

泛用類別與介面的綁定

這一節的重點跟前一篇 —— 子類別繼承父類別的概念很像。首先,筆者先把前一節的 LinkedList<T> 泛用介面給讀者過目一下,因為這是本篇的主角介面。

https://ithelp.ithome.com.tw/upload/images/20191011/201206140HyK4S4T1i.png

另外,前一篇討論過普通類別或者是泛用類別繼承泛用的父類別的狀況;這一次也是討論相似的狀況,普通類別或泛用類別綁定泛用介面的狀況,如下。

https://ithelp.ithome.com.tw/upload/images/20191011/20120614Tloragv592.png

首先,這兩段程式碼一定都會被 TypeScript 叫來叫去,因為一但跟介面綁定就等同於簽下契約 —— 必須實踐介面所描述的功能。(參見介面與類別的終極組合篇章

筆者就把兩種不同的錯誤訊息丟出來給大家看看~(如圖一、二)

https://ithelp.ithome.com.tw/upload/images/20191011/2012061471TKB3UmcU.png
圖一:除了 MyLinkedList 沒有符合介面的要求外,重點在於它所要 implements 的介面被推論為 LinkedList<any>,這是因為型別參數 T 在全域裡並沒有代表任何型別結構,因此被視為 any

https://ithelp.ithome.com.tw/upload/images/20191011/20120614eit1TWEM5Z.png
圖二:由於 MyGenericLinkedList<T> 有特別宣告 T 這個型別參數,因此試著 implements 這個 LinkedList 泛用介面時,可以填入該參數 T 進行型別連動的動作

思路跟昨天的文章差不多,因此筆者就不解釋直接丟昨天那一套重點改成今天這一套重點,相信讀者也想趕快目睹類別跟介面結合擦出火花的優勢而不是看筆者囉哩八唆。

本篇唯一重點.類別綁定泛用介面的情形 Class Implementing Generic Interface(s)

分成兩種形式,若類別為普通類別時,實踐到的泛用介面必須確切指名該介面之型別參數的確切型別值

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

貼心小提示

沒辦法,正式的東西不囉唆ㄧ點又顯得有些隨便,讀者在開發時不需要像作者ㄧ樣斤斤計較;除非像是遇到傳遞知識的場合,肯定要確保知識的精確性到某個程度,錯了就得改 —— 所以筆者還是得啟動廢話一點的模式。

但平常開發時,筆者有時會不經意寫出爛程式碼也會被別人 Fk 來 Fk 去,這是正常的。

每個人都 Fk 來 Fk 去是正常的 —— 古時候的蘇格拉底 Fk 別人,別人也 Fk 回去的過程據說也是挺猛烈的(參見本系列踢翻 Object Composition 迷思篇章,筆者也是寫得挺挫的,深怕被 Fk 來 Fk 去的,不過秉持本系列要成為 ACE 的理念還是得跨出這一步)

另外也可參見 WTF per Minute - An Actual Measurement for Code Quaility

類別與介面進行綁定的簡易實作 —— 鏈結串列

根據前一篇已經宣告過後的 LinkedList 這個泛用介面,我們來試試看用類別去實作該介面吧。

以下是完整的程式碼實踐:

https://ithelp.ithome.com.tw/upload/images/20191011/20120614CkrJEU5VqY.png

以上的程式碼筆者會開始深入分析。

GenericLinkedListNode 泛用類別的型別參數 TLinkedListNode 進行綁定的動作,建構子非常簡單,就是填入該節點必需要儲存的值 value。而 next 成員則是代表它所鏈結的下一個節點 LinkedListNode

GenericLinkedList 則複雜許多,但結構事實上很簡單,不過這邊要講很多細節:

1. head 成員變數

head 成員變數存的是 LinkedListNode,裡面的型別參數與 GenericLinkedList 的型別參數 T 進行綁定,也就是說 —— GenericLinkedList<number> 存的 LinkedListNode 必須也要為 LinkedListNode<number> 型別

2. length 成員方法

length 成員方法則是計算整個鏈結串列的長度(有些鏈結串列在計算長度的命名是用 size)。

然而,以下這一段:

https://ithelp.ithome.com.tw/upload/images/20191011/201206146PQlhdRonh.png

你會看到筆者 —— 儘管在 this.head 經過一次的型別確認(Type Guard)下 —— 確認不為 null,但仍然對 currentNode 作的註記是 LinkedListNode<T> | null,也就是與 null 的理由是因為後面使用 while 迴圈會不停更新 currentNode 的值,還是有潛在 null 發生的可能性。

另外,讀者可能認為 while 迴圈內部的 currentNode = currentNode.next 這一行可能會出現問題,因為在呼叫 next 之前,currentNode 被筆者強行註記為 LinkedListNode<T> | null,因此有成為 null 的可能性,而 null 不可能有 next 屬性。

然而,不需要積極註記為 currentNode = (currentNode as LinkedListNode<T>).next 的原因是因為 while 迴圈的判斷過程就已經確認 currentNode !== null,因此可以確定 while 迴圈內部的 currentNode 100% 絕對會是 LinkedListNode<T> 的型別 —— 這就是之前在講 Type Guard 部分:根據判斷敘述架構下進行型別限縮的案例之一。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191011/201206149YYwLQqWFD.png
圖三:currentNode 變成 LinkedListNode<T> 而非 LinkedListNode<T> | null,因為 null 狀況被 while 的判斷部分給濾掉了

另外,因為這一行 currentNode = currentNode.next,被指派的部分就因為出現指派 currentNode.next 而推論結果退化為 LinkedListNode<T> | null。(如圖四)

https://ithelp.ithome.com.tw/upload/images/20191011/20120614erKjYGqLZx.png
圖四:因為又再度被指派為型別 LinkedListNode<T> | null 的值,型別推論退化為 LinkedListNode<T> | null

3. at 成員方法

這裡就可以比對剛剛的 length 方法的實踐。

https://ithelp.ithome.com.tw/upload/images/20191011/20120614JhbtMHD3z9.png

首先,實踐鏈結串列的邏輯部分應該沒問題,at 方法主要是要找尋某節點在鏈結串列內的位置(index),而 index 如果被超出去的話就會丟出類似 Out of bound 之類的錯誤。

另外,筆者這一次在 while 迴圈積極註記了 ... as LinkedListNode<T> 型別:

https://ithelp.ithome.com.tw/upload/images/20191011/20120614nOQJNxJbxB.png

讀者可能會問:

“這樣不就跟筆者之前在第三篇章某部分有談到的型別壟斷(Type Monopolization)的情型很像嗎?你偏偏選擇將一個可能是 LinkedListNode<T> | null 的型別強行註記為 LinkedListNode<T>,這不就是打自己臉嗎?”

筆者之前提到類似這樣的概念:“假設某變數可能型別為 A | B,但有 100% 信心說,目前所代表的型別為某 A 型別,你才可以選擇註記”。

此時的狀況是:

筆者確認 index 這個值在鏈結串列的 length 範圍內,所以 currentNode 不管如何 100% 絕對會是 LinkedListNode<T> 這個型別

畢竟程式是人寫的,我們也會有自個兒一套的判斷標準 —— 不太可能會 100% 交給 TypeScript 去幫我們推論型別的樣貌,這也是為何我們有時候必須要積極註記來輔助 TypeScript 編譯器幫助維護程式碼的品質。(筆者知道這很廢話

4. insert 成員方法

https://ithelp.ithome.com.tw/upload/images/20191011/20120614MeV3ksr692.png

insert 方法運作的邏輯過程是這樣:

  1. 假設鏈結串鏈本身是空的,你也只能指定 index0 的狀況下插入新的值
  2. 假設鏈結串列不為空,而且 indexlength() 的範圍內,在該 index 上的節點拔出來後變成舊的節點,取代為新的節點,此時新的節點的 next 必須鏈結舊的節點,而 index - 1 位置的節點的 next 也必須取代為新的節點(示意圖如圖五)

https://ithelp.ithome.com.tw/upload/images/20191011/20120614E3Iwx6LgPv.png
圖五:鏈結串列插入一個新的節點的過程

不過這邊的判斷式過程複雜很多,因為你還要確認:

  1. 是不是插入第一個位置,是的話就得取代 this.head 的值,然後新節點就必須連結舊的 head 節點
  2. 是不是插入最後一個位置,是的話你就不需要鏈結後面的節點,因為本來插入到最後一個位置,而最後一個位置再更後面就是空的

另外,你會看到筆者依然有 ... as LinkedListNode<T> 這個顯性註記代表筆者 100% 確定該變數或值一定是 LinkedListNode<T> 這個型別,跟實踐 at 成員方法的概念也差不多。

讀者試試看

可以到 Maxwell-Alexius/Iron-Man-Competitionlinked-list 部分程式碼下載下來,並且試試看實踐拔除節點 remove 的功能。

本篇主旨在希望 —— 讀者要能夠自行判斷:

  1. 什麼時機不需要積極註記?什麼時機則會需要?能夠分辨哪些行為並不是型別壟斷的動作呢?
  2. 另外,如果是超出鏈結串列以外的值需要丟出 Out of bound 錯誤。
  3. 其實筆者故意漏掉了檢查 index 小於 '0' 的情形,亦或者是 index 值為 Floating Number,讀者可以想想看要怎麼去 Handle 這一類的狀況。

驗證型別泛用類別與介面的結合之推論與註記的機制

接下來就是簡單測試剛剛的程式碼實踐結果,首先筆者刻意多宣告 getInfo 這個成員方法,目的旨在檢視鏈結串列的內部內容。

貼心小提示

事實上筆者先寫出實踐出來的結果再去測試,跟提前先寫測試再去實踐功能比起來,筆者比較 Prefer 後者。由於本篇在談論的東西跟測試無關,所以不會深究太多這一類的東西。(當然筆者可以先講測試,不過考慮過後還是以語法教學為主不然範圍莫名其妙又被擴大,但又感覺這是一個好想寫寫看的坑啊

請參見 TDD(Test Driven Development)相關資源。

以下就是簡單的測試程式碼。(以下程式碼測試結果如圖六)

https://ithelp.ithome.com.tw/upload/images/20191011/20120614zL5CbefiAp.png

https://ithelp.ithome.com.tw/upload/images/20191011/20120614GM7IulTazO.png
圖六:鏈結串列的內容

當然,如果你也可以使用 at 方法來檢視裡面的內容。(以下程式碼測試結果如圖七)

https://ithelp.ithome.com.tw/upload/images/20191011/20120614NA4JpPiNvW.png

https://ithelp.ithome.com.tw/upload/images/20191011/20120614gKyEbhUohY.png
圖七:index 在 0 ~ 3 時會正常動作,但是超出 3 時就會跳出 Out of bound... 訊息

以上就是簡單的鏈結串列的實作結果過程,不過筆者強調的點在於 —— 判斷註記型別的時機,若型別部分你有 100% 信心認為是某特定型別,你應該選擇型別積極註記的動作,不需要擔心會不會造成型別壟斷的行為。

另外,仔細看會發現因為我們特地建構 GenericLinkedList<number> —— 指定型別參數為 number 型別 —— 也就是說你在使用 at 成員方法或 insert 成員方法,你都會看到原本型別參數 T 的提示都會被自動取代為 number 型別。(如圖八、九)

https://ithelp.ithome.com.tw/upload/images/20191011/201206147AaVZLHSOM.png
圖八:insert 方法內部的提示內容之 value 型別被自動提示為,需要填入 number 型別之值

https://ithelp.ithome.com.tw/upload/images/20191011/20120614pjaNgeTVlS.png
圖九:at 方法則是在函式的輸出部分自動推論為 LinkedListNode<number> | null

所以就算你可能在很遙遠的地方(如:其他檔案)宣告 lLinkedListNode<number> 型別,當要載入該 l 變數的值時,你可以藉由 TypeScript 的提示 —— 填入正確的型別值,而不需要追本溯源回去看 l 的型別到底是什麼

讀者試試看

儘管前面的讀者試試看單元,寬鬆一點來說可以跳過,但筆者強烈要求,這邊的試試看一定要親手驗證,而且就留給讀者去做。

由於鏈結串列 GenericLinkedList 的泛用類別部分,它的建構子函式沒有成員變數跟 GenericLinkedList 所宣告的型別參數 T 進行連結。因此,筆者想要考考讀者:

  1. 以下的程式碼會不會被 TypeScript 警告?

https://ithelp.ithome.com.tw/upload/images/20191011/20120614IwNIQ5mCJW.png

  1. 如果可以通過的話,那麼 unspecifiedTypeParamLinkedList 的推論結果為何?
  2. 如果可以使用 unspecifiedTypeParamLinkedList,設型別 Tany 為宣告過的若干型別化名,其中,specifiedTypeParamLinkedList 的宣告方式為:

https://ithelp.ithome.com.tw/upload/images/20191011/20120614Q6FEeOl1V1.png

請問 specifiedunspecified 版本的 TypeParamLinkedList 使用上各自差別會在哪?需要注意哪些事項?

這一題筆者認為有難度,但個人覺得不給讀者想想看也不好,姑且做題過程中給幾個提示,讀者可以選擇要不要看下面 XD:

  1. 前一篇有講過變數註記泛用型別的情形,與泛用類別的建構子建構物件的情形,兩者使用下有關鍵性的差別,第一題就算不經手驗證也可以秒答出結果。
  2. 可以參見本系列的 unknown 型別篇章

再三強調,《前線維護》的篇章的重要程度 —— 非・常・重・要。

小結

其實本篇都是在講實踐的部分,而非型別推論與註記的機制,但都是在練習泛用類別與介面的使用與開發過程中可能遇到的情境。

下一篇筆者想要提一下本系列中即將出場的第六個 OOP 的設計模式 —— Iterator 模式,中文名叫迭代器模式 —— 作為《通用武裝》篇章的第一個應用,順便讓讀者更熟悉 OOP 的設計外,也熟悉 TypeScript 類別與介面應用。

反正筆者的目標就是要逼讀者看過一遍又一遍的介面與類別的終極組合的應用,而這個就只能靠實作經驗來應證給讀者看

敬請期待~


上一篇
Day 44. 通用武裝・介面與類別 X 泛型註記機制 - TypeScript Generic Class & Interface
下一篇
Day 46. 通用武裝・迭代器模式 X 泛用迭代器 - Iterator Pattern Using TypeScript
系列文
讓 TypeScript 成為你全端開發的 ACE!51

尚未有邦友留言

立即登入留言