這個程式碼研究室是「程式設計人員的 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:配對和三元組
- 開啟 REPL (依序點選「Tools」 >「Kotlin」 >「Kotlin REPL」)。
- 建立配對,將設備與用途建立關聯,然後列印值。如要建立配對,請使用
to
關鍵字建立運算式,連結兩個值 (例如兩個字串),然後使用.first
或.second
參照每個值。
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
- 建立三元組並使用
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]
上述範例為配對或三元組的所有部分使用相同型別,但這並非必要。例如,這些部分可以是字串、數字或清單,甚至是另一個配對或三元組。
- 建立配對,其中配對的第一部分本身就是配對。
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 就會依序指派每個部分的值。
- 解構配對並列印值。
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
- 解構三元組並輸出值。
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42
請注意,解構配對和三元組的運作方式與資料類別相同,這部分已在先前的程式碼研究室中說明。
在這項工作中,您將進一步瞭解集合 (包括清單) 和新的集合類型 (雜湊對映)。
步驟 1:進一步瞭解清單
- 我們在先前的課程中介紹了清單和可變動清單。清單是非常實用的資料結構,因此 Kotlin 提供許多清單的內建函式。請參閱這份清單函式 (部分) 清單。如需完整清單,請參閱
List
和MutableList
的 Kotlin 說明文件。
功能 | Purpose |
| 將項目加入可變動的清單。 |
| 從可變動的清單中移除項目。 |
| 傳回清單副本,其中的元素會依相反順序排序。 |
| 如果清單包含該項目,則傳回 |
| 傳回清單的一部分,從第一個索引到第二個索引 (不含第二個索引)。 |
- 繼續在 REPL 中作業,建立數字清單並呼叫
sum()
。這會加總所有元素。
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
- 建立字串清單並加總清單。
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
- 如果元素不是
List
可直接加總的項目 (例如字串),您可以使用.sumBy()
和 lambda 函式指定加總方式,例如依每個字串的長度加總。lambda 引數的預設名稱為it
,這裡的it
是指清單中的每個元素,因為清單會經過遍歷。
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
- 清單還有許多其他用途。如要查看可用的功能,其中一種方法是在 IntelliJ IDEA 中建立清單、新增點,然後查看工具提示中的自動完成清單。這適用於任何物件。請試著使用清單。
- 從清單中選擇
listIterator()
,然後使用for
陳述式瀏覽清單,並列印以空格分隔的所有元素。
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
println("$s ")
}
⇒ a bbb cc
步驟 2:試用雜湊對應表
在 Kotlin 中,您可以使用 hashMapOf()
將幾乎任何項目對應至其他項目。雜湊對映有點像是配對清單,其中第一個值做為鍵。
- 建立與魚類症狀 (鍵) 和疾病 (值) 相符的雜湊對應。
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
- 接著,您可以使用
get()
或更簡短的方括號[]
,根據症狀鍵擷取疾病值。
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
- 嘗試指定地圖中未列出的症狀。
println(cures["scale loss"])
⇒ null
如果地圖中沒有金鑰,嘗試傳回相符疾病會傳回 null
。視地圖資料而定,可能的索引鍵可能沒有相符項目。Kotlin 提供 getOrDefault()
函式,可處理這類情況。
- 使用
getOrDefault()
查詢沒有相符項目的鍵。
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know
如果您需要執行的作業不只是傳回值,Kotlin 提供 getOrElse()
函式。
- 將程式碼改為使用
getOrElse()
,而非getOrDefault()
。
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this
系統不會傳回簡單的預設值,而是執行大括號 {}
之間的任何程式碼。在這個範例中,else
只會傳回字串,但也可以像尋找治療方法並傳回網頁一樣複雜。
與 mutableListOf
相同,您也可以建立 mutableMapOf
。可變動的地圖可讓您放置及移除項目。可變表示能夠變更,不可變表示無法變更。
- 製作可修改的庫存地圖,將設備字串對應至項目數量。使用魚網建立該項目,然後使用
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 的差異
- 在 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
關鍵字宣告的伴生物件包裝常數。伴生物件基本上是類別中的單例模式物件。
- 建立類別,並使用含有字串常數的伴生物件。
class MyClass {
companion object {
const val CONSTANT3 = "constant in companion"
}
}
伴隨物件和一般物件的基本差異如下:
- 系統會從所含類別的靜態建構函式初始化伴隨物件,也就是在建立物件時建立伴隨物件。
- 一般物件會在首次存取時延遲初始化,也就是首次使用時。
還有更多內容,但目前您只需要知道將類別中的常數包裝在伴隨物件中。
在這項工作中,您將瞭解如何擴充類別的行為。編寫公用程式函式來擴充類別的行為非常常見。Kotlin 提供便利的語法來宣告這些公用程式函式:擴充功能函式。
擴充函式可讓您將函式加入現有類別,而無需存取其原始碼。舉例來說,您可以在套件的 Extensions.kt 檔案中宣告這些項目。這實際上不會修改類別,但可以讓您在呼叫該類別物件的函式時使用點號標記法。
步驟 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
- 您可以簡化
hasSpaces()
函式。不需要明確使用this
,函式可以縮減為單一運算式並傳回,因此也不需要周圍的大括號{}
。
fun String.hasSpaces() = find { it == ' ' } != null
步驟 2:瞭解擴充功能的限制
擴充函式只能存取擴充類別的公開 API。您無法存取 private
的變數。
- 請嘗試將擴充功能函式新增至標示為
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'
- 請檢查下列程式碼,並找出程式碼會列印的內容。
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 也允許您新增擴充功能屬性。與擴充功能函式類似,您要指定擴充的類別,後面接著一個點和屬性名稱。
- 繼續在 REPL 中作業,將擴充屬性
isGreen
新增至AquariumPlant
,如果顏色為綠色,則為true
。
val AquariumPlant.isGreen: Boolean
get() = color == "green"
存取 isGreen
屬性時,就像存取一般屬性一樣;存取時,系統會呼叫 isGreen
的 getter 來取得值。
- 列印
aquariumPlant
變數的isGreen
屬性,並觀察結果。
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true
步驟 4:瞭解可為空值的接收器
您擴充的類別稱為「接收器」,可以將該類別設為可為空值。如果這麼做,主體中使用的 this
變數可能會是 null
,請務必測試這點。如果預期呼叫端會想對可為空值的變數呼叫擴充方法,或是想在函式套用至 null
時提供預設行為,您會想採用可為空值的接收器。
- 繼續在 REPL 中作業,定義採用可為空值的接收器的方法。
pull()
類型後方會顯示問號?
,且問號位於點號之前。在主體內,您可以使用問號點套用?.apply.
,測試this
是否不是null
fun AquariumPlant?.pull() {
this?.apply {
println("removing $this")
}
}
val plant: AquariumPlant? = null
plant.pull()
- 在本例中,執行程式時不會有任何輸出內容。由於
plant
是null
,因此不會呼叫內部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
定義常數?
檔案頂層的 ▢
▢ 在一般課程中
單例模式物件中的 ▢
隨播廣告中的 ▢
繼續下一個課程:
如要查看課程總覽,包括其他程式碼研究室的連結,請參閱「程式設計人員的 Kotlin 新手上路課程:歡迎參加本課程。」