這個程式碼研究室是「程式設計人員的 Kotlin 新手上路課程」的一部分。如果您按部就班完成程式碼研究室,就能充分體驗到本課程的價值。視您的知識多寡而定,您或許能略過某些部分。本課程適用於熟悉物件導向語言,且想學習 Kotlin 的程式設計師。
簡介
在本程式碼研究室中,您將瞭解泛型類別、函式和方法,以及這些項目在 Kotlin 中的運作方式。
本課程的設計目標是協助您累積知識,但各單元之間彼此半獨立,因此您可以略過熟悉的部分,不必建構單一範例應用程式。為將這些範例連結在一起,許多範例都使用水族館主題。如要查看完整的魚缸故事,請參閱 Udacity 的程式設計人員 Kotlin 新手上路課程。
必備知識
- Kotlin 函式、類別和方法的語法
- 如何在 IntelliJ IDEA 中建立新類別並執行程式
課程內容
- 如何使用泛型類別、方法和函式
學習內容
- 建立泛型類別並新增限制
- 建立
in和out類型 - 建立泛型函式、方法和擴充功能函式
泛型簡介
Kotlin 與許多程式設計語言一樣,都有泛型。泛型可讓您製作泛型類別,進而大幅提升類別的彈性。
假設您要實作 MyList 類別,其中包含項目清單。如果沒有泛型,您需要為每個型別實作新版 MyList:一個用於 Double、一個用於 String,一個用於 Fish。使用泛型時,您可以將清單設為泛型,因此可以保留任何類型的物件。這就像將型別設為可適用於多種型別的萬用字元。
如要定義泛型,請在類別名稱後方加上尖括號 <T>,並在尖括號中放入 T。(您可以使用其他字母或較長的名稱,但泛型型別的慣例是 T)。
class MyList<T> {
fun get(pos: Int): T {
TODO("implement")
}
fun addItem(item: T) {}
}您可以參照 T,就像參照一般型別一樣。get() 的傳回類型為 T,而 addItem() 的參數類型為 T。當然,泛型清單非常實用,因此 Kotlin 內建 List 類別。
步驟 1:建立型別階層
在本步驟中,您會建立一些類別,以便在下一個步驟中使用。我們在先前的程式碼研究室中介紹過子類別,但這裡會簡要複習。
- 為保持範例簡潔,請在 src 下建立名為
generics的新套件。 - 在 generics 套件中,建立新的
Aquarium.kt檔案。這樣一來,您就能使用相同名稱重新定義項目,不會發生衝突,因此本程式碼研究室的其餘程式碼都會寫入這個檔案。 - 建立供水類型階層。首先,請將
WaterSupply設為open類別,以便加入子類別。 - 新增布林值
var參數needsProcessing。這會自動建立可變動的屬性,以及 getter 和 setter。 - 建立擴充
WaterSupply的子類別TapWater,並傳遞true做為needsProcessing的needsProcessing,因為自來水含有對魚有害的添加物。 - 在
TapWater中,定義名為addChemicalCleaners()的函式,在淨水後將needsProcessing設為false。needsProcessing屬性可從TapWater設定,因為該屬性預設為public,且可供子類別存取。以下是已完成的程式碼。
package generics
open class WaterSupply(var needsProcessing: Boolean)
class TapWater : WaterSupply(true) {
fun addChemicalCleaners() {
needsProcessing = false
}
}- 建立
WaterSupply的另外兩個子類別,分別命名為FishStoreWater和LakeWater。FishStoreWater不需要處理,但LakeWater必須使用filter()方法進行篩選。篩選後就不需要再次處理,因此請在filter()中設定needsProcessing = false。
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}如需更多資訊,請參閱先前的 Kotlin 繼承課程。
步驟 2:建立泛型類別
在這個步驟中,您會修改 Aquarium 類別,以支援不同類型的供水。
- 在 Aquarium.kt 中定義
Aquarium類別,並在類別名稱後方的方括號中加入<T>。 - 將類型為
T的不可變動屬性waterSupply新增至Aquarium。
class Aquarium<T>(val waterSupply: T)- 編寫名為
genericsExample()的函式。這不是類別的一部分,因此可以放在檔案頂層,就像main()函式或類別定義一樣。在函式中建立Aquarium,並傳遞WaterSupply。由於waterSupply參數是泛型,因此您必須在角括號<>中指定型別。
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
}- 在
genericsExample()中,程式碼可以存取水族館的waterSupply。由於屬於TapWater類型,因此您可以呼叫addChemicalCleaners(),不必進行任何類型轉換。
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}- 建立
Aquarium物件時,您可以移除角括號和括號之間的內容,因為 Kotlin 具有型別推論功能。因此建立執行個體時,不需要說兩次「TapWater」。系統可以從Aquarium的引數推斷類型,但仍會建立TapWater類型的Aquarium。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}- 如要查看發生了什麼情況,請在呼叫
addChemicalCleaners()前後輸出needsProcessing。以下是完成的函式。
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
aquarium.waterSupply.addChemicalCleaners()
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
}- 新增
main()函式來呼叫genericsExample(),然後執行程式並觀察結果。
fun main() {
genericsExample()
}⇒ water needs processing: true water needs processing: false
步驟 3:提供更具體的資訊
「泛型」是指幾乎任何項目都可以傳遞,有時這會造成問題。在這個步驟中,您會讓 Aquarium 類別更具體地說明可放入的內容。
- 在
genericsExample()中建立Aquarium,傳遞waterSupply的字串,然後輸出水族館的waterSupply屬性。
fun genericsExample() {
val aquarium2 = Aquarium("string")
println(aquarium2.waterSupply)
}- 執行程式並觀察結果。
⇒ string
結果是您傳遞的字串,因為 Aquarium 不會對 T.Any 類型設下任何限制,包括 String 在內,任何類型都可以傳遞。
- 在
genericsExample()中建立另一個Aquarium,並傳遞null做為waterSupply。如果waterSupply為空值,則列印"waterSupply is null"。
fun genericsExample() {
val aquarium3 = Aquarium(null)
if (aquarium3.waterSupply == null) {
println("waterSupply is null")
}
}- 執行程式並觀察結果。
⇒ waterSupply is null
為什麼建立 Aquarium 時可以傳遞 null?這是因為根據預設,T 代表可為空值的 Any? 型別,也就是型別階層頂端的型別。這與您先前輸入的內容相同。
class Aquarium<T: Any?>(val waterSupply: T)- 如要禁止傳遞
null,請移除Any後方的?,明確將T設為Any類型。
class Aquarium<T: Any>(val waterSupply: T)在此情況下,Any 稱為「泛型限制」。也就是說,只要不是 null,任何型別都可以傳遞至 T。
- 您真正想做的是確保只有
WaterSupply(或其子類別之一) 可以傳遞至T。將Any替換為WaterSupply,定義更具體的泛型限制。
class Aquarium<T: WaterSupply>(val waterSupply: T)步驟 4:新增更多檢查
在本步驟中,您將瞭解 check() 函式,確保程式碼行為符合預期。check() 函式是 Kotlin 中的標準程式庫函式。這項函式會做為判斷提示,如果引數的計算結果為 false,就會擲回 IllegalStateException。
- 在
Aquarium類別中新增addWater()方法來加水,並使用check()確保不必先處理水。
class Aquarium<T: WaterSupply>(val waterSupply: T) {
fun addWater() {
check(!waterSupply.needsProcessing) { "water supply needs processing first" }
println("adding water from $waterSupply")
}
}在這種情況下,如果 needsProcessing 為 true,check() 就會擲回例外狀況。
- 在
genericsExample()中,加入程式碼來製作Aquarium,然後加水。LakeWater
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.addWater()
}- 執行程式,您會收到例外狀況,因為水必須先經過濾。
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first
at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)- 先呼叫函式來過濾水,再將水加入
Aquarium。現在執行程式時,不會擲回例外狀況。
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.waterSupply.filter()
aquarium4.addWater()
}⇒ adding water from generics.LakeWater@880ec60
上述內容涵蓋泛型的基本概念。下列工作涵蓋更多內容,但重要概念是瞭解如何使用泛型限制條件宣告及使用泛型類別。
在這項工作中,您將瞭解泛型中的「輸入」和「輸出」型別。in 類型只能傳遞至類別,無法傳回。out 型別是只能從類別傳回的型別。
查看 Aquarium 類別,您會發現只有在取得 waterSupply 屬性時,才會傳回泛型型別。沒有任何方法會將 T 型別的值做為參數 (在建構函式中定義除外)。Kotlin 可讓您為這種情況定義 out 型別,並推斷型別安全使用的位置相關額外資訊。同樣地,您也可以為只會傳遞至方法 (不會傳回) 的泛型類型定義 in 類型。這樣 Kotlin 就能對程式碼安全性進行額外檢查。
in 和 out 類型是 Kotlin 類型系統的指令。說明整個型別系統超出本訓練營的範圍 (相當複雜),但編譯器會適當標記未標示 in 和 out 的型別,因此您必須瞭解這些型別。
步驟 1:定義 out 型別
- 在
Aquarium類別中,將T: WaterSupply變更為out類型。
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}- 在同一個檔案中,於類別外部宣告函式
addItemTo(),該函式預期WaterSupply的Aquarium。
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")- 從
genericsExample()呼叫addItemTo(),然後執行程式。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
addItemTo(aquarium)
}⇒ item added
Kotlin 可確保 addItemTo() 不會對泛型 WaterSupply 執行任何型別不安全的動作,因為它已宣告為 out 型別。
- 如果移除
out關鍵字,編譯器會在呼叫addItemTo()時傳回錯誤,因為 Kotlin 無法確保您不會對該型別執行任何不安全的動作。
步驟 2:定義輸入型別
in 類型與 out 類型類似,但適用於只會傳遞至函式,不會傳回的泛型。如果您嘗試傳回 in 型別,就會收到編譯器錯誤。在本範例中,您將定義 in 型別做為介面的一部分。
- 在 Aquarium.kt 中,定義採用泛型
T的Cleaner介面,該泛型會限制為WaterSupply。由於這只會做為clean()的引數,因此您可以將其設為in參數。
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}- 如要使用
Cleaner介面,請建立實作Cleaner的TapWaterCleaner類別,以便加入化學物質來清潔TapWater。
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}- 在
Aquarium類別中,更新addWater()以取得T類型的Cleaner,並在加入前先清潔水。
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
fun addWater(cleaner: Cleaner<T>) {
if (waterSupply.needsProcessing) {
cleaner.clean(waterSupply)
}
println("water added")
}
}- 更新
genericsExample()範例程式碼,建立TapWaterCleaner、含TapWater的Aquarium,然後使用清潔劑加水。並視需要使用清潔劑。
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}Kotlin 會使用 in 和 out 型別資訊,確保您的程式碼安全地使用泛型。Out 和 in 很容易記住:out 型別可做為傳回值向外傳遞,in 型別可做為引數向內傳遞。

如要進一步瞭解類型內和類型外的問題,請參閱說明文件。
在本工作中,您將瞭解泛型函式和使用時機。一般來說,只要函式採用具有泛型類型的類別引數,就適合建立泛型函式。
步驟 1:建立泛型函式
- 在 generics/Aquarium.kt 中,建立採用
Aquarium的isWaterClean()函式。您需要指定參數的泛型型別,其中一個選項是使用WaterSupply。
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
println("aquarium water is clean: ${aquarium.waterSupply.needsProcessing}")
}但這表示 Aquarium 必須有 out 型別參數,才能呼叫。有時 out 或 in 的限制過多,因為您需要同時使用輸入和輸出類型。您可以將函式設為泛型,移除 out 需求。
- 如要將函式設為泛型,請在關鍵字
fun後方加上角括號,並加入泛型T和任何限制,在本例中為WaterSupply。將Aquarium變更為受T限制,而非WaterSupply。
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}T 是 isWaterClean() 的型別參數,用於指定水族館的一般型別。這個模式非常常見,建議您花點時間瞭解。
- 在函式名稱後方和括號前方,以角括號指定型別,即可呼叫
isWaterClean()函式。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean<TapWater>(aquarium)
}- 由於引數
aquarium的型別推論,不需要型別,因此請移除。執行程式並觀察輸出內容。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean(aquarium)
}⇒ aquarium water is clean: false
步驟 2:使用具體化型別建立泛型方法
您也可以將泛型函式用於方法,即使是在有自己泛型型別的類別中也一樣。在這個步驟中,您要將泛型方法新增至 Aquarium,檢查是否為 WaterSupply 型別。
- 在
Aquarium類別中,宣告方法hasWaterSupplyOfType(),該方法會採用受限於WaterSupply的泛型參數R(已使用T),並在waterSupply屬於R型別時傳回true。這類似於您先前宣告的函式,但位於Aquarium類別中。
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R- 請注意,最後的
R會加上紅色底線。將指標懸停在該圖示上,即可查看錯誤內容。
- 如要執行
is檢查,您需要告知 Kotlin 型別是「具體化」或實際型別,且可在函式中使用。如要這麼做,請在fun關鍵字前面加上inline,並在泛型R型別前面加上reified。
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R類型具體化後,您就可以像使用一般類型一樣使用,因為內嵌後就是實際類型。也就是說,您可以使用型別執行 is 檢查。
如果您未使用 reified,Kotlin 就不會允許 is 檢查,因為類型「不夠真實」。這是因為非具體化型別僅在編譯階段可用,無法在執行階段供程式使用。我們會在下一節中進一步討論。
- 傳遞
TapWater做為型別。與呼叫泛型函式類似,呼叫泛型方法時,請在函式名稱後方使用角括號和型別。執行程式並觀察結果。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>()) // true
}⇒ true
步驟 3:建立擴充功能函式
您也可以對一般函式和擴充函式使用具體化型別。
- 在
Aquarium類別外部,針對WaterSupply定義名為isOfType()的擴充功能函式,檢查傳遞的WaterSupply是否為特定型別,例如TapWater。
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T- 呼叫擴充功能函式,就像呼叫方法一樣。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.waterSupply.isOfType<TapWater>())
}⇒ true
只要是 Aquarium,無論是哪種型別 (Aquarium、TowerTank 或其他子類別),都可以使用這些擴充函式。Aquarium使用星號投影語法可輕鬆指定各種相符項目。使用星號投影時,Kotlin 也會確保您不會執行任何不安全的動作。
- 如要使用星號投影,請在
Aquarium後方加上<*>。將hasWaterSupplyOfType()移至擴充函式,因為它並非Aquarium核心 API 的一部分。
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R- 將呼叫變更為
hasWaterSupplyOfType(),然後執行程式。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>())
}⇒ true
在先前的範例中,您必須將泛型型別標示為 reified,並將函式設為 inline,因為 Kotlin 需要在執行階段 (而非僅在編譯階段) 瞭解這些型別。
Kotlin 只會在編譯時使用所有泛型。編譯器會藉此確保您安全地執行所有操作。所有泛型型別都會在執行階段遭到清除,因此先前才會出現有關檢查已清除型別的錯誤訊息。
結果顯示,編譯器可以建立正確的程式碼,而不必將泛型型別保留到執行階段。但這表示有時您會執行編譯器不支援的操作,例如對泛型型別進行 is 檢查。因此 Kotlin 新增了具體化或實際類型。
如要進一步瞭解具體化型別和型別抹除,請參閱 Kotlin 說明文件。
本課程著重於泛型,這對於讓程式碼更具彈性且更容易重複使用非常重要。
- 建立泛型類別,讓程式碼更具彈性。
- 新增泛型限制,限制與泛型搭配使用的型別。
- 搭配泛型使用
in和out型別,提供更完善的型別檢查,限制傳遞至類別或從類別傳回的型別。 - 建立通用函式和方法,處理通用型別。例如:
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... } - 使用泛型擴充功能函式,將非核心功能新增至類別。
- 有時需要具體化型別,是因為型別抹除。與泛型不同,具體化型別會持續到執行階段。
- 使用
check()函式,確認程式碼是否正常執行。例如:check(!waterSupply.needsProcessing) { "water supply needs processing first" }
Kotlin 說明文件
如要進一步瞭解本課程的任何主題,或遇到問題,請前往 https://kotlinlang.org。
Kotlin 教學課程
https://try.kotlinlang.org 網站提供豐富的教學課程 (稱為 Kotlin Koans)、網頁式解譯器,以及附有範例的完整參考文件。
Udacity 課程
如要查看這個主題的 Udacity 課程,請參閱「程式設計人員的 Kotlin 新手上路課程」。
IntelliJ IDEA
IntelliJ IDEA 的說明文件位於 JetBrains 網站。
本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:
- 視需要指派作業。
- 告知學員如何繳交作業。
- 為作業評分。
講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。
如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。
回答這些問題
第 1 題
下列何者為泛型類型的命名慣例?
▢ <Gen>
▢ <Generic>
▢ <T>
▢ <X>
第 2 題
對泛型允許的型別設下限制,稱為:
▢ 一般限制
▢ 一般限制
▢ 釐清
▢ 一般類型限制
第 3 題
具體化意義:
▢ 物件的實際執行影響已計算完畢。
▢ 類別已設定受限制的項目索引。
▢ 一般類型參數已轉換為實際類型。
▢ 觸發遠端錯誤指標。
繼續下一個課程:
如要查看課程總覽,包括其他程式碼研究室的連結,請參閱「程式設計人員的 Kotlin 新手上路課程:歡迎參加本課程。」