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

本程式碼研究室是程式設計課程 Kotlin 新手上路課程的一部分。使用程式碼研究室逐步完成程式碼課程後,您將能充分發揮本課程的潛能。視您的知識而定,您或許可以略過某些部分。本課程的適用對象為熟悉物件導向語言,且想要學習 Kotlin 的程式設計人員。

引言

在這個程式碼研究室中,您將認識許多 Kotlin 不同的實用功能,包括配對、集合和擴充功能函式。

本課程並非只建立一個範例應用程式,而是用來建立您的知識,但彼此之間互不相關,因此您可以熟悉自己熟悉的部分。許多產品是透過水族箱主題進行連結。如果您想看完整的水族箱故事,請參考程式設計程式 Kotlin 新手上路課程

須知事項

  • Kotlin 函式、類別和方法的語法
  • 如何在 IntelliJ IDEA 中使用 Kotlin's 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 ([工具] > [Kotlin] &gt [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() 將其轉換成清單。您可以使用 Triple() 搭配 3 個值建立三重值。使用 .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 為清單提供了許多內建函式。請參閱這份清單的部分函式清單。您可以在 Kotlin 說明文件中找到 ListMutableList 的完整資訊。

函式

用途

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 val」和「val」有什麼差別?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 關鍵字的宣告物件。隨播廣告物件基本上是類別中的單一 ton 物件。

  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 時,請在 AquariumPlant 中加入擴充功能屬性 isGreen;如果顏色為綠色,就會加上 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() 方法。這部分會在類型後面加上一個問號 ?。在內文中,您可以使用 questionmark-dot-apply ?.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 新手課程:歡迎參加這堂課程。