例外
例外 (Exceptions) 有助於您的程式碼運行更可預測,即使在可能中斷程式執行的執行時錯誤 (runtime errors) 發生時亦然。Kotlin 預設將所有例外視為 非檢查型 (unchecked)。非檢查型例外簡化了例外處理流程:您可以捕捉例外,但無需明確處理或 宣告 它們。
處理例外包含兩個主要動作:
- 拋出例外 (Throwing exceptions): 指示問題何時發生。
- 捕捉例外 (Catching exceptions): 手動處理非預期的例外,方式是解決問題或通知開發者或應用程式使用者。
例外由 Exception
類別的子類別表示,而 Exception
類別又是 Throwable
類別的子類別。有關階層的更多資訊,請參閱 例外階層 章節。由於 Exception
是一個 open class
,您可以建立 自訂例外 以符合應用程式的特定需求。
拋出例外
您可以使用 throw
關鍵字手動拋出例外。拋出例外表示程式碼中發生了非預期的執行時錯誤。例外是 物件,拋出例外會建立例外類別的一個實例。
您可以不帶任何參數地拋出例外:
throw IllegalArgumentException()
為了更好地理解問題的來源,請包含額外資訊,例如自訂訊息和原始原因:
val cause = IllegalStateException("Original cause: illegal state")
// Throws an IllegalArgumentException if userInput is negative
// Additionally, it shows the original cause, represented by the 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() {
// This fails with an IllegalArgumentException
println(getIndices(-1))
// Uncomment the line below to see a working example
// println(getIndices(3))
// [1, 2, 3]
}
NOTE
require()
函式允許編譯器執行 智慧型轉型。
成功檢查後,變數會自動轉型為不可空型別 (non-nullable type)。
這些函式經常用於空值檢查 (nullability checks),以確保變數在繼續執行之前不為空。例如:
fun 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
}
// If you uncomment the line below then the program fails with IllegalStateException
// getStateValue()
someState = ""
// If you uncomment the line below then the program fails with IllegalStateException
// getStateValue()
someState = "non-empty-state"
// This prints "non-empty-state"
println(getStateValue())
}
NOTE
check()
函式允許編譯器執行 智慧型轉型。
成功檢查後,變數會自動轉型為不可空型別。
這些函式經常用於空值檢查,以確保變數在繼續執行之前不為空。例如:
fun 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() {
// This works as expected
val user1 = User("Alice", "admin")
processUserRole(user1)
// Alice is an admin.
// This throws an IllegalStateException
val user2 = User("Bob", "guest")
processUserRole(user2)
}
使用 try-catch 區塊處理例外
當例外被拋出時,它會中斷程式的正常執行。您可以使用 try
和 catch
關鍵字優雅地處理例外,以保持程式穩定。try
區塊包含可能拋出例外程式碼,而 catch
區塊則在例外發生時捕捉並處理它。例外會被第一個符合其特定型別或例外 父類別 的 catch
區塊捕捉。
以下是如何同時使用 try
和 catch
關鍵字:
try {
// Code that may throw an exception
} catch (e: SomeException) {
// Code for handling the exception
}
將 try-catch
作為表達式使用是一種常見的方法,因此它可以在 try
區塊或 catch
區塊中返回值:
fun main() {
val num: Int = try {
// If count() completes successfully, its return value is assigned to num
count()
} catch (e: ArithmeticException) {
// If count() throws an exception, the catch block returns -1,
// which is assigned to num
-1
}
println("Result: $num")
}
// Simulates a function that might throw ArithmeticException
fun count(): Int {
// Change this value to return a different value to 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
// Change this value to test different scenarios
val withdrawalAmount = 500.5
try {
processWithdrawal(withdrawalAmount.toDouble(), availableFunds)
// The order of catch blocks is important!
} 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 {
// Code that may throw an exception
}
catch (e: YourException) {
// Exception handler
}
finally {
// Code that is always executed
}
try
表達式的返回值由 try
或 catch
區塊中最後執行的表達式決定。如果沒有例外發生,結果來自 try
區塊;如果例外被處理,結果來自 catch
區塊。finally
區塊總是執行,但它不會改變 try-catch
區塊的結果。
讓我們看一個範例來演示:
fun divideOrNull(a: Int): Int {
// The try block is always executed
// An exception here (division by zero) causes an immediate jump to the catch block
try {
val b = 44 / a
println("try block: Executing division: $b")
return b
}
// The catch block is executed due to the ArithmeticException (division by zero if a ==0)
catch (e: ArithmeticException) {
println("catch block: Encountered ArithmeticException $e")
return -1
}
finally {
println("finally block: The finally block is always executed")
}
}
fun main() {
// Change this value to get a different result. An ArithmeticException will return: -1
divideOrNull(0)
}
NOTE
在 Kotlin 中,管理實作 AutoClosable
介面的資源的慣用方法,例如 FileInputStream
或 FileOutputStream
等檔案串流,是使用 .use()
函式。此函式會在程式碼區塊完成時自動關閉資源,無論是否拋出例外,從而消除了對 finally
區塊的需求。因此,Kotlin 不需要像 Java 的 try-with-resources 這樣的特殊語法來進行資源管理。
FileWriter("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")
// Simulate a resource being used
// This throws an ArithmeticException if division by zero occurs
val result = 100 / 0
// This line is not executed if an exception is thrown
println("Result: $result")
}
fun close() {
println("Resource closed")
}
}
fun main() {
val resource = MockResource()
try {
// Attempts to use the resource
resource.use()
} finally {
// Ensures that the resource is always closed, even if an exception occurs
resource.close()
}
// This line is not printed if an exception is thrown
println("End of the program")
}
如您所見,finally
區塊保證資源會被關閉,無論是否發生例外。
在 Kotlin 中,您可以靈活地僅使用 catch
區塊、僅使用 finally
區塊或兩者都使用,具體取決於您的特定需求,但 try
區塊必須始終至少伴隨一個 catch
區塊或一個 finally
區塊。
建立自訂例外
在 Kotlin 中,您可以透過建立擴展內建 Exception
類別的類別來定義自訂例外。這允許您建立更特定、針對應用程式需求量身定制的錯誤型別。
要建立一個,您可以定義一個擴展 Exception
的類別:
class MyException: Exception("My message")
在此範例中,有一個預設錯誤訊息「My message」,但如果您願意,可以將其留空。
TIP
Kotlin 中的例外是有狀態物件 (stateful objects),攜帶與其建立上下文相關的資訊,即 堆疊追蹤。
避免使用 物件宣告 建立例外。
相反,每次需要時都建立一個新的例外實例。
這樣,您可以確保例外的狀態準確反映特定上下文。
自訂例外也可以是任何預存在例外子類別的子類別,例如 ArithmeticException
子類別:
class NumberTooLargeException: ArithmeticException("My message")
NOTE
如果您想建立自訂例外的子類別,則必須將父類別宣告為 open
,
因為 類別預設為 final,否則無法被子類別化。
例如:
// 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() {
// Change the value in this function to a get a different exception
myFunction(1)
}
在具有多樣錯誤情境的應用程式中,建立例外階層可以幫助使程式碼更清晰、更具體。您可以透過使用 抽象類別 或 密封類別 作為共同例外功能的基礎,並為詳細例外型別建立特定子類別來實現這一點。此外,帶有可選參數的自訂例外提供了靈活性,允許使用不同訊息進行初始化,從而實現更細粒度的錯誤處理。
讓我們看一個使用密封類別 AccountException
作為例外階層基礎的範例,以及子類別 APIKeyExpiredException
,它展示了如何使用可選參數來改進例外細節:
// Creates a sealed class as the base for an exception hierarchy for account-related errors
sealed class AccountException(message: String, cause: Throwable? = null):
Exception(message, cause)
// Creates a subclass of AccountException
class InvalidAccountCredentialsException : AccountException("Invalid account credentials detected")
// Creates a subclass of AccountException, which allows the addition of custom messages and causes
class APIKeyExpiredException(message: String = "API key expired", cause: Throwable? = null) : AccountException(message, cause)
// Change values of placeholder functions to get different results
fun areCredentialsValid(): Boolean = true
fun isAPIKeyExpired(): Boolean = true
// Validates account credentials and API key
fun validateAccount() {
if (!areCredentialsValid()) throw InvalidAccountCredentialsException()
if (isAPIKeyExpired()) {
// Example of throwing APIKeyExpiredException with a specific cause
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
,這是一個內建型別,是所有其他型別的子型別,也稱為 底部型別。這表示 Nothing
可以用作返回值型別或泛型型別,在預期任何其他型別的地方使用,而不會導致型別錯誤。
Nothing
是 Kotlin 中一種特殊型別,用於表示永不成功完成的函式或表達式,原因可能是它們總是拋出例外或進入無限執行路徑(例如無限迴圈)。您可以使用 Nothing
來標記尚未實作或設計為總是拋出例外的函式,清晰地向編譯器和程式碼閱讀者表明您的意圖。如果編譯器在函式簽名中推斷出 Nothing
型別,它會警告您。明確將 Nothing
定義為返回型別可以消除此警告。
此 Kotlin 程式碼演示了 Nothing
型別的使用,其中編譯器將函式呼叫後面的程式碼標記為無法到達:
class Person(val name: String?)
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
// This function will never return successfully.
// It will always throw an exception.
}
fun main() {
// Creates an instance of Person with 'name' as null
val person = Person(name = null)
val s: String = person.name ?: fail("Name required")
// 's' is guaranteed to be initialized at this point
println(s)
}
Kotlin 的 TODO()
函式也使用了 Nothing
型別,它作為佔位符,用於突出顯示程式碼中需要未來實作的區域:
fun notImplementedFunction(): Int {
TODO("This function is not yet implemented")
}
fun main() {
val result = notImplementedFunction()
// This throws a 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
NOTE
為了避免此例外,請使用更安全的替代方案,例如
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
NOTE
為了避免此例外,請使用更安全的替代方案,例如
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
NOTE
為了避免此例外,請使用更安全的替代方案,例如
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 中的所有例外都是非檢查型的,並且您無需明確捕捉它們,但您仍然可以靈活地根據需要捕捉它們。
例外階層
Kotlin 例外階層的根是 Throwable
類別。它有兩個直接子類別,Error
和 Exception
:
Error
子類別表示應用程式可能無法自行恢復的嚴重且根本的問題。這些問題通常您不會嘗試處理,例如OutOfMemoryError
或StackOverflowError
。Exception
子類別用於您可能希望處理的條件。Exception
型別的子型別,例如RuntimeException
和IOException
(輸入/輸出例外),處理應用程式中的例外事件。
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 將所有例外都視為非檢查型 (unchecked),當從區分檢查型 (checked) 和非檢查型例外 (unchecked exceptions) 的語言呼叫此類例外時,可能會導致複雜性。為了解決 Kotlin 與 Java、Swift 和 Objective-C 等語言在例外處理上的這種差異,您可以使用 @Throws
註解。此註解會提醒呼叫者可能發生的例外。欲了解更多資訊,請參閱 從 Java 呼叫 Kotlin 和 與 Swift/Objective-C 的互通性。