Обработка исключений

Как предусмотреть возможность появления исключения в программе? Вернёмся к задаче о преобразовании времени в формате «ЧЧ:ММ:СС» в число секунд, прошедшее с начала дня. В этой задаче нам известно, что число часов, минут и секунд неотрицательно, поэтому мы могли бы возвращать результат -1 в случае, когда исходная строка некорректна. В отличие от функции toInt(), в нашем случае -1 секунда не может получиться из любой корректной строки. Но как вернуть результат -1, если произошло исключение? Для этого исключение необходимо поймать (catch).

fun timeStrToSeconds(str: String): Int {
    val parts = str.split(":")
    var result = 0
    try {
        for (part in parts) {
            val number = part.toInt()
            result = result * 60 + number
        }
        return result
    }
    catch (e: NumberFormatException) {
        return -1
    }
}

Ловится исключение так. Часть функции, где может произойти исключение, оборачивается блоком try { } — сравните текст функции с её первоначальным вариантом. try с английского переводится как «попытаться» (выполнить участок программы, в котором может произойти исключение). После блока try записывается один (или несколько) блоков catch (e: ExceptionType) { } — в котором написано, что следует делать, если произошло определённое исключение. Как только в результате одного из вызовов функций внутри блока try происходит исключение типа NumberFormatException, выполнение блока try прерывается и начинает выполняться блок catche: ExceptionType — это параметр блока catchExceptionType указывает его тип — в нашем случае это NumberFormatException.

Рассмотрим порядок ловли исключения чуть более точно. Пусть в некоторой функции foo произошло определённое исключение типа SomeException. Будем считать, что функция способна обработать исключение типа SomeException, если в данный момент она находится внутри блока try, и за ним имеется блок catch для ловли исключения типа SomeException или более общего (например, Exception). Тогда программа последовательно выполнит следующие действия:

  1. Проверим, может ли функция foo обработать исключение. Если да — управление передаётся её блоку catch.
  2. В противном случае, перейдём к функции bar, которая до этого вызвала функцию foo. Проверим, может ли она обработать исключение. Если да — управление передаётся её блоку catch.
  3. В противном случае, перейдём у функции baz, которая до этого вызвала функцию bar. Проверим то же самое для неё.
  4. И так далее. Если в итоге мы дошли до самого верхнего уровня (например, функции main), и ни одна из функций на нашем пути не может обработать исключение — выполнение программы прерывается. В консоли при этом появится сообщение о произошедшем исключении и стек вызовов функций в момент его появления.

Выполнение блока catch после передачи управления ему происходит обычным образом. В нашем случае он содержит один оператор return -1, который формирует результат функции, и выполнение её на этом заканчивается. В общем случае содержимое блока catch может быть любым. После окончания его выполнения, начинает выполняться следующий оператор после try..catch, если такой оператор есть.

Ловля и обработка исключений — очень важный элемент программирования. Пользуясь чужими программами, вам, скорее всего, не раз приходилось говорить, что программа «упала». В современном программировании такое «падение» программы чаще всего вызывается именно исключением, которое возникло, но никем не было поймано и обработано. Такое исключение приводит к аварийной остановке работы программы, что в промышленном программировании недопустимо. Принято, что программа должна КОРРЕКТНО реагировать на любые, в том числе некорректные, действия пользователя, поэтому промышленные программы обычно включают в себя механизмы обработки исключений.

Форматирование строк

Не менее важной задачей является представление определённой информации пользователю. Здесь мы касаемся лишь маленького кусочка этой задачи — правильного форматирования строк. Вспомним ещё раз нашу задачу о преобразовании времени в число секунд и рассмотрим обратную ей. Пусть дано время в секундах, прошедшее с начала дня, и необходимо сформировать строку в формате «ЧЧ:ММ:СС», соответствующую данному времени.

Представим себе, что мы дали на эту задачу ответ вроде "13:8:1" вместо ожидаемого "13:08:01". С одной стороны, человек должен быть в состоянии понять и наш ответ, но с другой стороны, привычным для человека является всё-таки формат "13:08:01" и, увидев наш ответ без нулей, он на мгновение придёт в ступор и задумается, а что же это вообще такое — время или же просто последовательность чисел. Именно поэтому важно всё-таки соблюдать ожидаемый формат.

Для решения задачи мы могли бы воспользоваться функцией вроде этой:

fun twoDigitStr(n: Int) = if (n in 0..9) "0$n" else "$n"

которая для однозначных чисел формирует строку с нулём впереди, а для остальных всё оставляет как есть. Решение с помощью функции twoDigitStr выглядело бы так:

fun timeSecondsToStr(seconds: Int): String {
    val hour = seconds / 3600
    val minute = (seconds % 3600) / 60
    val second = seconds % 60
    return "${twoDigitStr(hour)}:${twoDigitStr(minute)}:${twoDigitStr(second)}"
}

В первых трёх операторах мы рассчитываем текущий час, минуту и секунду путём деления на 60. В последнем мы формируем требуемую строку, и данная функция работает верно. Есть только два «но»: выглядит последний оператор довольно уродливо, а кроме того, при форматировании строк может возникать много похожих задач и, казалось бы, для них должно существовать общее решение.

Таким решением является готовая функция String.format(). В данном случае она может использоваться так:

fun timeSecondsToStr(seconds: Int): String {
    val hour = seconds / 3600
    val minute = (seconds % 3600) / 60
    val second = seconds % 60
    return String.format("%02d:%02d:%02d", hour, minute, second)
}

Первым аргументом функции является форматная строка. Это обычный строковый литерал (константа), в которой, однако, особый смысл несёт символ процента %. Этот символ вместе с несколькими последующими образует модификатор формата, который функцией String.format будет заменён на её следующий аргумент (hour для первого процента, minute для второго и second для третьего). В этом смысле модификаторы формата напоминают строковые шаблоны "$name", но они имеют большую мощность, так как позволяют выбрать ещё и форматподстановки аргумента в строку.

Конкретно %02d означает «подставить в строку целое число, заняв НЕ МЕНЬШЕ двух (2) символов и заполнив НЕДОСТАЮЩИЕ символы (если число однозначное) нулём (0). Перечислим другие распространённые модификаторы формата:

  • %d — подставить число типа Int;
  • %3d — подставить число типа Int, заняв не меньше трёх позиций (пустые заполняются по умолчанию пробелами);
  • %c — подставить символ;
  • %s — подставить строку;
  • %20s — подставить строку, заняв не меньше 20 позиций;
  • %lf — подставить число типа Double в обычном формате;
  • %le — подставить число типа Double в экспоненциальном формате вида 1.3e+4;
  • %6.2lf — подставить число типа Double в обычном формате, заняв не меньше шести позиций и используя ровно два знака после запятой.

Полное перечисление возможностей форматной строки выходит за рамки этого пособия. Довольно полное описание имеется в соответствующей статье Википедии, см. https://en.wikipedia.org/wiki/Printf_format_string#Syntax или её русскоязычный аналог.

Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.