閱讀本篇文章前,仔細想想看
- 泛用型別化名的如何進行宣告?
- 泛用化名註記在變數時的注意事項為何?
- 泛用函式的特點為何?
如果還不清楚可以看一下前一篇文章喔~
這段寫作過程真是很神奇,不過筆者還是就貼一下在《機動藍圖》篇章探討過的介面與類別的結合篇章 —— 該篇重點在於探討類別 implements
介面種種的型別推論與註記的機制。
本篇則是多增加了泛型(Generics)的機制,但是就跟前一篇的調性差不多,也就是說 —— 筆者不會再度探討泛型的類別與介面結合時的推論與註記機制,因為這跟《機動藍圖》篇章討論的沒差多少。
而本篇重點在於強調泛型的宣告下類別與介面綁定時的規則與特點。
以下正文開始!
這一節的重點跟前一篇 —— 子類別繼承父類別的概念很像。首先,筆者先把前一節的 LinkedList<T>
泛用介面給讀者過目一下,因為這是本篇的主角介面。
另外,前一篇討論過普通類別或者是泛用類別繼承泛用的父類別的狀況;這一次也是討論相似的狀況,普通類別或泛用類別綁定泛用介面的狀況,如下。
首先,這兩段程式碼一定都會被 TypeScript 叫來叫去,因為一但跟介面綁定就等同於簽下契約 —— 必須實踐介面所描述的功能。(參見介面與類別的終極組合篇章)
筆者就把兩種不同的錯誤訊息丟出來給大家看看~(如圖一、二)
圖一:除了 MyLinkedList
沒有符合介面的要求外,重點在於它所要 implements
的介面被推論為 LinkedList<any>
,這是因為型別參數 T
在全域裡並沒有代表任何型別結構,因此被視為 any
圖二:由於 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
這個泛用介面,我們來試試看用類別去實作該介面吧。
以下是完整的程式碼實踐:
以上的程式碼筆者會開始深入分析。
GenericLinkedListNode
泛用類別的型別參數 T
對 LinkedListNode
進行綁定的動作,建構子非常簡單,就是填入該節點必需要儲存的值 value
。而 next
成員則是代表它所鏈結的下一個節點 LinkedListNode
。
GenericLinkedList
則複雜許多,但結構事實上很簡單,不過這邊要講很多細節:
head
成員變數head
成員變數存的是 LinkedListNode
,裡面的型別參數與 GenericLinkedList
的型別參數 T
進行綁定,也就是說 —— GenericLinkedList<number>
存的 LinkedListNode
必須也要為 LinkedListNode<number>
型別
length
成員方法length
成員方法則是計算整個鏈結串列的長度(有些鏈結串列在計算長度的命名是用 size
)。
然而,以下這一段:
你會看到筆者 —— 儘管在 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 部分:根據判斷敘述架構下進行型別限縮的案例之一。(如圖三)
圖三:currentNode
變成 LinkedListNode<T>
而非 LinkedListNode<T> | null
,因為 null
狀況被 while
的判斷部分給濾掉了
另外,因為這一行 currentNode = currentNode.next
,被指派的部分就因為出現指派 currentNode.next
而推論結果退化為 LinkedListNode<T> | null
。(如圖四)
圖四:因為又再度被指派為型別 LinkedListNode<T> | null
的值,型別推論退化為 LinkedListNode<T> | null
at
成員方法這裡就可以比對剛剛的 length
方法的實踐。
首先,實踐鏈結串列的邏輯部分應該沒問題,at
方法主要是要找尋某節點在鏈結串列內的位置(index),而 index
如果被超出去的話就會丟出類似 Out of bound
之類的錯誤。
另外,筆者這一次在 while
迴圈積極註記了 ... as LinkedListNode<T>
型別:
讀者可能會問:
“這樣不就跟筆者之前在第三篇章某部分有談到的型別壟斷(Type Monopolization)的情型很像嗎?你偏偏選擇將一個可能是
LinkedListNode<T> | null
的型別強行註記為LinkedListNode<T>
,這不就是打自己臉嗎?”
筆者之前提到類似這樣的概念:“假設某變數可能型別為 A | B
,但有 100% 信心說,目前所代表的型別為某 A
型別,你才可以選擇註記”。
此時的狀況是:
筆者確認
index
這個值在鏈結串列的length
範圍內,所以currentNode
不管如何 100% 絕對會是LinkedListNode<T>
這個型別
畢竟程式是人寫的,我們也會有自個兒一套的判斷標準 —— 不太可能會 100% 交給 TypeScript 去幫我們推論型別的樣貌,這也是為何我們有時候必須要積極註記來輔助 TypeScript 編譯器幫助維護程式碼的品質。(筆者知道這很廢話)
insert
成員方法insert
方法運作的邏輯過程是這樣:
index
為 0
的狀況下插入新的值index
在 length()
的範圍內,在該 index
上的節點拔出來後變成舊的節點,取代為新的節點,此時新的節點的 next
必須鏈結舊的節點,而 index - 1
位置的節點的 next
也必須取代為新的節點(示意圖如圖五)
圖五:鏈結串列插入一個新的節點的過程
不過這邊的判斷式過程複雜很多,因為你還要確認:
this.head
的值,然後新節點就必須連結舊的 head
節點另外,你會看到筆者依然有 ... as LinkedListNode<T>
這個顯性註記代表筆者 100% 確定該變數或值一定是 LinkedListNode<T>
這個型別,跟實踐 at
成員方法的概念也差不多。
讀者試試看
可以到
Maxwell-Alexius/Iron-Man-Competition
將linked-list
部分程式碼下載下來,並且試試看實踐拔除節點remove
的功能。本篇主旨在希望 —— 讀者要能夠自行判斷:
- 什麼時機不需要積極註記?什麼時機則會需要?能夠分辨哪些行為並不是型別壟斷的動作呢?
- 另外,如果是超出鏈結串列以外的值需要丟出
Out of bound
錯誤。- 其實筆者故意漏掉了檢查
index
小於 '0' 的情形,亦或者是index
值為 Floating Number,讀者可以想想看要怎麼去 Handle 這一類的狀況。
接下來就是簡單測試剛剛的程式碼實踐結果,首先筆者刻意多宣告 getInfo
這個成員方法,目的旨在檢視鏈結串列的內部內容。
貼心小提示
事實上筆者先寫出實踐出來的結果再去測試,跟提前先寫測試再去實踐功能比起來,筆者比較 Prefer 後者。由於本篇在談論的東西跟測試無關,所以不會深究太多這一類的東西。(當然筆者可以先講測試,不過考慮過後還是以語法教學為主
不然範圍莫名其妙又被擴大,但又感覺這是一個好想寫寫看的坑啊)請參見 TDD(Test Driven Development)相關資源。
以下就是簡單的測試程式碼。(以下程式碼測試結果如圖六)
圖六:鏈結串列的內容
當然,如果你也可以使用 at
方法來檢視裡面的內容。(以下程式碼測試結果如圖七)
圖七:index
在 0 ~ 3 時會正常動作,但是超出 3 時就會跳出 Out of bound...
訊息
以上就是簡單的鏈結串列的實作結果過程,不過筆者強調的點在於 —— 判斷註記型別的時機,若型別部分你有 100% 信心認為是某特定型別,你應該選擇型別積極註記的動作,不需要擔心會不會造成型別壟斷的行為。
另外,仔細看會發現因為我們特地建構 GenericLinkedList<number>
—— 指定型別參數為 number
型別 —— 也就是說你在使用 at
成員方法或 insert
成員方法,你都會看到原本型別參數 T
的提示都會被自動取代為 number
型別。(如圖八、九)
圖八:insert
方法內部的提示內容之 value
型別被自動提示為,需要填入 number
型別之值
圖九:at
方法則是在函式的輸出部分自動推論為 LinkedListNode<number> | null
所以就算你可能在很遙遠的地方(如:其他檔案)宣告 l
為 LinkedListNode<number>
型別,當要載入該 l
變數的值時,你可以藉由 TypeScript 的提示 —— 填入正確的型別值,而不需要追本溯源回去看 l
的型別到底是什麼。
讀者試試看
儘管前面的讀者試試看單元,寬鬆一點來說可以跳過,但筆者強烈要求,這邊的試試看一定要親手驗證,而且就留給讀者去做。
由於鏈結串列
GenericLinkedList
的泛用類別部分,它的建構子函式沒有成員變數跟GenericLinkedList
所宣告的型別參數T
進行連結。因此,筆者想要考考讀者:
- 以下的程式碼會不會被 TypeScript 警告?
- 如果可以通過的話,那麼
unspecifiedTypeParamLinkedList
的推論結果為何?- 如果可以使用
unspecifiedTypeParamLinkedList
,設型別Tany
為宣告過的若干型別化名,其中,specifiedTypeParamLinkedList
的宣告方式為:請問
specified
與unspecified
版本的TypeParamLinkedList
使用上各自差別會在哪?需要注意哪些事項?這一題筆者認為有難度,但個人覺得不給讀者想想看也不好,姑且做題過程中給幾個提示,讀者可以選擇要不要看下面 XD:
- 前一篇有講過變數註記泛用型別的情形,與泛用類別的建構子建構物件的情形,兩者使用下有關鍵性的差別,第一題就算不經手驗證也可以秒答出結果。
- 可以參見本系列的
unknown
型別篇章。再三強調,《前線維護》的篇章的重要程度 —— 非・常・重・要。
其實本篇都是在講實踐的部分,而非型別推論與註記的機制,但都是在練習泛用類別與介面的使用與開發過程中可能遇到的情境。
下一篇筆者想要提一下本系列中即將出場的第六個 OOP 的設計模式 —— Iterator 模式,中文名叫迭代器模式 —— 作為《通用武裝》篇章的第一個應用,順便讓讀者更熟悉 OOP 的設計外,也熟悉 TypeScript 類別與介面應用。
反正筆者的目標就是要逼讀者看過一遍又一遍的介面與類別的終極組合的應用,而這個就只能靠實作經驗來應證給讀者看。
敬請期待~