Publicado · en mejora
Guía de Kotlin · 5/6
Por ahora, este capítulo solo está disponible en inglés.
This chapter looks at exceptions in Kotlin, at modeling failure as a value with Result, at the precondition helpers require and check, and at writing tests with kotlin.test.
throw, try, catch and finally work much as in Java. The big difference is that Kotlin has no checked exceptions: functions never declare what they throw, and callers are never forced to catch anything.
class InsufficientFundsException(val shortage: Long) :
Exception("Short by $shortage")
fun withdraw(balance: Long, amount: Long): Long {
if (amount > balance) throw InsufficientFundsException(amount - balance)
return balance - amount
}
fun main() {
try {
withdraw(1_000, 5_000)
} catch (e: InsufficientFundsException) {
println(e.message) // Short by 4000
} finally {
println("Transaction closed")
}
}If Java callers need to catch a checked exception from your Kotlin function, annotate it with @Throws(IOException::class) so the Java compiler knows about it.
Like if and when, try yields a value: the last expression of the try block or of the matching catch. throw is an expression too, of type Nothing, so it fits anywhere an expression is expected.
fun parsePort(text: String): Int {
val port = try {
text.toInt()
} catch (e: NumberFormatException) {
8080 // fall back to the default port
}
return port
}
fun fail(message: String): Nothing = throw IllegalStateException(message)
fun main() {
println(parsePort("9000")) // 9000
println(parsePort("abc")) // 8080
}A function returning Nothing never completes normally, and the compiler uses that fact to treat code after the call as unreachable.
The standard library ships small helpers for preconditions. They throw the appropriate exception when a condition is false and state your intent more clearly than a hand-written if plus throw.
| Function | Throws | Use it for |
|---|---|---|
require(condition) | IllegalArgumentException | invalid arguments |
check(condition) | IllegalStateException | invalid object state |
requireNotNull(value) | IllegalArgumentException | arguments that must not be null |
error(message) | IllegalStateException | code that should be unreachable |
class Account(initial: Long) {
var balance = initial
private set
private var closed = false
init {
require(initial >= 0) { "Initial balance must not be negative: $initial" }
}
fun deposit(amount: Long) {
require(amount > 0) { "Deposit must be positive" }
check(!closed) { "Account is closed" }
balance += amount
}
fun close() {
if (closed) error("Account already closed")
closed = true
}
}Messages are passed as lambdas, so the string is only built when the check fails. private set makes the property readable everywhere but writable only inside the class.
When failure is common and expected, you can treat it as data instead of throwing. runCatching runs a block and wraps either its value or the caught exception in a Result<T>.
import java.io.File
fun main() {
val result: Result<String> = runCatching { File("app.conf").readText() }
result
.onSuccess { println("Read ${it.length} characters") }
.onFailure { println("Could not read: ${it.message}") }
val text = result.getOrElse { "default config" }
val lineCount = result.map { it.lines().size }.getOrDefault(0)
println("$text / $lineCount")
}Result also offers isSuccess, getOrNull, exceptionOrNull and fold. One caveat: runCatching catches every Throwable, so inside coroutines take care not to swallow CancellationException, which is how cancellation is signaled.
kotlin.test is a test library with the same API on every platform; on the JVM it runs on top of JUnit. The Gradle setup is short:
// build.gradle.kts
dependencies {
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}With useJUnitPlatform(), the Kotlin Gradle plugin picks the JUnit 5 flavor of kotlin-test for you. Tests go under src/test/kotlin.
// src/test/kotlin/AccountTest.kt
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class AccountTest {
@Test
fun `deposit increases the balance`() {
val account = Account(1_000)
account.deposit(500)
assertEquals(1_500L, account.balance)
}
@Test
fun `non-positive deposits are rejected`() {
val account = Account(0)
val e = assertFailsWith<IllegalArgumentException> {
account.deposit(-1)
}
assertEquals("Deposit must be positive", e.message)
}
}Backtick-quoted function names can be full sentences, which makes test reports easy to read. Run everything with ./gradlew test. Popular additions include Kotest for richer assertions and test styles, MockK for mocking, and runTest from kotlinx-coroutines-test for coroutine code.
throws clauses and no forced catch blocks.try and throw are expressions, so their results can be assigned or used after the Elvis operator.require for arguments, check for state, and error for unreachable code.runCatching wraps outcomes in Result; avoid swallowing cancellation inside coroutines.@Test, assertEquals and assertFailsWith from kotlin.test, and run them with ./gradlew test.
0 comentarios
Iniciar sesión · Inicia sesión para dejar un comentario.
Sé el primero en comentar.