程式設計人員的 Kotlin 新手上路課程 5.1:擴充功能

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

簡介

在本程式碼研究室中,您將瞭解 Kotlin 中多種不同的實用功能,包括二元組、集合和擴充功能函式。

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

必備知識

  • Kotlin 函式、類別和方法的語法
  • 如何在 IntelliJ IDEA 中使用 Kotlin 的 REPL (Read-Eval-Print Loop)
  • 如何在 IntelliJ IDEA 中建立新類別並執行程式

課程內容

  • 如何使用配對和三元組
  • 進一步瞭解集合
  • 定義及使用常數
  • 編寫擴充功能函式

學習內容

  • 瞭解 REPL 中的配對、三元組和雜湊對映
  • 瞭解整理常數的不同方式
  • 編寫擴充功能函式和擴充功能屬性

在這項工作中,您將瞭解配對和三元組,以及如何解構這些項目。成對和三元組是預先建立的資料類別,適用於 2 或 3 個通用項目。舉例來說,這項功能可讓函式傳回多個值。

假設您有一種魚的 List,以及一個函式 isFreshWater(),可檢查魚是淡水魚還是鹹水魚。List.partition() 會傳回兩個清單,一個是條件為 true 的項目,另一個是條件為 false 的項目。

val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")

步驟 1:配對和三元組

  1. 開啟 REPL (依序點選「Tools」 >「Kotlin」 >「Kotlin REPL」)。
  2. 建立配對,將設備與用途建立關聯,然後列印值。如要建立配對,請使用 to 關鍵字建立運算式,連結兩個值 (例如兩個字串),然後使用 .first.second 參照每個值。
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. 建立三元組並使用 toString() 列印,然後使用 toList() 轉換為清單。您可以使用 3 個值的 Triple() 建立三元組。使用 .first.second.third 參照每個值。
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42)
[6, 9, 42]

上述範例為配對或三元組的所有部分使用相同型別,但這並非必要。例如,這些部分可以是字串、數字或清單,甚至是另一個配對或三元組。

  1. 建立配對,其中配對的第一部分本身就是配對。
val equipment2 = ("fish net" to "catching fish") to "equipment"
println("${equipment2.first} is ${equipment2.second}\n")
println("${equipment2.first.second}")
⇒ (fish net, catching fish) is equipment
⇒ catching fish

步驟 2:解構部分配對和三元組

將配對和三元組分成多個部分,稱為「解構」。將該配對或三元組指派給適當數量的變數,Kotlin 就會依序指派每個部分的值。

  1. 解構配對並列印值。
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
  1. 解構三元組並輸出值。
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

請注意,解構配對和三元組的運作方式與資料類別相同,這部分已在先前的程式碼研究室中說明。

在這項工作中,您將進一步瞭解集合 (包括清單) 和新的集合類型 (雜湊對映)。

步驟 1:進一步瞭解清單

  1. 我們在先前的課程中介紹了清單和可變動清單。清單是非常實用的資料結構,因此 Kotlin 提供許多清單的內建函式。請參閱這份清單函式 (部分) 清單。如需完整清單,請參閱 ListMutableList 的 Kotlin 說明文件。

功能

Purpose

add(element: E)

將項目加入可變動的清單。

remove(element: E)

從可變動的清單中移除項目。

reversed()

傳回清單副本,其中的元素會依相反順序排序。

contains(element: E)

如果清單包含該項目,則傳回 true

subList(fromIndex: Int, toIndex: Int)

傳回清單的一部分,從第一個索引到第二個索引 (不含第二個索引)。

  1. 繼續在 REPL 中作業,建立數字清單並呼叫 sum()。這會加總所有元素。
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. 建立字串清單並加總清單。
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
  1. 如果元素不是 List 可直接加總的項目 (例如字串),您可以使用 .sumBy() 和 lambda 函式指定加總方式,例如依每個字串的長度加總。lambda 引數的預設名稱為 it,這裡的 it 是指清單中的每個元素,因為清單會經過遍歷。
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. 清單還有許多其他用途。如要查看可用的功能,其中一種方法是在 IntelliJ IDEA 中建立清單、新增點,然後查看工具提示中的自動完成清單。這適用於任何物件。請試著使用清單。

  1. 從清單中選擇 listIterator(),然後使用 for 陳述式瀏覽清單,並列印以空格分隔的所有元素。
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

步驟 2:試用雜湊對應表

在 Kotlin 中,您可以使用 hashMapOf() 將幾乎任何項目對應至其他項目。雜湊對映有點像是配對清單,其中第一個值做為鍵。

  1. 建立與魚類症狀 (鍵) 和疾病 (值) 相符的雜湊對應。
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. 接著,您可以使用 get() 或更簡短的方括號 [],根據症狀鍵擷取疾病值。
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. 嘗試指定地圖中未列出的症狀。
println(cures["scale loss"])
⇒ null

如果地圖中沒有金鑰,嘗試傳回相符疾病會傳回 null。視地圖資料而定,可能的索引鍵可能沒有相符項目。Kotlin 提供 getOrDefault() 函式,可處理這類情況。

  1. 使用 getOrDefault() 查詢沒有相符項目的鍵。
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know

如果您需要執行的作業不只是傳回值,Kotlin 提供 getOrElse() 函式。

  1. 將程式碼改為使用 getOrElse(),而非 getOrDefault()
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this

系統不會傳回簡單的預設值,而是執行大括號 {} 之間的任何程式碼。在這個範例中,else 只會傳回字串,但也可以像尋找治療方法並傳回網頁一樣複雜。

mutableListOf 相同,您也可以建立 mutableMapOf。可變動的地圖可讓您放置及移除項目。可變表示能夠變更,不可變表示無法變更。

  1. 製作可修改的庫存地圖,將設備字串對應至項目數量。使用魚網建立該項目,然後使用 put() 將 3 個水箱刷加入商品目錄,並使用 remove() 移除魚網。
val inventory = mutableMapOf("fish net" to 1)
inventory.put("tank scrubber", 3)
println(inventory.toString())
inventory.remove("fish net")
println(inventory.toString())
⇒ {fish net=1, tank scrubber=3}{tank scrubber=3}

在這項工作中,您將瞭解 Kotlin 中的常數,以及整理常數的不同方式。

步驟 1:瞭解 const 與 val 的差異

  1. 在 REPL 中,嘗試建立數值常數。在 Kotlin 中,您可以使用 const val 建立頂層常數,並在編譯期間指派值。
const val rocks = 3

系統會指派值,且無法變更,這聽起來很像宣告一般 val。那麼,const valval 有何不同?const val 的值是在編譯時決定,而 val 的值是在程式執行期間決定,也就是說,val 可在執行階段由函式指派。

也就是說,val 可以從函式指派值,但 const val 無法。

val value1 = complexFunctionCall() // OK
const val CONSTANT1 = complexFunctionCall() // NOT ok

此外,const val 僅適用於頂層,以及以 object 宣告的單例類別,不適用於一般類別。您可以使用這個方法建立只包含常數的檔案或單例物件,並視需要匯入。

object Constants {
    const val CONSTANT2 = "object constant"
}
val foo = Constants.CONSTANT2

步驟 2:建立隨附物件

Kotlin 沒有類別層級常數的概念。

如要在類別中定義常數,必須使用以 companion 關鍵字宣告的伴生物件包裝常數。伴生物件基本上是類別中的單例模式物件。

  1. 建立類別,並使用含有字串常數的伴生物件。
class MyClass {
    companion object {
        const val CONSTANT3 = "constant in companion"
    }
}

伴隨物件和一般物件的基本差異如下:

  • 系統會從所含類別的靜態建構函式初始化伴隨物件,也就是在建立物件時建立伴隨物件。
  • 一般物件會在首次存取時延遲初始化,也就是首次使用時。

還有更多內容,但目前您只需要知道將類別中的常數包裝在伴隨物件中。

在這項工作中,您將瞭解如何擴充類別的行為。編寫公用程式函式來擴充類別的行為非常常見。Kotlin 提供便利的語法來宣告這些公用程式函式:擴充功能函式。

擴充函式可讓您將函式加入現有類別,而無需存取其原始碼。舉例來說,您可以在套件的 Extensions.kt 檔案中宣告這些項目。這實際上不會修改類別,但可以讓您在呼叫該類別物件的函式時使用點號標記法。

步驟 1:編寫擴充功能函式

  1. 還是在 REPL 中工作,撰寫簡單的擴充功能函式 hasSpaces(),檢查字串是否含有空格。函式名稱開頭是其使用的類別。在函式中,this 是指呼叫的物件,而 it 是指 find() 呼叫中的迭代器。
fun String.hasSpaces(): Boolean {
    val found = this.find { it == ' ' }
    return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
  1. 您可以簡化 hasSpaces() 函式。不需要明確使用 this,函式可以縮減為單一運算式並傳回,因此也不需要周圍的大括號 {}
fun String.hasSpaces() = find { it == ' ' } != null

步驟 2:瞭解擴充功能的限制

擴充函式只能存取擴充類別的公開 API。您無法存取 private 的變數。

  1. 請嘗試將擴充功能函式新增至標示為 private 的屬性。
class AquariumPlant(val color: String, private val size: Int)

fun AquariumPlant.isRed() = color == "red"    // OK
fun AquariumPlant.isBig() = size > 50         // gives error
⇒ error: cannot access 'size': it is private in 'AquariumPlant'
  1. 請檢查下列程式碼,並找出程式碼會列印的內容。
open class AquariumPlant(val color: String, private val size: Int)

class GreenLeafyPlant(size: Int) : AquariumPlant("green", size)

fun AquariumPlant.print() = println("AquariumPlant")
fun GreenLeafyPlant.print() = println("GreenLeafyPlant")

val plant = GreenLeafyPlant(size = 10)
plant.print()
println("\n")
val aquariumPlant: AquariumPlant = plant
aquariumPlant.print()  // what will it print?
⇒ GreenLeafyPlant
AquariumPlant

plant.print() 張沖印相片GreenLeafyPlant。您可能也會預期 aquariumPlant.print() 列印 GreenLeafyPlant,因為它已指派 plant 的值。但類型是在編譯時間解析,因此系統會輸出 AquariumPlant

步驟 3:新增擴充功能屬性

除了擴充功能函式,Kotlin 也允許您新增擴充功能屬性。與擴充功能函式類似,您要指定擴充的類別,後面接著一個點和屬性名稱。

  1. 繼續在 REPL 中作業,將擴充屬性 isGreen 新增至 AquariumPlant,如果顏色為綠色,則為 true
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

存取 isGreen 屬性時,就像存取一般屬性一樣;存取時,系統會呼叫 isGreen 的 getter 來取得值。

  1. 列印 aquariumPlant 變數的 isGreen 屬性,並觀察結果。
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

步驟 4:瞭解可為空值的接收器

您擴充的類別稱為「接收器」,可以將該類別設為可為空值。如果這麼做,主體中使用的 this 變數可能會是 null,請務必測試這點。如果預期呼叫端會想對可為空值的變數呼叫擴充方法,或是想在函式套用至 null 時提供預設行為,您會想採用可為空值的接收器。

  1. 繼續在 REPL 中作業,定義採用可為空值的接收器的方法。pull()類型後方會顯示問號 ?,且問號位於點號之前。在主體內,您可以使用問號點套用 ?.apply.,測試 this 是否不是 null
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. 在本例中,執行程式時不會有任何輸出內容。由於 plantnull,因此不會呼叫內部 println()

擴充函式非常強大,Kotlin 標準程式庫的大部分內容都是以擴充函式實作。

在本課程中,您進一步瞭解了集合和常數,並體驗了擴充函式和屬性的強大功能。

  • 您可以使用配對和三元組,從函式傳回多個值。例如:
    val twoLists = fish.partition { isFreshWater(it) }
  • Kotlin 有許多實用的 List 函式,例如 reversed()contains()subList()
  • HashMap 可用來將鍵對應至值。例如:
    val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  • 使用 const 關鍵字宣告編譯時間常數。您可以將這些函式放在頂層、以單例物件的形式整理,或是放在伴隨物件中。
  • 伴生物件是類別定義中的單例模式物件,以 companion 關鍵字定義。
  • 擴充函式和屬性可為類別新增功能。例如:
    fun String.hasSpaces() = find { it == ' ' } != null
  • 可為空值的接收器可讓您在類別上建立擴充功能,這類擴充功能可以是 null?. 運算子可與 apply 配對,在執行程式碼前檢查 null。例如:
    this?.apply { println("removing $this") }

Kotlin 說明文件

如要進一步瞭解本課程的任何主題,或遇到問題,請前往 https://kotlinlang.org

Kotlin 教學課程

https://try.kotlinlang.org 網站提供豐富的教學課程 (稱為 Kotlin Koans)、網頁式解譯器,以及附有範例的完整參考文件。

Udacity 課程

如要查看這個主題的 Udacity 課程,請參閱「程式設計人員的 Kotlin 新手上路課程」。

IntelliJ IDEA

IntelliJ IDEA 的說明文件位於 JetBrains 網站。

本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:

  • 視需要指派作業。
  • 告知學員如何繳交作業。
  • 為作業評分。

講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。

如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。

回答問題

第 1 題

下列哪一項會傳回清單副本?

add()

remove()

reversed()

contains()

第 2 題

下列哪個 class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean) 的擴充函式會產生編譯器錯誤?

fun AquariumPlant.isRed() = color == "red"

fun AquariumPlant.isBig() = size > 45

fun AquariumPlant.isExpensive() = cost > 10.00

fun AquariumPlant.isNotLeafy() = leafy == false

第 3 題

下列哪一個位置「無法」使用 const val 定義常數?

檔案頂層的 ▢

▢ 在一般課程中

單例模式物件中的 ▢

隨播廣告中的 ▢

繼續下一個課程:5.2 泛型

如要查看課程總覽,包括其他程式碼研究室的連結,請參閱「程式設計人員的 Kotlin 新手上路課程:歡迎參加本課程。