這個程式碼研究室是「程式設計人員的 Kotlin 新手上路課程」的一部分。如果您按部就班完成程式碼研究室,就能充分體驗到本課程的價值。視您的知識多寡而定,您或許能略過某些部分。本課程適用於熟悉物件導向語言,且想學習 Kotlin 的程式設計師。
簡介
這是 Kotlin 新手上路課程的最後一個程式碼研究室。在本程式碼研究室中,您將瞭解註解和加上標籤的中斷。您會複習 lambda 和高階函式,這是 Kotlin 的重要部分。您也會進一步瞭解內嵌函式和單一抽象方法 (SAM) 介面。最後,您會進一步瞭解 Kotlin 標準程式庫。
本課程的設計目標是協助您累積知識,但各單元之間彼此半獨立,因此您可以略過熟悉的部分,不必建構單一範例應用程式。為將這些範例連結在一起,許多範例都使用水族館主題。如要查看完整的魚缸故事,請參閱 Udacity 的程式設計人員 Kotlin 新手上路課程。
必備知識
- Kotlin 函式、類別和方法的語法
- 如何在 IntelliJ IDEA 中建立新類別並執行程式
- Lambda 和高階函式的基本概念
課程內容
- 註解基本概念
- 如何使用標示中斷
- 進一步瞭解高階函式
- 關於單一抽象方法 (SAM) 介面
- 關於 Kotlin 標準程式庫
學習內容
- 建立簡單的註解。
- 使用標示中斷。
- 複習 Kotlin 中的 lambda 函式。
- 使用及建立高階函式。
- 呼叫一些單一抽象方法介面。
- 使用 Kotlin 標準程式庫中的部分函式。
註解是用來在程式碼中附加中繼資料,並非 Kotlin 專屬功能。編譯器會讀取註解,並用於產生程式碼或邏輯。許多架構 (例如 Ktor 和 Kotlinx) 以及 Room,都會使用註解來設定執行方式,以及與程式碼的互動方式。您不太可能遇到任何註解,直到開始使用架構為止,但瞭解如何解讀註解很有幫助。
此外,您也可以透過 Kotlin 標準程式庫取得註解,藉此控制程式碼的編譯方式。如果您要將 Kotlin 匯出為 Java 程式碼,這些註解就非常實用,但除此之外,您通常不需要這些註解。
註解會放在註解項目正前方,大多數項目都可以註解,包括類別、函式、方法,甚至是控制結構。部分註解可以接受引數。
以下是註解範例。
@file:JvmName("InteropFish")
class InteropFish {
companion object {
@JvmStatic fun interop()
}
}這表示這個檔案的匯出名稱是 InteropFish,並帶有 JvmName 註解;JvmName 註解會採用 "InteropFish" 引數。在伴隨物件中,@JvmStatic 會告知 Kotlin 將 interop() 設為 InteropFish 中的靜態函式。
您也可以建立自己的註解,但這項功能主要適用於編寫程式庫的情況,因為程式庫需要在執行階段取得類別的特定資訊,也就是反射。
步驟 1:建立新套件和檔案
- 在 src 下方建立新套件
example。 - 在「example」中,建立新的 Kotlin 檔案
Annotations.kt。
步驟 2:建立自己的註解
- 在
Annotations.kt中,建立含有trim()和fertilize()兩個方法的Plant類別。
class Plant {
fun trim(){}
fun fertilize(){}
}- 建立函式,列印類別中的所有方法。使用
::class在執行階段取得類別的相關資訊。使用declaredMemberFunctions取得類別的方法清單。(如要存取這項功能,請匯入kotlin.reflect.full.*)
import kotlin.reflect.full.* // required import
class Plant {
fun trim(){}
fun fertilize(){}
}
fun testAnnotations() {
val classObj = Plant::class
for (m in classObj.declaredMemberFunctions) {
println(m.name)
}
}- 建立
main()函式來呼叫測試常式。執行程式並觀察輸出內容。
fun main() {
testAnnotations()
}⇒ trim fertilize
- 建立簡單的註解,
ImAPlant。
annotation class ImAPlant這項作業只會標示註解。
- 在
Plant類別前面新增註解。
@ImAPlant class Plant{
...
}- 變更
testAnnotations(),即可列印類別的所有註解。使用annotations取得類別的所有註解。執行程式並觀察結果。
fun testAnnotations() {
val plantObject = Plant::class
for (a in plantObject.annotations) {
println(a.annotationClass.simpleName)
}
}⇒ ImAPlant
- 變更
testAnnotations()即可找到ImAPlant註解。使用findAnnotation()找出特定註解。執行程式並觀察結果。
fun testAnnotations() {
val plantObject = Plant::class
val myAnnotationObject = plantObject.findAnnotation<ImAPlant>()
println(myAnnotationObject)
}
⇒ @example.ImAPlant()
步驟 3:建立目標備註
註解可以指定 getter 或 setter。這時,您可以使用 @get: 或 @set: 前置字串套用這些述詞。使用含有註解的架構時,這類情況很常見。
- 宣告兩個註解,
OnGet只能套用至屬性 Getter,OnSet只能套用至屬性 Setter。在每個項目上使用@Target(AnnotationTarger.PROPERTY_GETTER)或PROPERTY_SETTER。
annotation class ImAPlant
@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class OnGet
@Target(AnnotationTarget.PROPERTY_SETTER)
annotation class OnSet
@ImAPlant class Plant {
@get:OnGet
val isGrowing: Boolean = true
@set:OnSet
var needsFood: Boolean = false
}註解功能非常強大,可建立程式庫,在執行階段和編譯階段檢查項目。不過,一般應用程式碼只會使用架構提供的註解。
Kotlin 提供多種控制流程的方式。您已熟悉 return,這個關鍵字會從函式傳回至封閉函式。使用 break 的方式與 return 類似,但適用於迴圈。
Kotlin 提供所謂的「標籤中斷」,可進一步控管迴圈。break 標示有標籤的 break 會跳到標示該標籤的迴圈後方,處理巢狀迴圈時,這項功能特別實用。
Kotlin 中的任何運算式都可以標示標籤。標籤的形式為 ID 後方加上 @ 符號。
- 在
Annotations.kt中,嘗試從內部迴圈中跳出,使用標籤中斷。
fun labels() {
outerLoop@ for (i in 1..100) {
print("$i ")
for (j in 1..100) {
if (i > 10) break@outerLoop // breaks to outer loop
}
}
}
fun main() {
labels()
}- 執行程式並觀察輸出內容。
⇒ 1 2 3 4 5 6 7 8 9 10 11
同樣地,您可以使用標籤 continue。標籤式 continue 會繼續執行迴圈的下一次疊代,而不是跳出標籤式迴圈。
Lambda 是匿名函式,也就是沒有名稱的函式。您可以將這些函式指派給變數,並做為引數傳遞至函式和方法。非常實用。
步驟 1:建立簡單的 Lambda
- 在 IntelliJ IDEA 中啟動 REPL,方法是依序點選「Tools」>「Kotlin」>「Kotlin REPL」。
- 建立含有引數
dirty: Int的 lambda,執行計算作業,將dirty除以 2。將 lambda 指派給變數waterFilter。
val waterFilter = { dirty: Int -> dirty / 2 }- 呼叫
waterFilter,並傳入值 30。
waterFilter(30)⇒ res0: kotlin.Int = 15
步驟 2:建立篩選器 Lambda
- 在 REPL 中,建立含有一個屬性
name的資料類別Fish。
data class Fish(val name: String)- 建立 3 個
Fish的清單,名稱分別為 Flipper、Moby Dick 和 Dory。
val myFish = listOf(Fish("Flipper"), Fish("Moby Dick"), Fish("Dory"))- 新增篩選器,檢查名稱是否含有字母「i」。
myFish.filter { it.name.contains("i")}
⇒ res3: kotlin.collections.List<Line_1.Fish> = [Fish(name=Flipper), Fish(name=Moby Dick)]
在 lambda 運算式中,it 是指目前的清單元素,篩選器會依序套用至每個清單元素。
- 使用
", "做為分隔符,將joinString()套用至結果。
myFish.filter { it.name.contains("i")}.joinToString(", ") { it.name }
⇒ res4: kotlin.String = Flipper, Moby Dick
joinToString() 函式會將經過篩選的名稱以指定字串分隔,然後串連成字串。這是 Kotlin 標準程式庫內建的眾多實用函式之一。
將 lambda 或其他函式做為引數傳遞至函式,即可建立高階函式。上述篩選器就是簡單的例子。filter() 是一個函式,您會將 lambda 傳遞給該函式,指定如何處理清單中的每個元素。
使用擴充 lambda 撰寫高階函式是 Kotlin 語言最進階的部分之一。撰寫這些函式需要一段時間,但使用起來非常方便。
步驟 1:建立新類別
- 在 example 套件中,建立新的 Kotlin 檔案
Fish.kt。 - 在
Fish.kt中,建立資料類別Fish,並包含一個屬性name。
data class Fish (var name: String)- 建立
fishExamples()函式。在fishExamples()中,建立名為"splashy"的魚 (全小寫)。
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
}- 建立呼叫
fishExamples()的main()函式。
fun main () {
fishExamples()
}- 按一下
main()左側的綠色三角形,編譯並執行程式。目前沒有輸出內容。
步驟 2:使用高階函式
with() 函式可讓您以更精簡的方式,建立一或多個物件或屬性的參照。使用 this,with() 實際上是高階函式,您可以在 lambda 中指定如何處理提供的物件。
- 使用
with()將fishExamples()中的魚名改為大寫。在大括號中,this是指傳遞至with()的物件。
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
with (fish.name) {
this.capitalize()
}
}- 沒有輸出內容,因此請在周圍加上
println()。此外,this是隱含的,不需要明確指定,因此可以移除。
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
with (fish.name) {
println(capitalize())
}
}⇒ Splashy
步驟 3:建立高階函式
在實際運作時,with() 是高階函式。如要瞭解運作方式,您可以自行製作 with() 的大幅簡化版本,僅適用於字串。
- 在
Fish.kt中,定義採用兩個引數的函式myWith()。引數是要執行的物件,以及定義作業的函式。函式的引數名稱慣例為block。在本例中,該函式不會傳回任何內容,並以Unit指定。
fun myWith(name: String, block: String.() -> Unit) {}在 myWith() 中,block() 現在是 String 的擴充函式。要擴充的類別通常稱為「接收器物件」。因此,name 在這個案例中是接收器物件。
- 在
myWith()的主體中,將傳入的函式block()套用至接收器物件name。
fun myWith(name: String, block: String.() -> Unit) {
name.block()
}- 在
fishExamples()中,將with()替換為myWith()。
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
myWith (fish.name) {
println(capitalize())
}
}fish.name 是名稱引數,println(capitalize()) 則是區塊函式。
- 執行程式,程式會照常運作。
⇒ Splashy
步驟 4:探索更多內建擴充功能
with() 擴充功能 lambda 非常實用,屬於 Kotlin 標準程式庫。以下是其他幾個實用項目:run()、apply() 和 let()。
run() 函式是適用於所有型別的擴充功能。這個函式會將一個 lambda 做為引數,並傳回執行 lambda 的結果。
- 在
fishExamples()中,對fish呼叫run(),取得名稱。
fish.run {
name
}這只會傳回 name 屬性。您可以將該值指派給變數或列印出來。這並非實用範例,因為您只要存取屬性即可,但 run() 可用於更複雜的運算式。
apply() 函式與 run() 類似,但會傳回套用變更的物件,而不是 lambda 的結果。這項功能有助於呼叫新建立物件的方法。
- 複製
fish並呼叫apply(),設定新副本的名稱。
val fish2 = Fish(name = "splashy").apply {
name = "sharky"
}
println(fish2.name)
⇒ sharky
let() 函式與 apply() 類似,但會傳回含有變更內容的物件副本。這項功能有助於將操控動作串連在一起。
- 使用
let()取得fish的名稱,將名稱大寫,將另一個字串串連至該名稱,取得該結果的長度,將長度加上 31,然後列印結果。
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})⇒ 42
在這個範例中,it 所參照的物件類型依序為 Fish、String、String 和 Int。
- 在呼叫
let()後列印fish,您會發現它沒有變更。
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})
println(fish)⇒ 42 Fish(name=splashy)
Lambda 和高階函式非常實用,但您應該瞭解:lambda 是物件。lambda 運算式是 Function 介面的執行個體,而該介面本身是 Object 的子型別。請參閱先前的 myWith() 範例。
myWith(fish.name) {
capitalize()
}Function 介面有一個方法 invoke(),這個方法會遭到覆寫,以呼叫 lambda 運算式。以手寫方式表示,看起來會類似於下列程式碼。
// actually creates an object that looks like this
myWith(fish.name, object : Function1<String, Unit> {
override fun invoke(name: String) {
name.capitalize()
}
})一般來說,這不會造成問題,因為建立物件和呼叫函式不會產生太多額外負擔,也就是記憶體和 CPU 時間。但如果您要定義 myWith() 等隨處可用的項目,額外負荷可能會增加。
Kotlin 提供 inline 做為處理這個情況的方法,可為編譯器增加一些工作,藉此減少執行階段的負擔。(在先前討論具體化型別的課程中,您已稍微瞭解 inline)。將函式標示為 inline,表示每次呼叫函式時,編譯器都會實際轉換原始碼,將函式「內嵌」。也就是說,編譯器會變更程式碼,將 lambda 替換為 lambda 內的指令。
如果上述範例中的 myWith() 標示為 inline:
inline myWith(fish.name) {
capitalize()
}轉換為直接呼叫:
// with myWith() inline, this becomes
fish.name.capitalize()請注意,內嵌大型函式會增加程式碼大小,因此最好用於多次使用的簡單函式,例如 myWith()。您稍早學到的程式庫中的擴充功能函式會標示為 inline,因此不必擔心會建立額外的物件。
單一抽象方法是指介面上只有一個方法。使用以 Java 程式設計語言編寫的 API 時,這類介面非常常見,因此有縮寫 SAM。舉例來說,Runnable 只有一個抽象方法 run(),而 Callable 只有一個抽象方法 call()。
在 Kotlin 中,您必須一律呼叫以 SAM 做為參數的函式。請參考下列範例。
- 在 example 中建立 Java 類別
JavaRun,然後將下列內容貼到該檔案中。
package example;
public class JavaRun {
public static void runNow(Runnable runnable) {
runnable.run();
}
}Kotlin 可讓您在型別前加上 object:,藉此將實作介面的物件例項化。這項功能可用於將參數傳遞至 SAM。
- 回到
Fish.kt,建立runExample()函式,使用object:建立Runnable。物件應實作run(),方法是列印"I'm a Runnable"。
fun runExample() {
val runnable = object: Runnable {
override fun run() {
println("I'm a Runnable")
}
}
}- 使用您建立的物件呼叫
JavaRun.runNow()。
fun runExample() {
val runnable = object: Runnable {
override fun run() {
println("I'm a Runnable")
}
}
JavaRun.runNow(runnable)
}- 從
main()呼叫runExample(),然後執行程式。
⇒ I'm a Runnable
雖然列印內容需要大量工作,但這是 SAM 運作方式的絕佳範例。當然,Kotlin 提供更簡單的方法來達成此目標,也就是使用 lambda 取代物件,讓程式碼更精簡。
- 移除
runExample中的現有程式碼,將其變更為呼叫含有 lambda 的runNow(),然後執行程式。
fun runExample() {
JavaRun.runNow({
println("Passing a lambda as a Runnable")
})
}
⇒ Passing a lambda as a Runnable
- 您可以使用最後一個參數呼叫語法,進一步簡化程式碼,並移除括號。
fun runExample() {
JavaRun.runNow {
println("Last parameter is a lambda as a Runnable")
}
}⇒ Last parameter is a lambda as a Runnable
這就是 SAM (單一抽象方法) 的基本概念。您可以使用以下模式,以一行程式碼例項化、覆寫及呼叫 SAM:Class.singleAbstractMethod { lambda_of_override }
本課程複習了 lambda,並深入探討高階函式,這是 Kotlin 的重要部分。您也瞭解了註解和加上標籤的中斷。
- 使用註解向編譯器指定事項。例如:
@file:JvmName("Foo") - 使用標籤中斷,讓程式碼從巢狀迴圈內退出。例如:
if (i > 10) break@outerLoop // breaks to outerLoop label - 搭配高階函式使用時,Lambda 相當實用。
- Lambda 是物件。如要避免建立物件,可以利用
inline標記函式,編譯器會直接將 lambda 的內容放入程式碼中。 - 請謹慎使用
inline,但這項功能有助於減少程式的資源用量。 - 單一抽象方法 (SAM) 是常見模式,使用 lambda 可簡化這個模式。基本模式為:
Class.singleAbstractMethod { lamba_of_override } - Kotlin 標準程式庫提供多項實用函式,包括多個 SAM,因此請瞭解程式庫的內容。
本課程介紹的 Kotlin 內容只是冰山一角,但您現在已掌握基本知識,可以開始開發自己的 Kotlin 程式。希望您會喜歡這種富有表現力的語言,並期待能以更少的程式碼建立更多功能 (如果您是使用 Java 程式設計語言,更是如此)。邊做邊學是成為 Kotlin 專家的最佳途徑,因此請繼續自行探索及學習 Kotlin。
Kotlin 說明文件
如要進一步瞭解本課程的任何主題,或遇到問題,請前往 https://kotlinlang.org。
Kotlin 教學課程
https://try.kotlinlang.org 網站提供豐富的教學課程 (稱為 Kotlin Koans)、網頁式解譯器,以及附有範例的完整參考文件。
Udacity 課程
如要查看這個主題的 Udacity 課程,請參閱「程式設計人員的 Kotlin 新手上路課程」。
IntelliJ IDEA
IntelliJ IDEA 的說明文件位於 JetBrains 網站。
Kotlin 標準程式庫
Kotlin 標準程式庫提供許多實用函式。撰寫自己的函式或介面前,請務必先檢查標準程式庫,看看是否有人已為您節省一些工作。我們經常會新增功能,建議您不時回來查看。
Kotlin 教學課程
https://try.kotlinlang.org 網站提供豐富的教學課程 (稱為 Kotlin Koans)、網頁式解譯器,以及附有範例的完整參考文件。
Udacity 課程
如要查看這個主題的 Udacity 課程,請參閱「程式設計人員的 Kotlin 新手上路課程」。
IntelliJ IDEA
IntelliJ IDEA 的說明文件位於 JetBrains 網站。
本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:
- 視需要指派作業。
- 告知學員如何繳交作業。
- 為作業評分。
講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。
如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。
回答問題
第 1 題
在 Kotlin 中,SAM 代表:
▢ 安全引數比對
▢ 簡易存取方法
▢ 單一抽象方法
▢ 策略存取方法
第 2 題
下列哪一項不是 Kotlin 標準程式庫擴充函式?
▢ elvis()
▢ apply()
▢ run()
▢ with()
第 3 題
下列有關 Kotlin 中 lambda 的敘述,何者不正確?
▢ Lambda 是匿名函式。
▢ Lambda 是物件,除非內嵌。
▢ Lambda 耗用大量資源,不應使用。
▢ Lambda 可以傳遞至其他函式。
第 4 題
Kotlin 中的標籤會以 ID 表示,後接:
▢ :
▢ ::
▢ @:
▢ @
恭喜!您已完成「程式設計人員的 Kotlin 新手上路課程」程式碼研究室。
如要查看課程總覽,包括其他程式碼研究室的連結,請參閱「程式設計人員的 Kotlin 新手上路課程:歡迎參加本課程。」