這個程式碼研究室是「程式設計人員的 Kotlin 新手上路課程」的一部分。如果您按部就班完成程式碼研究室,就能充分體驗到本課程的價值。視您的知識多寡而定,您或許能略過某些部分。本課程適用於熟悉物件導向語言,且想學習 Kotlin 的程式設計師。
簡介
在本程式碼研究室中,您將建立 Kotlin 程式,並瞭解 Kotlin 中的函式,包括參數、篩選器、lambda 和精簡函式的預設值。
本課程的設計目標是協助您累積知識,但各單元之間彼此半獨立,因此您可以略過熟悉的部分,不必建構單一範例應用程式。為將這些範例連結在一起,許多範例都使用水族館主題。如要查看完整的魚缸故事,請參閱 Udacity 的程式設計人員 Kotlin 新手上路課程。
必備知識
- 現代物件導向靜態型別程式設計語言的基礎知識
- 至少一種語言的類別、方法和例外狀況處理程式設計
- 如何在 IntelliJ IDEA 中使用 Kotlin 的 REPL (Read-Eval-Print Loop)
- Kotlin 的基本概念,包括型別、運算子和迴圈
本程式碼研究室適用於熟悉物件導向語言,且想進一步瞭解 Kotlin 的程式設計師。
課程內容
- 如何在 IntelliJ IDEA 中使用
main()函式和引數建立程式 - 如何使用預設值和精簡函式
- 如何為清單套用篩選條件
- 如何建立基本 lambda 和高階函式
學習內容
- 使用 REPL 試用一些程式碼。
- 使用 IntelliJ IDEA 建立基本 Kotlin 程式。
在這項工作中,您將建立 Kotlin 程式並瞭解 main() 函式,以及如何從指令列將引數傳遞至程式。
您可能還記得在先前的程式碼實驗室中,您在 REPL 中輸入的 printHello() 函式:
fun printHello() {
println ("Hello World")
}
printHello()⇒ Hello World
您可以使用 fun 關鍵字定義函式,後面加上函式名稱。與其他程式設計語言一樣,括號 () 用於函式引數 (如有)。大括號 {} 會框住函式的程式碼。這個函式不會傳回任何內容,因此沒有傳回類型。
步驟 1:建立 Kotlin 檔案
- 開啟 IntelliJ IDEA。
- IntelliJ IDEA 左側的「Project」窗格會顯示專案檔案和資料夾清單。找到「Hello Kotlin」底下的「src」src資料夾,然後按一下滑鼠右鍵。(您應該已在上一個程式碼研究室中建立「Hello Kotlin」專案)。
- 依序選取「New」>「Kotlin File / Class」。
- 將「Kind」保留為「File」,並將檔案命名為「Hello」。
- 按一下「確定」。
現在 src 資料夾中有名為 Hello.kt 的檔案。

步驟 2:新增程式碼並執行程式
- 與其他語言一樣,Kotlin 的
main()函式會指定執行作業的進入點。任何指令列引數都會以字串陣列的形式傳遞。
在 Hello.kt 檔案中輸入或貼上下列程式碼:
fun main(args: Array<String>) {
println("Hello, world!")
}與先前的 printHello() 函式一樣,這個函式沒有 return 陳述式。Kotlin 中的每個函式都會傳回某些內容,即使未明確指定也是如此。因此,像 main() 函式這樣的函式會傳回 kotlin.Unit 型別,這是 Kotlin 表示沒有值的方式。
- 如要執行程式,請按一下「
main()」函式左側的綠色三角形。從選單中選取「Run 'HelloKt'」。 - IntelliJ IDEA 會編譯並執行程式。結果會顯示在底部的記錄窗格中,如下所示。

步驟 3:將引數傳遞至 main()
由於您是從 IntelliJ IDEA 執行程式,而不是從指令列執行,因此需要以稍微不同的方式指定程式的任何引數。
- 依序選取「Run」>「Edit Configurations」。「Run/Debug Configurations」視窗隨即開啟。
- 在「程式引數」欄位中輸入
Kotlin!。 - 按一下「確定」。

步驟 4:變更程式碼以使用字串範本
字串範本會在字串中插入變數或運算式,而 $ 則會指定字串的哪個部分是變數或運算式。大括號 {} 會框住運算式 (如有)。
- 在 Hello.kt 中,將問候訊息變更為使用傳遞至程式的第一個引數
args[0],而非"world"。
fun main(args: Array<String>) {
println("Hello, ${args[0]}")
}- 執行程式,輸出內容會包含您指定的引數。
⇒ Hello, Kotlin!
在本工作中,您將瞭解 Kotlin 中幾乎所有項目都有值的原因,以及這項特點的實用性。
其他語言則有陳述式,也就是沒有值的程式碼行。在 Kotlin 中,幾乎所有內容都是運算式,而且都有值,即使該值為 kotlin.Unit 也不例外。
- 在 Hello.kt 中,於
main()內編寫程式碼,將println()指派給名為isUnit的變數,然後列印該變數。(println()不會傳回值,因此會傳回kotlin.Unit)。
// Will assign kotlin.Unit
val isUnit = println("This is an expression")
println(isUnit)- 執行程式。第一個
println()會列印字串"This is an expression"。第二個println()會列印第一個println()陳述式的值,也就是kotlin.Unit。
⇒ This is an expression kotlin.Unit
- 宣告名為
temperature的val,並將其初始化為 10。 - 宣告另一個名為
isHot的val,並將if/else陳述式的回傳值指派給isHot,如下列程式碼所示。由於這是運算式,因此您可以立即使用if運算式的值。
val temperature = 10
val isHot = if (temperature > 50) true else false
println(isHot)⇒ false
- 在字串範本中使用運算式的值。新增一些程式碼來檢查溫度,判斷魚是否安全或過熱,然後執行程式。
val temperature = 10
val message = "The water temperature is ${ if (temperature > 50) "too warm" else "OK" }."
println(message)⇒ The water temperature is OK.
在本工作中,您將進一步瞭解 Kotlin 中的函式,以及非常實用的 when 條件運算式。
步驟 1:建立一些函式
在這個步驟中,您將整合所學內容,並建立不同類型的函式。您可以將 Hello.kt 的內容替換為這段新程式碼。
- 編寫名為
feedTheFish()的函式,呼叫randomDay()取得隨機星期幾。使用字串範本,列印當天魚要吃的food。目前魚每天吃的食物都一樣。
fun feedTheFish() {
val day = randomDay()
val food = "pellets"
println ("Today is $day and the fish eat $food")
}
fun main(args: Array<String>) {
feedTheFish()
}- 編寫
randomDay()函式,從陣列中隨機挑選一天並傳回。
nextInt() 函式會採用整數限制,將 Random() 中的數字限制為 0 到 6,以符合 week 陣列。
fun randomDay() : String {
val week = arrayOf ("Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday")
return week[Random().nextInt(week.size)]
}Random()和nextInt()函式是在java.util.*中定義。在檔案頂端新增所需匯入項目:
import java.util.* // required import- 執行程式,並檢查輸出內容。
⇒ Today is Tuesday and the fish eat pellets
步驟 2:使用 when 運算式
進一步延伸,使用 when 運算式變更程式碼,在不同天選擇不同食物。when 陳述式類似於其他程式設計語言中的 switch,但 when 會在每個分支結尾自動中斷。如果您要檢查列舉,這項功能也會確保程式碼涵蓋所有分支。
- 在 Hello.kt 中,新增名為
fishFood()的函式,該函式會將某天做為String,並以String形式傳回當天的魚食。請使用when(),確保每天餵魚的食物都不一樣。執行程式幾次,看看不同的輸出內容。
fun fishFood (day : String) : String {
var food = ""
when (day) {
"Monday" -> food = "flakes"
"Tuesday" -> food = "pellets"
"Wednesday" -> food = "redworms"
"Thursday" -> food = "granules"
"Friday" -> food = "mosquitoes"
"Saturday" -> food = "lettuce"
"Sunday" -> food = "plankton"
}
return food
}
fun feedTheFish() {
val day = randomDay()
val food = fishFood(day)
println ("Today is $day and the fish eat $food")
}⇒ Today is Thursday and the fish eat granules
- 使用
else將預設分支版本新增至when運算式。如要進行測試,請移除Tuesday和Saturday分支,確保程式有時會採用預設值。
預設分支可確保food在傳回前取得值,因此不再需要初始化。由於程式碼現在只會將字串指派給food一次,因此您可以使用val(而非var) 宣告food。
fun fishFood (day : String) : String {
val food : String
when (day) {
"Monday" -> food = "flakes"
"Wednesday" -> food = "redworms"
"Thursday" -> food = "granules"
"Friday" -> food = "mosquitoes"
"Sunday" -> food = "plankton"
else -> food = "nothing"
}
return food
}- 由於每個運算式都有值,因此您可以讓這段程式碼更簡潔。直接傳回
when運算式的值,並排除food變數。when運算式的值是滿足條件的分支的最後一個運算式的值。
fun fishFood (day : String) : String {
return when (day) {
"Monday" -> "flakes"
"Wednesday" -> "redworms"
"Thursday" -> "granules"
"Friday" -> "mosquitoes"
"Sunday" -> "plankton"
else -> "nothing"
}
}最終版本的程式碼如下所示。
import java.util.* // required import
fun randomDay() : String {
val week = arrayOf ("Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday")
return week[Random().nextInt(week.size)]
}
fun fishFood (day : String) : String {
return when (day) {
"Monday" -> "flakes"
"Wednesday" -> "redworms"
"Thursday" -> "granules"
"Friday" -> "mosquitoes"
"Sunday" -> "plankton"
else -> "nothing"
}
}
fun feedTheFish() {
val day = randomDay()
val food = fishFood(day)
println ("Today is $day and the fish eat $food")
}
fun main(args: Array<String>) {
feedTheFish()
}在這項工作中,您將瞭解函式和方法的預設值。您也會瞭解精簡函式,這類函式可讓程式碼更簡潔易讀,並減少測試的程式碼路徑數量。緊湊函式也稱為單一運算式函式。
步驟 1:為參數建立預設值
在 Kotlin 中,您可以依參數名稱傳遞引數。您也可以為參數指定預設值:如果呼叫端未提供引數,系統就會使用預設值。稍後撰寫方法 (成員函式) 時,您就能避免撰寫大量相同方法的過載版本。
- 在 Hello.kt 中,編寫
swim()函式,並加入名為speed的String參數,用來列印魚的速度。speed參數的預設值為"fast"。
fun swim(speed: String = "fast") {
println("swimming $speed")
}- 從
main()函式呼叫swim()函式,有三種方式。首先,請使用預設值呼叫函式。然後呼叫函式並傳遞不含名稱的speed參數,接著呼叫函式並命名speed參數。
swim() // uses default speed
swim("slow") // positional argument
swim(speed="turtle-like") // named parameter⇒ swimming fast swimming slow swimming turtle-like
步驟 2:新增必要參數
如果未指定參數的預設值,就必須一律傳遞相應的引數。
- 在 Hello.kt 中,編寫
shouldChangeWater()函式,其中包含三個參數:day、temperature和dirty層級。如果應該換水 (星期日、水溫過高或水質太髒),函式會傳回true。必須提供星期幾,但預設溫度為 22,預設髒污程度為 20。
使用不含引數的when運算式,在 Kotlin 中這會做為一系列的if/else if檢查。
fun shouldChangeWater (day: String, temperature: Int = 22, dirty: Int = 20): Boolean {
return when {
temperature > 30 -> true
dirty > 30 -> true
day == "Sunday" -> true
else -> false
}
}- 從
feedTheFish()呼叫shouldChangeWater(),並提供日期。day參數沒有預設值,因此您必須指定引數。shouldChangeWater()的另外兩個參數都有預設值,因此您不必為這些參數傳遞引數。
fun feedTheFish() {
val day = randomDay()
val food = fishFood(day)
println ("Today is $day and the fish eat $food")
println("Change water: ${shouldChangeWater(day)}")
}=> Today is Thursday and the fish eat granules Change water: false
步驟 3:建立精簡函式
您在上一個步驟中編寫的 when 運算式,會將大量邏輯封裝到少量程式碼中。如果想稍微解壓縮,或是要檢查的條件較為複雜,可以使用一些命名良好的本機變數。但 Kotlin 的做法是使用精簡函式。
緊湊函式或單一運算式函式是 Kotlin 中的常見模式。如果函式傳回單一運算式的結果,您可以在 = 符號後指定函式主體,並省略大括號 {} 和 return。
- 在 Hello.kt 中,新增小型函式來測試條件。
fun isTooHot(temperature: Int) = temperature > 30
fun isDirty(dirty: Int) = dirty > 30
fun isSunday(day: String) = day == "Sunday"- 變更
shouldChangeWater()以呼叫新函式。
fun shouldChangeWater (day: String, temperature: Int = 22, dirty: Int = 20): Boolean {
return when {
isTooHot(temperature) -> true
isDirty(dirty) -> true
isSunday(day) -> true
else -> false
}
}- 執行程式。使用
shouldChangeWater()的println()輸出內容,應與切換為使用精簡函式前相同。
預設值
參數的預設值不一定要是值,可以是另一個函式,如下列部分範例所示:
fun shouldChangeWater (day: String, temperature: Int = 22, dirty: Int = getDirtySensorReading()): Boolean {
...在這項工作中,您將稍微瞭解 Kotlin 中的篩選器。篩選器是根據某些條件取得部分清單的實用方法。
步驟 1:建立篩選器
- 在 Hello.kt 中,使用
listOf()在頂層定義水族箱裝飾清單。您可以取代 Hello.kt 的內容。
val decorations = listOf ("rock", "pagoda", "plastic plant", "alligator", "flowerpot")- 建立新的
main()函式,並加入一行程式碼,只列印開頭為字母「p」的裝飾。篩選條件的程式碼位於大括號{}中,而it則是指篩選迴圈中的每個項目。如果運算式傳回true,就會納入該項目。
fun main() {
println( decorations.filter {it[0] == 'p'})
}- 執行程式後,您會在「Run」視窗中看到下列輸出內容:
⇒ [pagoda, plastic plant]
步驟 2:比較急切和延遲篩選器
如果您熟悉其他語言的篩選器,可能會想知道 Kotlin 中的篩選器是即時還是延遲。結果清單是立即建立,還是存取清單時才建立?在 Kotlin 中,無論您需要哪種方式,都可以達成。根據預設,filter 為「急切」狀態,每次使用篩選器時都會建立清單。
如要讓篩選器延遲,可以使用 Sequence,這個集合一次只能查看一個項目,從頭開始,然後到結尾。方便的是,這正是延遲篩選器所需的 API。
- 在 Hello.kt 中,變更程式碼,將篩選後的清單指派給名為
eager的變數,然後列印該變數。
fun main() {
val decorations = listOf ("rock", "pagoda", "plastic plant", "alligator", "flowerpot")
// eager, creates a new list
val eager = decorations.filter { it [0] == 'p' }
println("eager: " + eager)- 在該程式碼下方,使用
Sequence和asSequence()評估篩選器。將序列指派給名為filtered的變數,然後列印該變數。
// lazy, will wait until asked to evaluate
val filtered = decorations.asSequence().filter { it[0] == 'p' }
println("filtered: " + filtered)將篩選結果做為 Sequence 傳回時,filtered 變數不會保留新清單,而是會保留清單元素的 Sequence,以及要套用至這些元素的篩選器知識。每當您存取 Sequence 的元素時,系統就會套用篩選器,並將結果傳回給您。
- 將序列轉換為
List(使用toList()),強制評估序列。列印結果。
// force evaluation of the lazy list
val newList = filtered.toList()
println("new list: " + newList)- 執行程式並觀察輸出內容。
⇒ eager: [pagoda, plastic plant] filtered: kotlin.sequences.FilteringSequence@386cc1c4 new list: [pagoda, plastic plant]
如要以視覺化方式呈現 Sequence 和延遲求值的情況,請使用 map() 函式。map() 函式會對序列中的每個元素執行簡單的轉換。
- 使用與上述相同的
decorations清單,透過map()進行轉換,但不要執行任何動作,只要傳回傳遞的元素即可。新增println(),在每次存取元素時顯示,並將序列指派給名為lazyMap的變數。
val lazyMap = decorations.asSequence().map {
println("access: $it")
it
}- 列印
lazyMap、使用first()列印lazyMap的第一個元素,以及列印轉換為List的lazyMap。
println("lazy: $lazyMap")
println("-----")
println("first: ${lazyMap.first()}")
println("-----")
println("all: ${lazyMap.toList()}")- 執行程式,看看輸出結果。列印
lazyMap只會列印Sequence的參照,不會呼叫內部println()。列印第一個元素時,只會存取第一個元素。將Sequence轉換為List即可存取所有元素。
⇒ lazy: kotlin.sequences.TransformingSequence@5ba23b66 ----- access: rock first: rock ----- access: rock access: pagoda access: plastic plant access: alligator access: flowerpot all: [rock, pagoda, plastic plant, alligator, flowerpot]
- 套用
map前,請先使用原始篩選器建立新的Sequence。列印該結果。
val lazyMap2 = decorations.asSequence().filter {it[0] == 'p'}.map {
println("access: $it")
it
}
println("-----")
println("filtered: ${ lazyMap2.toList() }")- 執行程式並觀察額外的輸出內容。與取得第一個元素相同,系統只會針對存取的元素呼叫內部
println()。
⇒ ----- access: pagoda access: plastic plant filtered: [pagoda, plastic plant]
在這項工作中,您將瞭解 Kotlin 中的 lambda 和高階函式。
Lambdas
除了傳統的具名函式,Kotlin 也支援 lambda。Lambda 是用於建立函式的運算式。但這不代表您要宣告已命名函式,而是要宣告沒有名稱的函式。這項功能之所以實用,是因為 lambda 運算式現在可以做為資料傳遞。在其他語言中,lambda 稱為「匿名函式」、「函式常值」或類似名稱。
高階函式
您可以將 lambda 傳遞至其他函式,藉此建立高階函式。在先前的工作中,您建立了名為 filter 的高階函式。您已將下列 lambda 運算式傳遞至 filter,做為要檢查的條件:{it[0] == 'p'}
同樣地,map 是高階函式,而您傳遞至該函式的 lambda 則是要套用的轉換。
步驟 1:瞭解 Lambda
- 如同已命名函式,lambda 也可以有參數。如果是 lambda,參數 (和類型,如有需要) 會位於所謂的函式箭頭
->左側。要執行的程式碼會放在函式箭頭的右側。將 lambda 指派給變數後,您就可以像呼叫函式一樣呼叫 lambda。
使用 REPL (「Tools」>「Kotlin」>「Kotlin REPL」),試試這段程式碼:
var dirtyLevel = 20
val waterFilter = { dirty : Int -> dirty / 2}
println(waterFilter(dirtyLevel))⇒ 10
在這個範例中,lambda 會採用名為 dirty 的 Int,並傳回 dirty / 2。(因為過濾可去除髒污)。
- Kotlin 的函式類型語法與 lambda 語法密切相關。使用這個語法可清楚宣告保存函式的變數:
val waterFilter: (Int) -> Int = { dirty -> dirty / 2 }程式碼內容如下:
- 建立名為
waterFilter的變數。 waterFilter可以是任何函式,該函式會採用Int並傳回Int。- 將 lambda 指派給
waterFilter。 - lambda 會傳回引數
dirty除以 2 的值。
請注意,您不必再指定 lambda 引數的型別。系統會透過型別推論計算型別。
步驟 2:建立高階函式
到目前為止,lambda 的範例大多看起來像函式。lambda 的真正強大之處在於用來建立高階函式,其中一個函式的引數是另一個函式。
- 編寫高階函式。以下是基本範例,這個函式會採用兩個引數。第一個引數是整數。第二個引數是會接收整數並傳回整數的函式。在 REPL 中試試看。
fun updateDirty(dirty: Int, operation: (Int) -> Int): Int {
return operation(dirty)
}程式碼主體會呼叫做為第二個引數傳遞的函式,並將第一個引數一併傳遞給該函式。
- 如要呼叫這個函式,請傳遞整數和函式。
val waterFilter: (Int) -> Int = { dirty -> dirty / 2 }
println(updateDirty(30, waterFilter))⇒ 15
您傳遞的函式不一定要是 lambda,也可以是正規的具名函式。如要將引數指定為一般函式,請使用 :: 運算子。這樣 Kotlin 就會知道您要傳遞函式參照做為引數,而不是嘗試呼叫函式。
- 請嘗試將一般具名函式傳遞至
updateDirty()。
fun increaseDirty( start: Int ) = start + 1
println(updateDirty(15, ::increaseDirty))⇒ 16
var dirtyLevel = 19;
dirtyLevel = updateDirty(dirtyLevel) { dirtyLevel -> dirtyLevel + 23}
println(dirtyLevel)⇒ 42
- 如要在 IntelliJ IDEA 中建立 Kotlin 來源檔案,請先建立 Kotlin 專案。
- 如要在 IntelliJ IDEA 中編譯及執行程式,請按一下
main()函式旁邊的綠色三角形。輸出內容會顯示在下方的記錄視窗中。 - 在 IntelliJ IDEA 中,依序前往「Run」>「Edit Configurations」,指定要傳遞至
main()函式的命令列引數。 - Kotlin 中幾乎所有內容都有值。您可以利用這項事實,將
if或when的值做為運算式或傳回值,讓程式碼更加簡潔。 - 預設引數可讓您不必建立多個版本的函式或方法。例如:
fun swim(speed: String = "fast") { ... } - 精簡函式或單一運算式函式可讓程式碼更易於閱讀。例如:
fun isTooHot(temperature: Int) = temperature > 30 - 您已瞭解使用 lambda 運算式的篩選器基本概念。例如:
val beginsWithP = decorations.filter { it [0] == 'p' } - Lambda 運算式是一種運算式,可建立未命名的函式。lambda 運算式定義於大括號
{}之間。 - 在高階函式中,您會將函式 (例如 lambda 運算式) 做為資料傳遞至其他函式。例如:
dirtyLevel = updateDirty(dirtyLevel) { dirtyLevel -> dirtyLevel + 23}
本課程內容豐富,如果您是 Lambda 新手,更是如此。後續課程會再次介紹 lambda 和高階函式。
Kotlin 說明文件
如要進一步瞭解本課程的任何主題,或遇到問題,請前往 https://kotlinlang.org。
Kotlin 教學課程
https://try.kotlinlang.org 網站提供豐富的教學課程 (稱為 Kotlin Koans)、網頁式解譯器,以及附有範例的完整參考文件。
Udacity 課程
如要查看這個主題的 Udacity 課程,請參閱「程式設計人員的 Kotlin 新手上路課程」。
IntelliJ IDEA
IntelliJ IDEA 的說明文件位於 JetBrains 網站。
本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:
- 視需要指派作業。
- 告知學員如何繳交作業。
- 為作業評分。
講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。
如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。
回答問題
第 1 題
如果字串 element 包含在呼叫的字串中,contains(element: String) 函式會傳回 true。下列程式碼的輸出內容為何?
val decorations = listOf ("rock", "pagoda", "plastic plant", "alligator", "flowerpot")
println(decorations.filter {it.contains('p')})
▢ [pagoda, plastic, plant]
▢ [pagoda, plastic plant]
▢ [pagoda, plastic plant, flowerpot]
▢ [rock, alligator]
第 2 題
在下列函式定義中,哪一個參數是必要參數?fun shouldChangeWater (day: String, temperature: Int = 22, dirty: Int = 20, numDecorations: Int = 0): Boolean {...}
▢ numDecorations
▢ dirty
▢ day
▢ temperature
第 3 題
您可以將一般具名函式 (而非呼叫函式的結果) 傳遞至其他函式。你會如何將 increaseDirty( start: Int ) = start + 1 傳遞至 updateDirty(dirty: Int, operation: (Int) -> Int)?
▢ updateDirty(15, &increaseDirty())
▢ updateDirty(15, increaseDirty())
▢ updateDirty(15, ("increaseDirty()"))
▢ updateDirty(15, ::increaseDirty)
繼續下一個課程:
如要查看課程總覽,包括其他程式碼研究室的連結,請參閱「程式設計人員的 Kotlin 新手上路課程:歡迎參加本課程。」