Обработка исключений
Как предусмотреть возможность появления исключения в программе? Вернёмся к задаче о преобразовании времени в формате “ЧЧ:ММ:СС” в число секунд, прошедшее с начала дня. В этой задаче нам известно, что число часов, минут и секунд неотрицательно, поэтому мы могли бы возвращать результат -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 прерывается и начинает выполняться блок catch. e: ExceptionType
— это параметр блока catch, ExceptionType
указывает его тип — в нашем случае это NumberFormatException
.
Рассмотрим порядок ловли исключения чуть более точно. Пусть в некоторой функции foo
произошло определённое исключение типа SomeException
. Будем считать, что функция способна обработать исключение типа SomeException
, если в данный момент она находится внутри блока try, и за ним имеется блок catch для ловли исключения типа SomeException
или более общего (например, Exception
). Тогда программа последовательно выполнит следующие действия:
- Проверим, может ли функция
foo
обработать исключение. Если да — управление передаётся её блоку catch. - В противном случае, перейдём к функции
bar
, которая до этого вызвала функциюfoo
. Проверим, может ли она обработать исключение. Если да — управление передаётся её блоку catch. - В противном случае, перейдём у функции
baz
, которая до этого вызвала функциюbar
. Проверим то же самое для неё. - И так далее. Если в итоге мы дошли до самого верхнего уровня (например, функции
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 или её русскоязычный аналог.
Благодарю!