iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Software Development

Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app系列 第 3

程式不只照順序跑:Kotlin 條件判斷、迴圈與例外處理

  • 分享至 

  • xImage
  •  

一開賽就有說到新時代語言 Kotlin 據有變數、資料型態與 Null Safety,今天要進一步控制程式的執行方向。我們將學習 ifwhenforwhiledo-whilebreakcontinue,最後使用 try-catch-throw 處理錯誤。參考資料:https://kotlinlang.org/docs/control-flow.html


今天會學到什麼?

  • if 條件判斷
  • when 多條件判斷
  • forwhiledo-while 迴圈
  • step 間隔走訪
  • breakcontinue
  • try-catch-finally 例外處理
  • 使用 throw 主動拋出例外
  • toIntOrNull() 與 Elvis 運算子 ?:

參考資料:https://kotlinlang.org/docs/control-flow.html


一、使用 if 判斷條件

if 是最基本的條件判斷。當條件成立時執行第一個區塊,不成立時則執行 else 區塊。Kotlin 的 if 同時也是運算式,因此可以直接產生一個結果並指派給變數。參考資料:https://kotlinlang.org/docs/control-flow.html#if-expression

fun main() {

    print("請輸入成績:")

    // 使用 toIntOrNull() 安全轉換成整數。
    // 如果輸入無法轉換,使用 Elvis 運算子 ?: 給予預設值 0。
    val score = readln().toIntOrNull() ?: 0

    val result = if (score >= 60) {
        "及格"
    } else {
        "不及格"
    }

    println("成績:$score")
    println("結果:$result")
}

輸入 80 時,執行結果如下:

請輸入成績:80
成績:80
結果:及格

二、使用 when 處理多個條件

如果需要判斷很多種情況,連續寫很多組 if-else if 會降低可讀性。Kotlin 提供 when,可以依照數值、字串、範圍或其他條件選擇要執行的分支。參考資料:https://kotlinlang.org/docs/control-flow.html#when-expressions-and-statements

1. 判斷單一數值

fun main() {

    print("請輸入 1~11 的整數:")
    val value = readln().toIntOrNull() ?: 0

    when (value) {
        1 -> println("A")
        2 -> println("B")

        // 同一個分支可以放入多個值。
        3, 4 -> println("E")

        // in 可以判斷數值是否位於指定範圍。
        in 6..10 -> println("F~J")

        11 -> println("K")

        // 前面的條件皆不成立時執行 else。
        else -> println("輸入不在指定範圍內")
    }
}

2. 使用 when 判斷成績

when 不只可以執行程式,也能直接回傳結果。將判斷結果指派給變數,可以減少重複的 println()。參考資料:https://kotlinlang.org/docs/control-flow.html#when-expressions-and-statements

fun main() {

    print("請輸入成績:")
    val score = readln().toIntOrNull() ?: -1

    val scoreStatus = when (score) {
        in 0..59 -> "不及格"
        in 60..100 -> "及格"
        else -> "輸入錯誤"
    }

    println("成績狀態:$scoreStatus")
}

3. 使用 when 判斷水果價格

字串也可以放進 when。使用 lowercase() 將輸入統一轉成小寫,可避免使用者輸入 AppleAPPLE 時比對失敗。參考資料:https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.text/lowercase.html

fun main() {

    print("請輸入水果英文名稱:")
    val fruitName = readln()

    val price = when (fruitName.lowercase()) {
        "apple" -> 100
        "pear" -> 150
        "pineapple" -> 80
        else -> 50
    }

    println("水果:$fruitName")
    println("價格:$$price")
}

注意 Kotlin 字串中的 $ 代表字串模板開頭。如果要輸出貨幣符號 $,可以寫成 \$;若 $ 後接的是變數,也可以利用字串模板組合輸出。參考資料:https://kotlinlang.org/docs/strings.html#string-templates


三、不指定判斷值的 when

when 後面也可以不放變數,直接在每個分支撰寫布林條件。這種寫法很適合處理分數、生命值、溫度及年齡區間。參考資料:https://kotlinlang.org/docs/control-flow.html#when-expressions-and-statements

fun main() {

    print("請輸入生命值:")

    val healthPoint = readln().toIntOrNull() ?: 0
    val isBlessed = true

    val state = when {
        healthPoint == 100 -> {
            "狀態良好"
        }

        healthPoint >= 90 -> {
            "只有一些擦傷"
        }

        healthPoint >= 75 -> {
            if (isBlessed) {
                "受到祝福保護"
            } else {
                "受到輕傷"
            }
        }

        healthPoint >= 15 -> {
            "傷勢嚴重"
        }

        else -> {
            "生命垂危"
        }
    }

    println("生命狀態:$state")
}

when 會由上往下檢查條件,找到第一個成立的分支後便停止,所以範圍判斷通常應從較大的數值開始排列。參考資料:https://kotlinlang.org/docs/control-flow.html#when-expressions-and-statements


四、for 迴圈

當程式需要重複執行某件事時,可以使用迴圈。for 適合走訪數值範圍、字串、陣列或集合。參考資料:https://kotlinlang.org/docs/control-flow.html#for-loops

1. 印出 1 到 10

fun main() {

    for (number in 1..10) {
        println(number)
    }
}

1..10 代表包含 1 和 10 的封閉區間,因此程式會依序印出 1~10。參考資料:https://kotlinlang.org/docs/ranges.html


2. 使用 step 控制間隔

step 可以設定每次遞增的距離。以下程式從 1 開始,每次增加 2,因此會取得 1、3、5、7、9。參考資料:https://kotlinlang.org/docs/ranges.html#progression

fun main() {

    val numberRange = 1..10
    var sum = 0

    // 從 1 開始,每次增加 2,只會取得奇數。
    for (number in numberRange step 2) {
        sum += number
    }

    println("1 到 10 的奇數總和:$sum")
}

執行結果:

1 到 10 的奇數總和:25

3. 九九乘法表

巢狀迴圈就是在一個迴圈裡再放入另一個迴圈。外層控制被乘數,內層控制乘數,即可產生完整的九九乘法表。參考資料:https://kotlinlang.org/docs/control-flow.html#for-loops

fun main() {

    printMultiplicationTable()
}

/**
 * 印出完整的九九乘法表。
 */
fun printMultiplicationTable() {

    // 外層迴圈控制第一個數字。
    for (i in 1..9) {

        // 內層迴圈控制第二個數字。
        for (j in 1..9) {
            println("$i × $j = ${i * j}")
        }
    }
}

部分執行結果:

1 × 1 = 1
1 × 2 = 2
1 × 3 = 3
1 × 4 = 4
1 × 5 = 5
1 × 6 = 6
1 × 7 = 7
1 × 8 = 8
1 × 9 = 9
2 × 1 = 2
2 × 2 = 4
2 × 3 = 6

五、while 迴圈:猜數字遊戲

while 會先判斷條件;條件成立才執行迴圈內容。如果條件一開始就不成立,迴圈可能一次也不會執行。參考資料:https://kotlinlang.org/docs/control-flow.html#while-loops

下面製作一個 1~10 的猜數字遊戲,玩家最多可以猜 5 次:

fun main() {

    guessNumber()
}

/**
 * 1~10 猜數字遊戲。
 * 玩家最多可以猜 5 次。
 */
fun guessNumber() {

    // random() 會從 1..10 中隨機取出一個整數。
    val answer = (1..10).random()

    var guess = -1
    var attempt = 1

    // 答案尚未猜中,而且次數不超過 5 次時繼續執行。
    while (guess != answer && attempt <= 5) {

        print("第 $attempt 次,請猜一個 1~10 的數字:")

        // 如果輸入不是整數,先使用 -1 代替。
        guess = readln().toIntOrNull() ?: -1

        when {
            guess !in 1..10 -> {
                println("請輸入 1~10 之間的整數!")
            }

            guess < answer -> {
                println("太小了!")
            }

            guess > answer -> {
                println("太大了!")
            }

            else -> {
                println("恭喜你猜對了!")
            }
        }

        attempt++
    }

    // 迴圈結束後仍未猜中,公布答案。
    if (guess != answer) {
        println("挑戰失敗,正確答案是:$answer")
    }
}

六、do-while 迴圈

do-whilewhile 最大的差異是:do-while 會先執行一次程式,再檢查條件,所以無論條件是否成立,內容至少會執行一次。參考資料:https://kotlinlang.org/docs/control-flow.html#while-loops

以下使用 do-while 計算 1~10 的總和:

fun main() {

    var value = 1
    var sum = 0

    do {
        sum += value
        value++
    } while (value <= 10)

    println("1 到 10 的總和:$sum")
}

執行結果:

1 到 10 的總和:55

七、break 與 continue

breakcontinue 都可以改變迴圈的執行流程,但用途不同。break 會立刻結束整個迴圈;continue 則只跳過目前這一次,接著執行下一次迴圈。參考資料:https://kotlinlang.org/docs/returns.html#break-and-continue-labels

1. break:直接結束迴圈

fun main() {

    print("請輸入停止數字(1~10):")
    val stopNumber = readln().toIntOrNull() ?: 0

    var sum = 0

    for (number in 1..10) {

        if (number == stopNumber) {
            println("遇到 $stopNumber,結束迴圈!")
            break
        }

        sum += number
        println("目前數字:$number,目前總和:$sum")
    }

    println("最後總和:$sum")
}

如果輸入 5,程式只會加總 1+2+3+4,遇到 5 時立即離開迴圈。參考資料:https://kotlinlang.org/docs/returns.html#break-and-continue-labels


2. continue:跳過本次迴圈

fun main() {

    print("請輸入要跳過的數字(1~10):")
    val skipNumber = readln().toIntOrNull() ?: 0

    var sum = 0

    for (number in 1..10) {

        if (number == skipNumber) {
            println("跳過數字:$skipNumber")
            continue
        }

        sum += number
        println("目前數字:$number,目前總和:$sum")
    }

    println("最後總和:$sum")
}

如果輸入 5,程式會略過 5,但仍會繼續處理 6~10。參考資料:https://kotlinlang.org/docs/returns.html#break-and-continue-labels


八、為什麼需要例外處理?

使用 readln().toInt() 將文字轉成整數時,如果使用者輸入 ABC,程式便會產生 NumberFormatException 並中斷。可預期的錯誤應該適當處理,避免整個 App 因單一錯誤而停止。參考資料:https://kotlinlang.org/docs/exceptions.html

以下程式沒有例外處理:

fun main() {

    print("請輸入一個整數:")

    // 如果輸入 ABC,這一行會拋出 NumberFormatException。
    val number = readln().toInt()

    println("你輸入了:$number")
}

九、try-catch-finally

try 放置可能產生例外的程式;catch 負責接住並處理例外;finally 則無論有沒有發生例外都會執行,常用來關閉檔案、資料庫或網路連線等資源。參考資料:https://kotlinlang.org/docs/exceptions.html#handle-exceptions-using-try-catch-blocks

fun main() {

    print("請輸入一個整數:")

    try {
        val number = readln().toInt()
        println("你輸入了:$number")

    } catch (e: NumberFormatException) {
        // 只捕捉數字格式錯誤,比直接捕捉 Exception 更明確。
        println("輸入錯誤,請輸入有效的整數!")

    } finally {
        // 不論成功或失敗都會執行。
        println("程式執行結束。")
    }
}

輸入正確整數:

請輸入一個整數:100
你輸入了:100
程式執行結束。

輸入非數字文字:

請輸入一個整數:ABC
輸入錯誤,請輸入有效的整數!
程式執行結束。

十、使用 throw 主動拋出例外

除了系統自動產生的例外,我們也可以使用 throw 主動拋出例外。例如名字不可空白,也不可包含數字或符號,只要資料不符合規則就中止正常流程並交給 catch 處理。參考資料:https://kotlinlang.org/docs/exceptions.html#throw-exceptions-with-the-throw-expression

Kotlin try-catch-throw 執行流程圖

發布到 iThome 前,請先將上面的流程圖上傳至 iThome 圖片空間,再換成平台產生的圖片網址。

完整程式如下:

fun main() {

    print("請輸入你的名字:")
    val name = readln()

    try {
        // readln() 回傳非空字串,因此這裡檢查是否為空字串即可。
        if (name.isEmpty()) {
            throw IllegalArgumentException("名字不能為空!")
        }

        // 逐一檢查名字中的每一個字元。
        name.forEach { character ->

            // 只要找到不是字母的字元,就主動拋出例外。
            if (!character.isLetter()) {
                throw IllegalArgumentException("名字只能包含字母!")
            }
        }

        // 所有檢查皆通過才會執行。
        println("你好,$name!")

    } catch (e: IllegalArgumentException) {
        // 顯示 throw 所設定的例外訊息。
        println("錯誤:${e.message}")
    }
}

這一版將原本的 Exception 改成 IllegalArgumentException,因為發生錯誤的原因是「傳入資料不符合要求」,使用更明確的例外型別有助於閱讀及維護程式。參考資料:https://kotlinlang.org/docs/exceptions.html#create-custom-exceptions


流程說明

程式會依照下列順序執行:

  1. 讀取使用者輸入的名字。
  2. 進入 try 區塊。
  3. 如果名字為空,使用 throw 拋出例外。
  4. 使用 forEach 逐一檢查每個字元。
  5. 如果出現數字或符號,拋出例外。
  6. 如果全部都是字母,顯示問候訊息。
  7. 發生例外時,由 catch 顯示錯誤原因。

參考資料:https://kotlinlang.org/docs/exceptions.html

輸入正確名字:

請輸入你的名字:Alex
你好,Alex!

輸入含有數字的名字:

請輸入你的名字:Alex123
錯誤:名字只能包含字母!

直接按下 Enter:

請輸入你的名字:
錯誤:名字不能為空!

十一、Null Safety 與例外處理

Kotlin 使用可為空型別來降低 NullPointerException 發生的機會。在型別後加上 ?,代表該變數允許儲存 null。參考資料:https://kotlinlang.org/docs/null-safety.html

fun main() {

    val name: String? = null

    // 安全呼叫運算子 ?.:
    // name 為 null 時,不會呼叫 length,而是回傳 null。
    val nullableLength: Int? = name?.length

    println("nullableLength:$nullableLength")

    // Elvis 運算子 ?::
    // 左邊為 null 時,改用右邊的預設值 0。
    val safeLength: Int = name?.length ?: 0

    println("safeLength:$safeLength")
}

執行結果:

nullableLength:null
safeLength:0

不建議濫用 !!

!! 會強制將可為空型別視為不可為空。當變數實際上是 null 時,程式會拋出 NullPointerException,所以只有在完全確定資料不可能為空時才能使用。參考資料:https://kotlinlang.org/docs/null-safety.html#not-null-assertion-operator

fun main() {

    val text: String? = null

    try {
        // text 是 null,使用 !! 會產生 NullPointerException。
        val length = text!!.length

        println("字串長度:$length")

    } catch (e: NullPointerException) {
        println("發生錯誤:字串目前是 null")
    }
}

更安全的寫法是使用 ?. 搭配 ?:

fun main() {

    val text: String? = null

    // text 為 null 時,直接將長度設為 0。
    val length = text?.length ?: 0

    println("字串長度:$length")
}

十二、toInt() 與 toIntOrNull() 的差異

toInt() 轉換失敗時會拋出 NumberFormatExceptiontoIntOrNull() 轉換失敗時則回傳 null。如果只是接收使用者輸入,通常使用 toIntOrNull() 會更安全。參考資料:https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.text/to-int-or-null.html

fun main() {

    print("請輸入年齡:")

    // 輸入無法轉成整數時得到 null,再使用 ?: 指定預設值。
    val age = readln().toIntOrNull() ?: 0

    println("你的年齡是:$age")
}

兩者適用情境如下:

寫法 轉換失敗結果 適合情境
text.toInt() 拋出例外 格式必須正確,錯誤時需要進入 catch
text.toIntOrNull() 回傳 null 使用者輸入、資料格式可能不正確
text.toIntOrNull() ?: 0 使用預設值 0 允許使用預設值繼續執行

參考資料:https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.text/to-int-or-null.html


十三、綜合實作:安全版猜數字遊戲

最後將今天學到的 whilewhen、範圍、continuebreakthrowtry-catch 組合成完整的猜數字遊戲。參考資料:https://kotlinlang.org/docs/control-flow.html

fun main() {

    val answer = (1..10).random()
    val maximumAttempts = 5

    var attempt = 1
    var isCorrect = false

    println("歡迎來到 Kotlin 猜數字遊戲!")
    println("請猜一個 1~10 的整數,你共有 $maximumAttempts 次機會。")

    while (attempt <= maximumAttempts) {

        print("第 $attempt 次:")

        try {
            val input = readln()

            // toIntOrNull() 轉換失敗時,主動拋出例外。
            val guess = input.toIntOrNull()
                ?: throw IllegalArgumentException("請輸入整數!")

            // 檢查輸入是否位於允許的範圍。
            if (guess !in 1..10) {
                throw IllegalArgumentException("數字必須介於 1~10!")
            }

            when {
                guess < answer -> {
                    println("太小了!")
                }

                guess > answer -> {
                    println("太大了!")
                }

                else -> {
                    println("恭喜你猜對了,答案就是 $answer!")
                    isCorrect = true

                    // 猜中後立即離開 while 迴圈。
                    break
                }
            }

            // 只有有效的猜測才計算次數。
            attempt++

        } catch (e: IllegalArgumentException) {
            println("輸入錯誤:${e.message}")

            // 錯誤輸入不計算次數,直接重新執行迴圈。
            continue
        }
    }

    if (!isCorrect) {
        println("挑戰失敗,正確答案是 $answer。")
    }

    println("遊戲結束。")
}

今日重點整理

今天我們開始讓 Kotlin 程式具備判斷、重複執行及處理錯誤的能力:

  • if 適合處理簡單的兩種結果。
  • when 適合處理多個值或範圍。
  • for 適合走訪範圍及集合。
  • while 會先判斷條件再執行。
  • do-while 至少會執行一次。
  • break 會結束整個迴圈。
  • continue 只會跳過目前這一次迴圈。
  • try 放置可能出錯的程式。
  • catch 負責接住並處理例外。
  • finally 無論成功或失敗都會執行。
  • throw 可以主動拋出例外。
  • toIntOrNull() 能降低輸入格式錯誤造成的程式中斷。
  • ?.?: 通常比直接使用 !! 更安全。

參考資料:https://kotlinlang.org/docs/control-flow.html


明日預告

下一篇將進入 Kotlin 函式與 Lambda,介紹函式參數、回傳值、預設參數、命名參數、單一運算式函式、匿名函式,以及如何把 Lambda 當成參數傳給其他函式。參考資料:https://kotlinlang.org/docs/functions.html


官方參考資料


上一篇
工欲善其事,必先利其器-打造 Android studio 開發環境
下一篇
Kotlin Lambda 完全入門:從一般函式、匿名函式到高階函式
系列文
Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言