程式設計人員的 Kotlin 新手上路課程 6:函式操控

這個程式碼研究室是「程式設計人員的 Kotlin 新手上路課程」的一部分。如果您按部就班完成程式碼研究室,就能充分體驗到本課程的價值。視您的知識多寡而定,您或許能略過某些部分。本課程適用於熟悉物件導向語言,且想學習 Kotlin 的程式設計師。

簡介

這是 Kotlin 新手上路課程的最後一個程式碼研究室。在本程式碼研究室中,您將瞭解註解和加上標籤的中斷。您會複習 lambda 和高階函式,這是 Kotlin 的重要部分。您也會進一步瞭解內嵌函式和單一抽象方法 (SAM) 介面。最後,您會進一步瞭解 Kotlin 標準程式庫

本課程的設計目標是協助您累積知識,但各單元之間彼此半獨立,因此您可以略過熟悉的部分,不必建構單一範例應用程式。為將這些範例連結在一起,許多範例都使用水族館主題。如要查看完整的魚缸故事,請參閱 Udacity 的程式設計人員 Kotlin 新手上路課程

必備知識

  • Kotlin 函式、類別和方法的語法
  • 如何在 IntelliJ IDEA 中建立新類別並執行程式
  • Lambda 和高階函式的基本概念

課程內容

  • 註解基本概念
  • 如何使用標示中斷
  • 進一步瞭解高階函式
  • 關於單一抽象方法 (SAM) 介面
  • 關於 Kotlin 標準程式庫

學習內容

  • 建立簡單的註解。
  • 使用標示中斷。
  • 複習 Kotlin 中的 lambda 函式。
  • 使用及建立高階函式。
  • 呼叫一些單一抽象方法介面。
  • 使用 Kotlin 標準程式庫中的部分函式。

註解是用來在程式碼中附加中繼資料,並非 Kotlin 專屬功能。編譯器會讀取註解,並用於產生程式碼或邏輯。許多架構 (例如 KtorKotlinx) 以及 Room,都會使用註解來設定執行方式,以及與程式碼的互動方式。您不太可能遇到任何註解,直到開始使用架構為止,但瞭解如何解讀註解很有幫助。

此外,您也可以透過 Kotlin 標準程式庫取得註解,藉此控制程式碼的編譯方式。如果您要將 Kotlin 匯出為 Java 程式碼,這些註解就非常實用,但除此之外,您通常不需要這些註解。

註解會放在註解項目正前方,大多數項目都可以註解,包括類別、函式、方法,甚至是控制結構。部分註解可以接受引數。

以下是註解範例。

@file:JvmName("InteropFish")
class InteropFish {
   companion object {
       @JvmStatic fun interop()
   }
}

這表示這個檔案的匯出名稱是 InteropFish,並帶有 JvmName 註解;JvmName 註解會採用 "InteropFish" 引數。在伴隨物件中,@JvmStatic 會告知 Kotlin 將 interop() 設為 InteropFish 中的靜態函式。

您也可以建立自己的註解,但這項功能主要適用於編寫程式庫的情況,因為程式庫需要在執行階段取得類別的特定資訊,也就是反射

步驟 1:建立新套件和檔案

  1. src 下方建立新套件 example
  2. 在「example」中,建立新的 Kotlin 檔案 Annotations.kt

步驟 2:建立自己的註解

  1. Annotations.kt 中,建立含有 trim()fertilize() 兩個方法的 Plant 類別。
class Plant {
        fun trim(){}
        fun fertilize(){}
}
  1. 建立函式,列印類別中的所有方法。使用 ::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)
    }
}
  1. 建立 main() 函式來呼叫測試常式。執行程式並觀察輸出內容。
fun main() {
    testAnnotations()
}
⇒ trim
fertilize
  1. 建立簡單的註解,ImAPlant
annotation class ImAPlant

這項作業只會標示註解。

  1. Plant 類別前面新增註解。
@ImAPlant class Plant{
    ...
}
  1. 變更 testAnnotations(),即可列印類別的所有註解。使用 annotations 取得類別的所有註解。執行程式並觀察結果。
fun testAnnotations() {
    val plantObject = Plant::class
    for (a in plantObject.annotations) {
        println(a.annotationClass.simpleName)
    }
}
⇒ ImAPlant
  1. 變更 testAnnotations() 即可找到 ImAPlant 註解。使用 findAnnotation() 找出特定註解。執行程式並觀察結果。
fun testAnnotations() {
    val plantObject = Plant::class
    val myAnnotationObject = plantObject.findAnnotation<ImAPlant>()
    println(myAnnotationObject)
}
⇒ @example.ImAPlant()

步驟 3:建立目標備註

註解可以指定 getter 或 setter。這時,您可以使用 @get:@set: 前置字串套用這些述詞。使用含有註解的架構時,這類情況很常見。

  1. 宣告兩個註解,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 後方加上 @ 符號。

  1. 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. 執行程式並觀察輸出內容。
⇒ 1 2 3 4 5 6 7 8 9 10 11 

同樣地,您可以使用標籤 continue。標籤式 continue 會繼續執行迴圈的下一次疊代,而不是跳出標籤式迴圈。

Lambda 是匿名函式,也就是沒有名稱的函式。您可以將這些函式指派給變數,並做為引數傳遞至函式和方法。非常實用。

步驟 1:建立簡單的 Lambda

  1. 在 IntelliJ IDEA 中啟動 REPL,方法是依序點選「Tools」>「Kotlin」>「Kotlin REPL」
  2. 建立含有引數 dirty: Int 的 lambda,執行計算作業,將 dirty 除以 2。將 lambda 指派給變數 waterFilter
val waterFilter = { dirty: Int -> dirty / 2 }
  1. 呼叫 waterFilter,並傳入值 30。
waterFilter(30)
⇒ res0: kotlin.Int = 15

步驟 2:建立篩選器 Lambda

  1. 在 REPL 中,建立含有一個屬性 name 的資料類別 Fish
data class Fish(val name: String)
  1. 建立 3 個 Fish 的清單,名稱分別為 Flipper、Moby Dick 和 Dory。
val myFish = listOf(Fish("Flipper"), Fish("Moby Dick"), Fish("Dory"))
  1. 新增篩選器,檢查名稱是否含有字母「i」。
myFish.filter { it.name.contains("i")}
⇒ res3: kotlin.collections.List<Line_1.Fish> = [Fish(name=Flipper), Fish(name=Moby Dick)]

在 lambda 運算式中,it 是指目前的清單元素,篩選器會依序套用至每個清單元素。

  1. 使用 ", " 做為分隔符,將 joinString() 套用至結果。
myFish.filter { it.name.contains("i")}.joinToString(", ") { it.name }
⇒ res4: kotlin.String = Flipper, Moby Dick

joinToString() 函式會將經過篩選的名稱以指定字串分隔,然後串連成字串。這是 Kotlin 標準程式庫內建的眾多實用函式之一。

將 lambda 或其他函式做為引數傳遞至函式,即可建立高階函式。上述篩選器就是簡單的例子。filter() 是一個函式,您會將 lambda 傳遞給該函式,指定如何處理清單中的每個元素。

使用擴充 lambda 撰寫高階函式是 Kotlin 語言最進階的部分之一。撰寫這些函式需要一段時間,但使用起來非常方便。

步驟 1:建立新類別

  1. example 套件中,建立新的 Kotlin 檔案 Fish.kt
  2. Fish.kt 中,建立資料類別 Fish,並包含一個屬性 name
data class Fish (var name: String)
  1. 建立 fishExamples() 函式。在 fishExamples() 中,建立名為 "splashy" 的魚 (全小寫)。
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
}
  1. 建立呼叫 fishExamples()main() 函式。
fun main () {
    fishExamples()
}
  1. 按一下 main() 左側的綠色三角形,編譯並執行程式。目前沒有輸出內容。

步驟 2:使用高階函式

with() 函式可讓您以更精簡的方式,建立一或多個物件或屬性的參照。使用 thiswith() 實際上是高階函式,您可以在 lambda 中指定如何處理提供的物件。

  1. 使用 with()fishExamples() 中的魚名改為大寫。在大括號中,this 是指傳遞至 with() 的物件。
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    with (fish.name) {
        this.capitalize()
    }
}
  1. 沒有輸出內容,因此請在周圍加上 println()。此外,this 是隱含的,不需要明確指定,因此可以移除。
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    with (fish.name) {
        println(capitalize())
    }
}
⇒ Splashy

步驟 3:建立高階函式

在實際運作時,with() 是高階函式。如要瞭解運作方式,您可以自行製作 with() 的大幅簡化版本,僅適用於字串。

  1. Fish.kt 中,定義採用兩個引數的函式 myWith()。引數是要執行的物件,以及定義作業的函式。函式的引數名稱慣例為 block。在本例中,該函式不會傳回任何內容,並以 Unit 指定。
fun myWith(name: String, block: String.() -> Unit) {}

myWith() 中,block() 現在是 String 的擴充函式。要擴充的類別通常稱為「接收器物件」。因此,name 在這個案例中是接收器物件。

  1. myWith() 的主體中,將傳入的函式 block() 套用至接收器物件 name
fun myWith(name: String, block: String.() -> Unit) {
    name.block()
}
  1. fishExamples() 中,將 with() 替換為 myWith()
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    myWith (fish.name) {
        println(capitalize())
    }
}

fish.name 是名稱引數,println(capitalize()) 則是區塊函式。

  1. 執行程式,程式會照常運作。
⇒ Splashy

步驟 4:探索更多內建擴充功能

with() 擴充功能 lambda 非常實用,屬於 Kotlin 標準程式庫。以下是其他幾個實用項目:run()apply()let()

run() 函式是適用於所有型別的擴充功能。這個函式會將一個 lambda 做為引數,並傳回執行 lambda 的結果。

  1. fishExamples() 中,對 fish 呼叫 run(),取得名稱。
fish.run {
   name
}

這只會傳回 name 屬性。您可以將該值指派給變數或列印出來。這並非實用範例,因為您只要存取屬性即可,但 run() 可用於更複雜的運算式。

apply() 函式與 run() 類似,但會傳回套用變更的物件,而不是 lambda 的結果。這項功能有助於呼叫新建立物件的方法。

  1. 複製 fish 並呼叫 apply(),設定新副本的名稱。
val fish2 = Fish(name = "splashy").apply {
     name = "sharky"
}
println(fish2.name)
⇒ sharky

let() 函式與 apply() 類似,但會傳回含有變更內容的物件副本。這項功能有助於將操控動作串連在一起。

  1. 使用 let() 取得 fish 的名稱,將名稱大寫,將另一個字串串連至該名稱,取得該結果的長度,將長度加上 31,然後列印結果。
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})
⇒ 42

在這個範例中,it 所參照的物件類型依序為 FishStringStringInt

  1. 在呼叫 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 做為參數的函式。請參考下列範例。

  1. example 中建立 Java 類別 JavaRun,然後將下列內容貼到該檔案中。
package example;

public class JavaRun {
    public static void runNow(Runnable runnable) {
        runnable.run();
    }
}

Kotlin 可讓您在型別前加上 object:,藉此將實作介面的物件例項化。這項功能可用於將參數傳遞至 SAM。

  1. 回到 Fish.kt,建立 runExample() 函式,使用 object: 建立 Runnable。物件應實作 run(),方法是列印 "I'm a Runnable"
fun runExample() {
    val runnable = object: Runnable {
        override fun run() {
            println("I'm a Runnable")
        }
    }
}
  1. 使用您建立的物件呼叫 JavaRun.runNow()
fun runExample() {
    val runnable = object: Runnable {
        override fun run() {
            println("I'm a Runnable")
        }
    }
    JavaRun.runNow(runnable)
}
  1. main() 呼叫 runExample(),然後執行程式。
⇒ I'm a Runnable

雖然列印內容需要大量工作,但這是 SAM 運作方式的絕佳範例。當然,Kotlin 提供更簡單的方法來達成此目標,也就是使用 lambda 取代物件,讓程式碼更精簡。

  1. 移除 runExample 中的現有程式碼,將其變更為呼叫含有 lambda 的 runNow(),然後執行程式。
fun runExample() {
    JavaRun.runNow({
        println("Passing a lambda as a Runnable")
    })
}
⇒ Passing a lambda as a Runnable
  1. 您可以使用最後一個參數呼叫語法,進一步簡化程式碼,並移除括號。
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 新手上路課程:歡迎參加本課程。