本程式碼研究室是程式設計課程 Kotlin 新手上路課程的一部分。使用程式碼研究室逐步完成程式碼課程後,您將能充分發揮本課程的潛能。視您的知識而定,您或許可以略過某些部分。本課程的適用對象為熟悉物件導向語言,且想要學習 Kotlin 的程式設計人員。
引言
本程式碼研究室將介紹一般類別、函式和方法,以及其在 Kotlin 中的運作方式。
本課程並非只建立一個範例應用程式,而是用來建立您的知識,但彼此之間互不相關,因此您可以熟悉自己熟悉的部分。許多產品是透過水族箱主題進行連結。如果您想看完整的水族箱故事,請參考程式設計程式 Kotlin 新手上路課程。
須知事項
- Kotlin 函式、類別和方法的語法
- 如何在 IntelliJ IDEA 中建立新類別並執行程式
課程內容
- 如何使用一般類別、方法和函式
執行步驟
- 建立一般類別並新增限制條件
- 建立
in
和out
類型 - 建立一般函式、方法和擴充函式
一般簡介
Kotlin 和許多程式設計語言一樣,都有一般類型。一般類型可讓您使用一般類別,讓課程更有彈性。
假設您導入的 MyList
類別含有項目清單。如果您沒有通用,就必須為每種類型導入新版 MyList
:一個用於 Double
,一個用於 String
,一個用於 Fish
。您可以利用通用範本將清單設為通用,以便存放任何類型的物件。就像將類型設為可配合許多類型的萬用字元。
如要定義一般類型,請在類別名稱後方加上角括號 <T>
。(您可以使用其他字母或較長的名稱,不過一般類型的慣例為 T)。
class MyList<T> {
fun get(pos: Int): T {
TODO("implement")
}
fun addItem(item: T) {}
}
您可以將 T
視為一般類型。get()
的傳回類型為 T
,而 addItem()
的參數類型為 T
。當然,一般名單相當實用,因此 List
類別內建在 Kotlin 中。
步驟 1:建立類型階層
在這個步驟中,您會建立一些要在下一個步驟中使用的類別。子類別在先前的程式碼研究室中已經介紹,但以下提供簡短說明。
- 為了讓範例不會顯得雜亂,請在 src 下方建立新套件並呼叫
generics
。 - 在「一般」套件中,建立新的
Aquarium.kt
檔案。這樣您就能使用相同名稱重新定義相同名稱,而不發生衝突,因此這個程式碼研究室的其他程式碼則放入這個檔案中。 - 建立供水類型的類型階層。首先請將
WaterSupply
設為open
類別,以便做為子類別。 - 新增布林值
var
參數needsProcessing
。這樣系統就會自動建立可變動的屬性,以及 getter 和 setter。 - 建立延伸
WaterSupply
的TapWater
子類別,並傳遞true
的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
不需要處理,但必須使用filter()
方法篩選LakeWater
。篩選完畢後,您不需要重新處理,因此在filter()
中設定needsProcessing = false
。
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}
如需其他資訊,請參閱 Kotlin 中有關繼承的課程。
步驟 2:建立一般類別
在這個步驟中,您會修改 Aquarium
類別以支援不同類型的水源。
- 在 Aquarium.kt 中定義
Aquarium
類別,並在類別名稱後方加上括號。<T>
- 在
Aquarium
中加入waterSupply
類型的不可變更屬性waterSupply
。
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.
任何類型 (包括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
後移除?
,明確地將Any
類型設為T
。
class Aquarium<T: Any>(val waterSupply: T)
在這種情況下,Any
稱為一般限制條件。任何類型都可以傳送 T
,前提是該類型不得為 null
。
- 您真正想要確保
T
只能傳遞一個WaterSupply
(或其其中一個子類別)。將Any
替換為WaterSupply
可定義較籠統的一般限制條件。
class Aquarium<T: WaterSupply>(val waterSupply: T)
步驟 4:新增更多檢查事項
在這個步驟中,您將瞭解 check()
函式如何確保程式碼正常運作。check()
函式是 Kotlin 中的標準程式庫函式。它作為聲明,如果其引數的評估為 false
,它就滿於 IllegalStateException
。
- 將
addWater()
方法新增至Aquarium
類別以加入水,並利用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()
中加入程式碼,以LakeWater
建立Aquarium
並加上一些水。
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:定義結束類型
- 在
Aquarium
類別中,將T: WaterSupply
變更為out
類型。
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}
- 在同一個類別中,在類別外宣告
addItemTo()
函式。這個函式會預期Aquarium
為WaterSupply
。
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 類型
in
類型與 out
類型類似,但在一般類型中只能傳遞至函式而非傳回的一般類型。如果您嘗試傳回 in
類型,就會收到編譯器錯誤。在此範例中,您將會定義 in
類型做為介面的一部分。
- 在 Aquarium.kt 中定義
Cleaner
介面,以便使用一般限制為WaterSupply
的一般T
。由於這個參數只會做為clean()
的引數使用,因此您可以將其設為in
參數。
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}
- 要使用
Cleaner
接口,請製作一個TapWaterCleaner
類,通過添加化碼器為Cleaner
執行TapWater
清塗。
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}
- 在
Aquarium
類別中,更新addWater()
以擷取Cleaner
類型的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
設為Aquarium
,然後使用TapWater
來加入一些用水,再使用清潔劑操作。它會視需要使用清潔劑。
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}
Kotlin 會使用 in
和 out
類型的資訊,確保您的程式碼安全無虞地使用一般用途。「Out
」和「in
」很容易記住:out
類型可以傳回傳回值,in
類型可以當做引數傳入。
如果您想進一步瞭解類型和排除類型的問題,請參閱說明文件,深入瞭解問題類型。
在這項工作中,您將瞭解通用函式的使用時機和使用時機。一般而言,每當函式接受一個具有一般類型的類別引數時,將為一般函式。
步驟 1:使用一般函式
- 在 generics/Aquarium.kt 中,製作一個
isWaterClean()
函數,需要Aquarium
。您必須指定參數的一般類型;其中一個選項是使用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()
方法,將R
限制為一般參數R
(T
已在使用中),如果waterSupply
類型為R
,則傳回true
。這應該是您之前宣告的函式,但在Aquarium
類別中。
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
- 請注意,最後一個
R
會以紅色底線標示。將滑鼠遊標移到指標上方,即可查看錯誤的定義。 - 如要進行
is
檢查,您必須告知 Kotlin 該類型為 reified (真實) 類型,且可在函式中使用。方法很簡單,只要在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
即可。使用 star-投影語法是指定各種配對的便利方法。當您使用星號投影功能時,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 新手課程:歡迎參加這堂課程。