1. Когда нужен try/catch
Если честно, вопрос справедливый: «Зачем мне try/catch, если я могу написать readln().toIntOrNull() и жить спокойно?» И во многих местах так и надо делать. Но у исключений есть неприятная особенность: они возникают не только там, где вы ожидаете, и не всегда у вас есть «безопасная версия» операции.
Например, toInt() при неверной строке выбрасывает исключение (а не возвращает null). Деление на ноль выбрасывает исключение. Индекс массива вне диапазона тоже. И если вы хотите, чтобы программа не завершалась аварийно, вам нужен механизм «перехватить проблему и продолжить». Именно это и делает try/catch: «попробуй выполнить опасный код; если случилась ожидаемая ошибка — не падай, а сделай понятное действие».
С точки зрения Kotlin это выглядит так: код в try может прерваться исключением, и тогда управление прыгает в подходящий catch.
Ментальная модель: try/catch — это не «ещё один if»
Очень важно настроить правильную «картинку в голове». if — это выбор ветки внутри нормального потока выполнения. Вы проверили условие и аккуратно пошли по нужной дорожке. Исключение — это не дорожка, это люк в полу: вы шли-шли, а потом внезапно провалились в подвал.
try/catch — это способ поставить под этим люком страховочную сетку. Вы не говорите «если будет ошибка — сделай вот так», вы говорите «попробуй; если сорвёшься — я поймаю». Поэтому try/catch особенно полезен там, где вы не можете заранее проверить всё условием (или проверка будет громоздкой), и где причина проблемы выражается именно исключением.
Синтаксис try/catch и объект исключения e
Давайте разберём форму, которую вы будете писать чаще всего. В Kotlin минимальный вариант обработки ожидаемой ошибки — это try { ... } catch (e: SomeException) { ... }. В try мы кладём то, что потенциально может «упасть», а в catch — то, что мы хотим сделать вместо аварийного завершения программы.
Самое «полезное для новичка» в catch — это переменная e (обычно так и называют), объект исключения. В нём есть как минимум message (строка с текстом ошибки, иногда null) и сам тип исключения. На этом этапе вам не нужно запоминать всю иерархию исключений, но важно понять простую вещь: тип исключения — это подсказка «что именно пошло не так».
Мини-шаблон:
fun main() {
try {
// здесь то, что может "упасть"
} catch (e: Exception) {
println("Что-то пошло не так: ${e.message}")
}
}
Только обратите внимание: ловить сразу Exception — это «широкая сеть». Иногда так делают, но сегодня мы тренируем правильную привычку: ловить конкретную ожидаемую ошибку, а не «всё подряд».
2. Пример: строка → число через toInt()
Начнём с классической ситуации: пользователь вводит число, но вводит не число. Ранее мы решали это через toIntOrNull(). Сейчас специально возьмём toInt(), чтобы увидеть, как выглядит «падение», и как try/catch превращает падение в нормальное сообщение.
fun main() {
print("Введите ваш возраст: ")
val raw = readln()
try {
val age = raw.toInt()
println("Возраст = $age") // например: Возраст = 25
} catch (e: NumberFormatException) {
println("Ошибка: это не похоже на целое число") // если ввели "12а"
}
}
Здесь важно заметить поведение: если toInt() «падает», мы не выполняем строку println("Возраст = $age") — вместо этого прыгаем в catch.
И ещё один маленький, но практичный момент: не надо пытаться «угадать» e.message и выводить её пользователю как единственный текст. Сообщения могут быть не очень дружелюбными. Для пользователя обычно лучше ваш текст («Введите целое число»), а e.message — это скорее подсказка для вас.
Пример: деление и ArithmeticException
Теперь ситуация, где пользователь мог ввести число корректно, но всё равно случилась ошибка. Деление на ноль — идеальный пример: «ввод валиден, но по смыслу не подходит».
Сделаем маленький фрагмент для нашего учебного приложения: хотим посчитать средний расход в день, вводим сумму и количество дней.
fun main() {
print("Сумма расходов: ")
val total = readln().toIntOrNull() ?: 0
print("Количество дней: ")
val days = readln().toIntOrNull() ?: 0
try {
val avg = total / days
println("В среднем: $avg в день") // например: В среднем: 100 в день
} catch (e: ArithmeticException) {
println("Ошибка: нельзя делить на 0 дней (логично, да?)")
}
}
Почему здесь try/catch вообще нужен, если мы делали toIntOrNull()? Потому что ошибка возникает не на разборе строки, а на арифметической операции. Вы можете сколько угодно «безопасно распарсить» 0, но деление total / 0 остаётся делением на ноль.
3. Размер try
Когда новичок впервые узнаёт про try/catch, у него обычно две крайности: либо вообще нигде не ставить try, либо поставить один огромный try на весь main и ловить всё подряд. Второй вариант выглядит удобно: «ну зато не упадёт». Но у этого удобства есть цена: вы перестаёте понимать, что именно упало и где искать проблему.
Хороший стиль на старте: try должен быть минимальным — оборачивать только ту операцию, которая реально может выбросить ожидаемое исключение в этом месте. Чем меньше try, тем меньше «магии», тем легче отлаживать, и тем точнее вы пишете обработку.
Сравните два варианта. Плохой (слишком широкий):
fun main() {
try {
print("Введите число: ")
val n = readln().toInt()
println("Квадрат: ${n * n}")
println("Готово!")
} catch (e: NumberFormatException) {
println("Не число")
}
}
Этот код вроде нормальный, но представьте, что ошибка будет не в toInt(), а где-то позже (не сегодня, но вообще). Вы поймаете NumberFormatException, хотя проблема могла быть другой.
Чуть аккуратнее (минимальный try вокруг toInt()):
fun main() {
print("Введите число: ")
val raw = readln()
val n = try {
raw.toInt()
} catch (e: NumberFormatException) {
println("Не число, беру 0")
0
}
println("Квадрат: ${n * n}") // если ввели "abc" -> Квадрат: 0
}
Формально тут try используется как выражение (возвращает значение). Kotlin это позволяет: результат берётся из последнего выражения в try или в catch.
4. Паттерн «ловим → объясняем → продолжаем»
Самая практичная польза try/catch в консольных программах — сделать так, чтобы пользователь мог ошибаться, а программа не превращалась в «ввёл не то — до свидания». Чтобы это получилось, нам обычно нужен цикл: спрашиваем, пытаемся обработать, если ошибка — объясняем и повторяем.
Напишем функцию readIntWithRetry(prompt: String): Int, которая просит целое число и повторяет ввод, пока не получится.
fun readIntWithRetry(prompt: String): Int {
while (true) {
print(prompt)
val raw = readln()
try {
return raw.toInt()
} catch (e: NumberFormatException) {
println("Нужно целое число. Попробуйте ещё раз.")
}
}
}
Обратите внимание на стиль: в try у нас ровно одна «опасная» операция — toInt(). Если она прошла, мы делаем return и выходим из функции. Если нет — мы печатаем сообщение и цикл повторяется.
Теперь можно использовать её в main, и код станет чище:
fun main() {
val total = readIntWithRetry("Сумма расходов: ")
val days = readIntWithRetry("Количество дней: ")
println("Вы ввели total=$total, days=$days")
}
5. Мини-приложение: учёт расходов с устойчивым вводом
Чтобы примеры не были набором отдельных кусочков, соберём маленькое консольное приложение, которое мы сможем дополнять дальше по курсу. Пусть это будет «копилка расходов»: добавляем траты, показываем сумму, считаем среднее (и не падаем).
Мы пока не используем when (он будет позже), поэтому команды сделаем через if/else. Данные сохраним в IntArray, потому что массивы вы уже знаете.
Заготовка хранилища и добавление расхода
Сначала набросаем хранение: массив expenses и счётчик count. И маленькую функцию добавления.
fun addExpense(expenses: IntArray, count: Int, value: Int): Int {
expenses[count] = value
return count + 1
}
Да, пока мы не проверяем переполнение массива — просто держим в голове то, что это учебный пример (и что массив не резиновый).
Сумма расходов
fun total(expenses: IntArray, count: Int): Int {
var sum = 0
var i = 0
while (i < count) {
sum += expenses[i]
i++
}
return sum
}
Среднее значение: возможна ошибка деления на ноль
Вот здесь у нас появляется естественная точка для try/catch: среднее = сумма / количество. Если count == 0, будет деление на ноль.
fun average(expenses: IntArray, count: Int): Int {
val sum = total(expenses, count)
return try {
sum / count
} catch (e: ArithmeticException) {
0
}
}
Этот пример показывает полезный бытовой смысл try/catch как выражения: мы хотим вернуть число при успехе или «разумный» 0 при невозможности посчитать.
main: команды и устойчивый ввод
Теперь делаем простой цикл команд. Список команд пусть будет такой: "add", "total", "avg", "exit".
fun main() {
val expenses = IntArray(100)
var count = 0
while (true) {
print("Команда (add/total/avg/exit): ")
val cmd = readln().trim()
if (cmd == "add") {
val value = readIntWithRetry("Введите расход (целое число): ")
count = addExpense(expenses, count, value)
println("Добавлено: $value") // например: Добавлено: 150
} else if (cmd == "total") {
println("Сумма: ${total(expenses, count)}")
} else if (cmd == "avg") {
println("Среднее: ${average(expenses, count)}")
} else if (cmd == "exit") {
println("Пока!")
return
} else {
println("Неизвестная команда: $cmd")
}
}
}
Что важно по смыслу: приложение больше не падает от банального ввода "qwe" вместо числа. Оно спокойно говорит «Нужно целое число» и просит повторить. Это и есть то самое «ловим → объясняем → продолжаем».
6. Типичные ошибки при работе с try/catch
Ошибка №1: ловить Exception “на всякий случай”, не понимая, что именно ожидается.
Так делать хочется из лучших побуждений: «лишь бы не упало». Но вы превращаете ошибки в кашу: программа скажет «что-то пошло не так», а вам потом непонятно, почему. На старте дисциплина простая: если вы ожидаете «ввели не число», ловите NumberFormatException; если ожидаете деление на ноль — ArithmeticException. Общий Exception лучше оставлять на потом, когда вы уже понимаете стратегию обработки.
Ошибка №2: делать try слишком большим и смешивать в нём ввод, вычисления и вывод.
Огромный try создаёт эффект «чёрного ящика»: что-то внутри упало, но вы теряете контекст. Плюс легко случайно поймать исключение не там, где вы его реально ожидали. Практика для новичка: в try кладите одну-две конкретные операции, которые могут выбросить конкретную ожидаемую ошибку.
Ошибка №3: пустой catch, который “проглатывает” ошибку молча.
Если вы ничего не делаете в catch, программа может продолжить работу в странном состоянии: данные не считались, значение не посчиталось, но внешне «всё ок». Такое потом очень больно отлаживать. Даже если вы не знаете, что делать, лучше хотя бы вывести понятное сообщение пользователю или себе.
Ошибка №4: печатать пользователю только ${e.message} и надеяться, что это UX.
Сообщения исключений часто технические и иногда вообще null. Для пользователя лучше «Введите целое число» или «Делить на ноль нельзя». А e.message — это скорее вспомогательная деталь, которую можно добавить в скобках, если вы делаете режим отладки.
Ошибка №5: надеяться, что try/catch заменяет валидацию.
try/catch помогает, но не делает код «умным». Например, если вы хотите запретить отрицательные расходы, это не исключение — это бизнес-правило. Его лучше проверить if (value < 0) и объяснить пользователю. Исключения — для действительно «аварийных» ситуаций выполнения, а не для обычной логики.
Ошибка №6: ловить исключение и продолжать, но забыть восстановить состояние.
Если вы начали изменять переменные, а потом что-то упало, можно оставить программу в «полусломанном» состоянии. Поэтому хороший стиль: сначала получить и проверить ввод, потом менять состояние (например, увеличивать count). В нашем примере мы сначала читаем value, и только потом добавляем в массив — это маленькая, но важная привычка.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ