例外
例外狀況能幫助您的程式碼運行更具可預測性,即使在運行時發生可能中斷程式執行的錯誤。Kotlin 預設將所有例外視為 unchecked (非檢查式)。Unchecked 例外簡化了例外處理的過程:您可以捕獲例外,但不需要明確處理或 宣告 它們。
欲了解 Kotlin 如何在與 Java、Swift 和 Objective-C 互動時處理例外,請參閱 與 Java、Swift 和 Objective-C 的例外互通性 一節。
處理例外主要包含兩個動作:
- 拋出例外 (Throwing exceptions): 指示問題發生時。
- 捕獲例外 (Catching exceptions): 透過解決問題或通知開發者或應用程式使用者來手動處理非預期的例外。
例外由 Exception
類別的子類別表示,而 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 提供了其他方式,可以使用前置條件函式自動拋出例外。前置條件函式包括:
前置條件函式 | 使用情境 | 拋出的例外 |
---|---|---|
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()
函式允許編譯器執行 智慧型轉型 (smart casting)。 成功檢查後,變數會自動轉型為非空值型別。 這些函式常用於空值性檢查,以確保變數在繼續執行前不為空值。例如:kotlinfun printNonNullString(str: String?) { // Nullability check require(str != null) // After this successful check, 'str' is guaranteed to be // non-null and is automatically smart cast to non-nullable 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()
函式允許編譯器執行 智慧型轉型 (smart casting)。 成功檢查後,變數會自動轉型為非空值型別。 這些函式常用於空值性檢查,以確保變數在繼續執行前不為空值。例如:kotlinfun printNonNullString(str: String?) { // Nullability check check(str != null) // After this successful check, 'str' is guaranteed to be // non-null and is automatically smart cast to non-nullable 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
的一般 catch
區塊會捕獲其所有類型的例外,包括像 InsufficientFundsException
這樣的特定例外,除非它們在更早的 catch
區塊中被捕獲。
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") // After this block, the .use function automatically calls writer.close(), similar to a finally block }
如果您的程式碼需要資源清理而不處理例外,您也可以單獨使用 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 中的例外是有狀態的物件,攜帶著與其建立上下文相關的資訊,這些資訊被稱為 堆疊追蹤。 避免使用 物件宣告 (object declarations) 建立例外。 相反地,每次您需要例外時,都請建立一個新的例外實例。 這樣可以確保例外的狀態準確反映特定上下文。
自訂例外也可以是任何現有例外子類別的子類別,例如 ArithmeticException
子類別:
class NumberTooLargeException: ArithmeticException("My message")
如果您想建立自訂例外的子類別,則必須將父類別宣告為
open
,因為 類別預設為final
,否則無法建立子類別。例如:
kotlin// Declares a custom exception as an open class, making it subclassable open class MyCustomException(message: String): Exception(message) // Creates a subclass of the custom exception 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)
}
在具有多樣化錯誤情境的應用程式中,建立例外繼承結構有助於使程式碼更清晰和更具體。您可以透過使用 抽象類別 (abstract class) 或 密封類別 (sealed class) 作為共同例外功能的基礎,並為詳細例外類型建立特定的子類別來實現此目標。此外,包含帶有預設值的參數的自訂例外提供了靈活性,允許以不同的訊息進行初始化,從而實現更細粒度的錯誤處理。
讓我們看一個範例,使用密封類別 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 // throws ArithmeticException
IndexOutOfBoundsException
:拋出此例外表示某種索引(例如陣列或字串的索引)超出範圍。kotlinval myList = mutableListOf(1, 2, 3) myList.removeAt(3) // throws IndexOutOfBoundsException
為了避免此例外,請使用更安全的替代方案,例如
getOrNull()
函式:kotlinval myList = listOf(1, 2, 3) // Returns null, instead of IndexOutOfBoundsException val element = myList.getOrNull(3) println("Element at index 3: $element")
NoSuchElementException
:當存取特定集合中不存在的元素時,會拋出此例外。它在使用預期特定元素的方法(例如first()
或last()
)時發生。kotlinval emptyList = listOf<Int>() val firstElement = emptyList.first() // throws NoSuchElementException
為了避免此例外,請使用更安全的替代方案,例如
firstOrNull()
函式:kotlinval emptyList = listOf<Int>() // Returns null, instead of NoSuchElementException val firstElement = emptyList.firstOrNull() println("First element in empty list: $firstElement")
NumberFormatException
:當嘗試將字串轉換為數值型別,但字串格式不正確時,會發生此例外。kotlinval string = "This is not a number" val number = string.toInt() // throws NumberFormatException
為了避免此例外,請使用更安全的替代方案,例如
toIntOrNull()
函式:kotlinval nonNumericString = "not a number" // Returns null, instead of NumberFormatException val number = nonNumericString.toIntOrNull() println("Converted number: $number")
NullPointerException
:當應用程式嘗試使用值為null
的物件引用時,會拋出此例外。即使 Kotlin 的空值安全功能顯著降低了NullPointerException
的風險,但它們仍可能透過刻意使用!!
運算符或與缺乏 Kotlin 空值安全的 Java 互動時發生。kotlinval text: String? = null println(text!!.length) // throws a NullPointerException
儘管 Kotlin 中所有例外都是 unchecked,且您不必明確捕獲它們,但您仍然可以根據需要靈活地捕獲它們。
例外繼承結構
Kotlin 例外繼承結構的根是 Throwable
類別。它有兩個直接子類別:Error
和 Exception
:
Error
子類別代表應用程式本身可能無法恢復的嚴重基本問題。這些是您通常不會嘗試處理的問題,例如OutOfMemoryError
或StackOverflowError
。Exception
子類別用於您可能希望處理的條件。Exception
型別的子型別,例如RuntimeException
和IOException
(輸入/輸出例外),處理應用程式中的例外事件。
RuntimeException
通常是由程式碼中檢查不足引起的,可以透過程式設計方式預防。Kotlin 有助於預防常見的 RuntimeException
,例如 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
開頭的都是堆疊追蹤。單行稱為 堆疊追蹤元素 或 堆疊框架:
at MainKt.main (Main.kt:3)
:這顯示了方法名稱 (MainKt.main
) 以及呼叫該方法的原始檔案和行號 (Main.kt:3
)。at MainKt.main (Main.kt)
:這表示例外發生在Main.kt
檔案的main()
函式中。
與 Java、Swift 和 Objective-C 的例外互通性
由於 Kotlin 將所有例外視為 unchecked (非檢查式),當從區分檢查式與非檢查式例外的語言呼叫這些例外時,可能會導致複雜性。為了彌補 Kotlin 與 Java、Swift 和 Objective-C 等語言之間例外處理的差異,您可以使用 @Throws
註解。此註解會提醒呼叫者可能的例外。更多資訊請參閱 從 Java 呼叫 Kotlin 和 與 Swift/Objective-C 的互通性。