Обработка ошибок в Kotlin: try/catch и не только
Обработка ошибок — неотъемлемая часть написания надёжного и устойчивого кода. В Kotlin, как и в Java, существует классический механизм try/catch/finally, однако язык предлагает и более современные, функциональные подходы. В этой статье мы подробно разберём все способы обработки исключений в Kotlin: от базового try/catch до элегантного runCatching и мощных Result и sealed class.
1. Классический try/catch/finally
Базовый синтаксис обработки исключений в Kotlin практически идентичен Java, но с одним важным отличием: try может быть выражением, то есть возвращать значение.
Пример 1: try как выражение
fun parseIntOrNull(input: String): Int? {
return try {
input.toInt()
} catch (e: NumberFormatException) {
null
}
}
fun main() {
val result = parseIntOrNull("123")
println(result) // 123
val badResult = parseIntOrNull("abc")
println(badResult) // null
}
Пример 2: Множественные блоки catch
fun processInput(input: String) {
try {
val number = input.toInt()
val result = 100 / number
println("Результат: $result")
} catch (e: NumberFormatException) {
println("Ошибка: неверный формат числа")
} catch (e: ArithmeticException) {
println("Ошибка: деление на ноль")
} finally {
println("Блок finally выполняется всегда")
}
}
Блок finally выполняется в любом случае — как при успешном выполнении, так и при возникновении исключения. Это удобно для освобождения ресурсов (закрытие файлов, соединений).
2. Элвис-оператор (?:) и безопасные вызовы
Kotlin предлагает элегантный способ обработки null значений, который часто помогает избежать громоздких конструкций try/catch.
Пример: Безопасное преобразование с элвис-оператором
fun safeStringToInt(input: String): Int {
return input.toIntOrNull() ?: 0 // Если null, возвращаем 0
}
fun main() {
println(safeStringToInt("42")) // 42
println(safeStringToInt("abc")) // 0
}
Функция toIntOrNull() — это встроенная в Kotlin альтернатива try/catch. Она не выбрасывает исключение, а возвращает null при ошибке. Элвис-оператор ?: подставляет значение по умолчанию.
3. Функциональный подход: runCatching и Result
Современный Kotlin рекомендует использовать функциональный стиль обработки ошибок. Ключевые инструменты — runCatching и класс Result.
Пример: runCatching
import kotlin.Result
fun divide(a: Int, b: Int): Result<Int> {
return runCatching {
a / b
}
}
fun main() {
val successResult = divide(10, 2)
val failureResult = divide(10, 0)
// Обработка результата
successResult
.onSuccess { println("Успех: $it") } // Успех: 5
.onFailure { println("Ошибка: ${it.message}") }
failureResult
.onSuccess { println("Успех: $it") }
.onFailure { println("Ошибка: ${it.message}") } // Ошибка: / by zero
// Получение значения с дефолтом
val value1 = successResult.getOrDefault(-1) // 5
val value2 = failureResult.getOrDefault(-1) // -1
// Преобразование результата
val mappedResult = successResult.map { it * 2 } // Result(10)
}
Преимущества runCatching:
- Код становится декларативным и читаемым.
- Нет необходимости вручную писать
try/catch. - Результат можно легко комбинировать, трансформировать и обрабатывать цепочками вызовов (
.map,.flatMap,.recover). - Ошибка не «проглатывается» — она явно представлена в типе
Result.
Важно: Класс Result в Kotlin — это value class, предназначенный для кратковременного использования. Не рекомендуется хранить его в полях классов или коллекциях.
4. Продвинутые техники: Sealed классы для моделирования ошибок
Для сложных доменов, где нужно различать типы ошибок, лучше всего подходят sealed class (запечатанные классы). Они позволяют создать иерархию ошибок и обрабатывать их в when-выражении с гарантией полноты покрытия.
Пример: Моделирование операций с файлом
import java.io.File
import java.io.IOException
// Моделируем возможные результаты операции
sealed class FileOperationResult {
data class Success(val content: String) : FileOperationResult()
data class NotFound(val path: String) : FileOperationResult()
data class PermissionDenied(val path: String) : FileOperationResult()
data class UnknownError(val exception: Throwable) : FileOperationResult()
}
fun readFileContent(path: String): FileOperationResult {
return try {
val file = File(path)
if (!file.exists()) {
return FileOperationResult.NotFound(path)
}
if (!file.canRead()) {
return FileOperationResult.PermissionDenied(path)
}
FileOperationResult.Success(file.readText())
} catch (e: IOException) {
FileOperationResult.UnknownError(e)
}
}
fun processFile(path: String) {
when (val result = readFileContent(path)) {
is FileOperationResult.Success -> {
println("Файл прочитан. Длина: ${result.content.length}")
}
is FileOperationResult.NotFound -> {
println("Ошибка: файл '$path' не найден.")
}
is FileOperationResult.PermissionDenied -> {
println("Ошибка: нет прав на чтение файла '$path'.")
}
is FileOperationResult.UnknownError -> {
println("Неизвестная ошибка: ${result.exception.message}")
}
}
}
Почему это круто?
- Безопасность типов: Компилятор проверяет, что вы обработали все возможные варианты в
when. Если вы добавите новый подклассFileOperationResult