例外與錯誤處理
例外有助於讓您的程式碼執行更加可預測,即使發生可能中斷程式執行的執行階段錯誤也是如此。 Kotlin 預設將所有例外視為 非受檢 (unchecked)。 非受檢例外簡化了例外處理程序:您可以擷取例外,但不需明確地處理或宣告它們。
若要進一步了解 Kotlin 在與 Java、Swift 及 Objective-C 互動時如何處理例外,請參閱 與 Java、Swift 及 Objective-C 的例外互通性章節。
處理例外包含兩個主要操作:
- 拋出例外 (Throwing exceptions): 指出問題何時發生。
- 擷取例外 (Catching exceptions): 透過解決問題或通知開發者或應用程式使用者,手動處理非預期的例外。
例外由 Exception 類別的子類別表示,而該類別又是 Throwable 類別的子類別。有關階層結構的更多資訊,請參閱例外階層結構章節。由於 Exception 是一個 open class,您可以建立自訂例外以符合您應用程式的特定需求。
拋出例外
您可以使用 throw 關鍵字手動拋出例外。 拋出例外表示程式碼中發生了非預期的執行階段錯誤。 例外是物件,拋出例外會建立例外類別的一個執行個體。
您可以拋出不帶任何參數的例外:
throw IllegalArgumentException()為了更清楚了解問題的來源,可以包含額外的資訊,例如自訂訊息和原始原因:
val cause = IllegalStateException("Original cause: illegal state")
// 如果 userInput 為負數,則拋出 IllegalArgumentException
// 此外,它還會顯示由 cause IllegalStateException 表示的原始原因
if (userInput < 0) {
throw IllegalArgumentException("Input must be non-negative", cause)
}在此範例中,當使用者輸入負值時,會拋出 IllegalArgumentException。 您可以建立自訂錯誤訊息並保留例外的原始原因 (cause), 這將會包含在堆疊追蹤中。
使用前置條件函式拋出例外
Kotlin 提供了使用前置條件函式 (precondition functions) 自動拋出例外的其他方式。 前置條件函式包括:
| 前置條件函式 | 使用案例 | 拋出的例外 |
|---|---|---|
require() | 檢查使用者輸入的有效性 | IllegalArgumentException |
check() | 檢查物件或變數狀態的有效性 | IllegalStateException |
error() | 表示不合法的狀態或條件 | IllegalStateException |
這些函式適用於如果未滿足特定條件則程式流程無法繼續的情況。 這可以簡化您的程式碼並使處理這些檢查變得有效率。
require() 函式
當輸入引數對於函式的運作至關重要,且如果這些引數無效則函式無法繼續執行時,請使用 require() 函式來驗證輸入引數。
如果未滿足 require() 中的條件,它會拋出 IllegalArgumentException:
fun getIndices(count: Int): List<Int> {
require(count >= 0) { "Count must be non-negative. You set count to $count." }
return List(count) { it + 1 }
}
fun main() {
// 這會失敗並拋出 IllegalArgumentException
println(getIndices(-1))
// 取消註解下面這行以查看運作範例
// println(getIndices(3))
// [1, 2, 3]
}
require()函式允許編譯器執行智慧轉型。 在成功檢查後,變數會自動轉型為不可為 null 的型別。 這些函式常用於可 null 性檢查,以確保變數在繼續執行前不是 null。例如:kotlinfun printNonNullString(str: String?) { // 可 null 性檢查 require(str != null) // 在此成功檢查後,保證 'str' 不為 null // 並自動智慧轉型為不可為 null 的 String println(str.length) }
check() 函式
使用 check() 函式來驗證物件或變數的狀態。 如果檢查失敗,則表示需要解決的邏輯錯誤。
如果 check() 函式中指定的條件為 false,它會拋出 IllegalStateException:
fun main() {
var someState: String? = null
fun getStateValue(): String {
val state = checkNotNull(someState) { "State must be set beforehand!" }
check(state.isNotEmpty()) { "State must be non-empty!" }
return state
}
// 如果您取消註解下面這行,程式將因 IllegalStateException 而失敗
// getStateValue()
someState = ""
// 如果您取消註解下面這行,程式將因 IllegalStateException 而失敗
// getStateValue()
someState = "non-empty-state"
// 這會列印 "non-empty-state"
println(getStateValue())
}
check()函式允許編譯器執行智慧轉型。 在成功檢查後,變數會自動轉型為不可為 null 的型別。 這些函式常用於可 null 性檢查,以確保變數在繼續執行前不是 null。例如:kotlinfun printNonNullString(str: String?) { // 可 null 性檢查 check(str != null) // 在此成功檢查後,保證 'str' 不為 null // 並自動智慧轉型為不可為 null 的 String println(str.length) }
error() 函式
error() 函式用於發出不合法狀態或程式碼中邏輯上不應發生之條件的訊號。 這適用於您想要在程式碼中刻意拋出例外的場景,例如當程式碼遇到非預期的狀態時。 此函式在 when 運算式中特別有用,提供了一種清晰的方式來處理邏輯上不應發生的情況。
在下列範例中,error() 函式用於處理未定義的使用者角色。 如果角色不是預定義的角色之一,則會拋出 IllegalStateException:
class User(val name: String, val role: String)
fun processUserRole(user: User) {
when (user.role) {
"admin" -> println("${user.name} is an admin.")
"editor" -> println("${user.name} is an editor.")
"viewer" -> println("${user.name} is a viewer.")
else -> error("Undefined role: ${user.role}")
}
}
fun main() {
// 這會按預期運作
val user1 = User("Alice", "admin")
processUserRole(user1)
// Alice is an admin.
// 這會拋出 IllegalStateException
val user2 = User("Bob", "guest")
processUserRole(user2)
}使用 try-catch 區塊處理例外
當拋出例外時,它會中斷程式的正常執行。 您可以使用 try 和 catch 關鍵字優雅地處理例外,以保持程式穩定。 try 區塊包含可能拋出例外的程式碼,而 catch 區塊則在例外發生時擷取並處理它。 例外會由第一個與其特定型別或該例外的父類別相符的 catch 區塊所擷取。
以下是您可以將 try 和 catch 關鍵字結合使用的方法:
try {
// 可能拋出例外的程式碼
} catch (e: SomeException) {
// 處理例外的程式碼
}使用 try-catch 作為運算式是一種常見的方法,因此它可以從 try 區塊或 catch 區塊傳回值:
fun main() {
val num: Int = try {
// 如果 count() 成功完成,其傳回值將指派給 num
count()
} catch (e: ArithmeticException) {
// 如果 count() 拋出例外,catch 區塊會傳回 -1,
// 並將其指派給 num
-1
}
println("Result: $num")
}
// 模擬一個可能拋出 ArithmeticException 的函式
fun count(): Int {
// 更改此值以將不同的值傳回給 num
val a = 0
return 10 / a
}您可以為同一個 try 區塊使用多個 catch 處理常式。 您可以根據需要新增任意數量的 catch 區塊,以分別處理不同的例外。 當您有多個 catch 區塊時,請務必按照從最具體到最不具體的例外順序排列,並在程式碼中遵循從上到下的順序。 此順序與程式的執行流程一致。
請考慮這個使用自訂例外的範例:
open class WithdrawalException(message: String) : Exception(message)
class InsufficientFundsException(message: String) : WithdrawalException(message)
fun processWithdrawal(amount: Double, availableFunds: Double) {
if (amount > availableFunds) {
throw InsufficientFundsException("Insufficient funds for the withdrawal.")
}
if (amount < 1 || amount % 1 != 0.0) {
throw WithdrawalException("Invalid withdrawal amount.")
}
println("Withdrawal processed")
}
fun main() {
val availableFunds = 500.0
// 更改此值以測試不同場景
val withdrawalAmount = 500.5
try {
processWithdrawal(withdrawalAmount.toDouble(), availableFunds)
// catch 區塊的順序很重要!
} catch (e: InsufficientFundsException) {
println("Caught an InsufficientFundsException: ${e.message}")
} catch (e: WithdrawalException) {
println("Caught a WithdrawalException: ${e.message}")
}
}一個處理 WithdrawalException 的通用擷取區塊會擷取其型別的所有例外,包括像 InsufficientFundsException 這樣具體的例外,除非它們先前已被更具體的擷取區塊所擷取。
finally 區塊
finally 區塊包含無論 try 區塊成功完成還是拋出例外都一律執行的程式碼。 使用 finally 區塊,您可以在執行 try 和 catch 區塊後清理程式碼。 這在處理檔案或網路連線等資源時特別重要,因為 finally 保證它們會被正確關閉或釋放。
以下是您通常如何將 try-catch-finally 區塊結合使用的方法:
try {
// 可能拋出例外的程式碼
}
catch (e: YourException) {
// 例外處理常式
}
finally {
// 始終執行的程式碼
}try 運算式的傳回值是由 try 或 catch 區塊中最後執行的運算式決定的。 如果沒有發生例外,結果來自 try 區塊;如果處理了例外,則結果來自 catch 區塊。 finally 區塊一律會執行,但它不會改變 try-catch 區塊的結果。
讓我們看一個範例來示範:
fun divideOrNull(a: Int): Int {
// try 區塊始終執行
// 這裡的例外(除以零)會導致立即跳轉到 catch 區塊
try {
val b = 44 / a
println("try block: Executing division: $b")
return b
}
// 由於 ArithmeticException(如果 a == 0 則除以零)而執行 catch 區塊
catch (e: ArithmeticException) {
println("catch block: Encountered ArithmeticException $e")
return -1
}
finally {
println("finally block: The finally block is always executed")
}
}
fun main() {
// 更改此值以獲得不同結果。ArithmeticException 將傳回:-1
divideOrNull(0)
}在 Kotlin 中,管理實作了
AutoClosable介面之資源(例如FileInputStream或FileOutputStream等檔案串流)的慣用法是使用.use()函式。 此函式會在程式碼區塊完成時自動關閉資源,無論是否拋出例外,進而消除了對finally區塊的需求。 因此,Kotlin 不需要像 Java 的 try-with-resources 這樣用於資源管理的特殊語法。kotlinFileWriter("test.txt").use { writer -> writer.write("some text") // 在此區塊之後,.use 函式會自動呼叫 writer.close(),類似於 finally 區塊 }
如果您的程式碼需要資源清理而不需處理例外,您也可以將 try 與 finally 區塊配合使用,而不帶 catch 區塊:
class MockResource {
fun use() {
println("Resource being used")
// 模擬正在使用的資源
// 如果發生除以零,這會拋出 ArithmeticException
val result = 100 / 0
// 如果拋出例外,則不會執行這行
println("Result: $result")
}
fun close() {
println("Resource closed")
}
}
fun main() {
val resource = MockResource()
try {
// 嘗試使用資源
resource.use()
} finally {
// 確保資源始終關閉,即使發生例外也是如此
resource.close()
}
// 如果拋出例外,則不列印此行
println("End of the program")
}如您所見,finally 區塊保證了資源會被關閉,無論是否發生例外。
在 Kotlin 中,您可以根據特定需求靈活地僅使用 catch 區塊、僅使用 finally 區塊,或者兩者都使用,但 try 區塊必須始終至少搭配一個 catch 區塊或一個 finally 區塊。
建立自訂例外
在 Kotlin 中,您可以藉由建立擴充內建 Exception 類別的類別來定義自訂例外。這允許您建立針對應用程式需求量身打造的更具體錯誤型別。
要建立一個自訂例外,您可以定義一個擴充 Exception 的類別:
class MyException: Exception("My message")在此範例中,有一個預設錯誤訊息 "My message",但如果您願意,也可以將其留空。
Kotlin 中的例外是具狀態的物件,帶有與建立時的上下文相關的資訊,稱為堆疊追蹤。 避免使用物件宣告來建立例外。 相反地,每次需要時都請建立例外的新執行個體。 這樣,您可以確保例外的狀態準確地反映特定上下文。
自訂例外也可以是任何現有例外子類別的子類別,例如 ArithmeticException 子類別:
class NumberTooLargeException: ArithmeticException("My message")如果您想要建立自訂例外的子類別,必須將父類別宣告為
open,因為 類別預設為 final,否則無法建立子類別。例如:
kotlin// 將自訂例外宣告為 open class,使其可被繼承 open class MyCustomException(message: String): Exception(message) // 建立自訂例外的子類別 class SpecificCustomException: MyCustomException("Specific error message")
自訂例外的行為與內建例外完全相同。您可以使用 throw 關鍵字拋出它們,並使用 try-catch-finally 區塊來處理它們。讓我們看一個範例來示範:
class NegativeNumberException: Exception("Parameter is less than zero.")
class NonNegativeNumberException: Exception("Parameter is a non-negative number.")
fun myFunction(number: Int) {
if (number < 0) throw NegativeNumberException()
else if (number >= 0) throw NonNegativeNumberException()
}
fun main() {
// 更改此函式中的值以獲得不同的例外
myFunction(1)
}在具有多樣化錯誤場景的應用程式中,建立例外階層結構有助於使程式碼更清晰、更具體。 您可以透過使用抽象類別或密封類別作為通用例外功能的基礎,並為詳細的例外型別建立特定子類別來達成此目的。 此外,包含具有預設值參數的自訂例外提供了靈活性,允許使用各種訊息進行初始化,從而實現更精細的錯誤處理。
讓我們看一個範例,使用密封類別 AccountException 作為例外階層結構的基礎,而子類別 APIKeyExpiredException 則展示了使用具有預設值參數來改進例外細節:
// 建立一個密封類別作為帳戶相關錯誤例外階層結構的基礎
sealed class AccountException(message: String, cause: Throwable? = null):
Exception(message, cause)
// 建立 AccountException 的子類別
class InvalidAccountCredentialsException : AccountException("Invalid account credentials detected")
// 建立 AccountException 的子類別,允許新增自訂訊息和原因
class APIKeyExpiredException(message: String = "API key expired", cause: Throwable? = null) : AccountException(message, cause)
// 更改占位符號函式的值以獲得不同結果
fun areCredentialsValid(): Boolean = true
fun isAPIKeyExpired(): Boolean = true
// 驗證帳戶憑據和 API 金鑰
fun validateAccount() {
if (!areCredentialsValid()) throw InvalidAccountCredentialsException()
if (isAPIKeyExpired()) {
// 拋出具有特定原因的 APIKeyExpiredException 的範例
val cause = RuntimeException("API key validation failed due to network error")
throw APIKeyExpiredException(cause = cause)
}
}
fun main() {
try {
validateAccount()
println("Operation successful: Account credentials and API key are valid.")
} catch (e: AccountException) {
println("Error: ${e.message}")
e.cause?.let { println("Caused by: ${it.message}") }
}
}Nothing 型別
在 Kotlin 中,每個運算式都有一個型別。 運算式 throw IllegalArgumentException() 的型別是 Nothing,這是一個內建型別,是所有其他型別的子型別,也稱為 底端型別 (bottom type)。 這意味著 Nothing 可以用作傳回型別或泛型型別,用於預期任何其他型別的地方,而不會導致型別錯誤。
Nothing 是 Kotlin 中的一個特殊型別,用於表示永遠無法成功完成的函式或運算式, 原因可能是它們總是拋出例外,或是進入了像無限迴圈這樣的無止盡執行路徑。 您可以使用 Nothing 來標記尚未實作或設計為一律拋出例外的函式, 向編譯器和程式碼讀者清晰地表明您的意圖。 如果編譯器在函式簽章中推論出 Nothing 型別,它將會發出警告。 明確地將 Nothing 定義為傳回型別可以消除此警告。
這段 Kotlin 程式碼示範了 Nothing 型別的使用,編譯器會將函式呼叫後的程式碼標記為無法到達:
class Person(val name: String?)
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
// 此函式永遠不會成功傳回。
// 它一律會拋出例外。
}
fun main() {
// 建立 Person 的執行個體,其中 'name' 為 null
val person = Person(name = null)
val s: String = person.name ?: fail("Name required")
// 到此處時,'s' 保證已初始化
println(s)
}Kotlin 的 TODO() 函式也使用了 Nothing 型別,作為一個占位符號來強調程式碼中需要未來實作的區域:
fun notImplementedFunction(): Int {
TODO("This function is not yet implemented")
}
fun main() {
val result = notImplementedFunction()
// 這會拋出 NotImplementedError
println(result)
}如您所見,TODO() 函式一律會拋出 NotImplementedError 例外。
例外類別
讓我們探索 Kotlin 中常見的一些例外型別,它們都是 RuntimeException 類別的子類別:
ArithmeticException:當無法執行算術運算(例如除以零)時,就會發生此例外。kotlinval example = 2 / 0 // 拋出 ArithmeticExceptionIndexOutOfBoundsException:拋出此例外以表示某種索引(例如陣列或字串)超出範圍。kotlinval myList = mutableListOf(1, 2, 3) myList.removeAt(3) // 拋出 IndexOutOfBoundsException若要避免此例外,請使用更安全的替代方案,例如
getOrNull()函式:kotlinval myList = listOf(1, 2, 3) // 傳回 null,而不是 IndexOutOfBoundsException val element = myList.getOrNull(3) println("Element at index 3: $element")NoSuchElementException:當存取特定集合中不存在的元素時,會拋出此例外。它發生在使用了預期特定元素的函式時,例如first()或last()。kotlinval emptyList = listOf<Int>() val firstElement = emptyList.first() // 拋出 NoSuchElementException若要避免此例外,請使用更安全的替代方案,例如
firstOrNull()函式:kotlinval emptyList = listOf<Int>() // 傳回 null,而不是 NoSuchElementException val firstElement = emptyList.firstOrNull() println("First element in empty list: $firstElement")NumberFormatException:當嘗試將字串轉換為數值型別,但該字串格式不正確時,就會發生此例外。kotlinval string = "This is not a number" val number = string.toInt() // 拋出 NumberFormatException若要避免此例外,請使用更安全的替代方案,例如
toIntOrNull()函式:kotlinval nonNumericString = "not a number" // 傳回 null,而不是 NumberFormatException val number = nonNumericString.toIntOrNull() println("Converted number: $number")NullPointerException:當應用程式嘗試使用值為null的物件參考時,會拋出此例外。 雖然 Kotlin 的 null 安全特性顯著降低了 NullPointerExceptions 的風險,但它們仍可能發生,不論是透過刻意使用!!運算子,還是與缺乏 Kotlin null 安全特性的 Java 互動時。kotlinval text: String? = null println(text!!.length) // 拋出 NullPointerException
雖然在 Kotlin 中所有例外都是非受檢的,且您不一定要明確地擷取它們,但如果您有需要,仍可以靈活地擷取它們。
例外階層結構
Kotlin 例外階層結構的根源是 Throwable 類別。 它有兩個直接子類別,即 Error 和 Exception:
Error子類別表示應用程式本身可能無法恢復的嚴重基本問題。 這些通常是您不會嘗試處理的問題,例如OutOfMemoryError或StackOverflowError。Exception子類別用於您可能想要處理的條件。Exception型別的子型別,例如RuntimeException和IOException(Input/Output Exception),處理應用程式中的例外事件。
RuntimeException 通常是由程式碼中檢查不足引起的,可以透過程式設計來預防。 Kotlin 有助於防止常見的 RuntimeExceptions(例如 NullPointerException),並針對潛在的執行階段錯誤(例如除以零)提供編譯期警告。下圖展示了從 RuntimeException 衍生出的子型別階層結構:
堆疊追蹤
堆疊追蹤 (stack trace) 是由執行環境產生的報告,用於偵錯。 它顯示了導致程式中特定點(尤其是發生錯誤或例外之處)的函式呼叫序列。
讓我們看一個在 JVM 環境中因例外而自動列印堆疊追蹤的範例:
fun main() {
throw ArithmeticException("This is an arithmetic exception!")
}在 JVM 環境中執行此程式碼會產生以下輸出:
Exception in thread "main" java.lang.ArithmeticException: This is an arithmetic exception!
at MainKt.main(Main.kt:3)
at MainKt.main(Main.kt)第一行是例外描述,其中包括:
- 例外型別:
java.lang.ArithmeticException - 執行緒:
main - 例外訊息:
"This is an arithmetic exception!"
例外描述後每一行以 at 開頭的行就是堆疊追蹤。單行稱為 堆疊追蹤元素 (stack trace element) 或 堆疊框架 (stack frame):
at MainKt.main (Main.kt:3):這顯示了方法名稱 (MainKt.main),以及呼叫該方法的原始程式檔和行號 (Main.kt:3)。at MainKt.main (Main.kt):這顯示例外發生在Main.kt檔案的main()函式中。
與 Java、Swift 及 Objective-C 的例外互通性
由於 Kotlin 將所有例外視為非受檢,因此當從區分受檢 (checked) 與非受檢例外的語言呼叫此類例外時,可能會導致複雜情況。 為了解決 Kotlin 與 Java、Swift 及 Objective-C 等語言在例外處理上的差異, 您可以使用 @Throws 註解。 此註解會提醒呼叫者可能發生的例外。 如需更多資訊,請參閱從 Java 呼叫 Kotlin 和 與 Swift/Objective-C 的互通性。
