Обмен информацией с пользователем является одной из важнейших задач в программировании. Информация от пользователя может поступать в разных формах, здесь мы рассмотрим лишь одну из ситуаций — когда информация поступает в виде строки, содержимое которой необходимо распознать. Традиционное название этой задачи — разборстроки (или текста), либо парсинг (от английского parsing) строки. Разбираемой строкой может быть как что-то, непосредственно введённое пользователем с клавиатуры, так и содержимое какого-либо файла, а часто и строчка, пришедшая по сети (например, содержимое HTTP-запроса). Отметим, что анализ текстов программ также включает разбор текста программы как одну из решаемых задач.
Второй стороны обмена является представление информации пользователю. Опять-таки существует много разных форм представления информации. Если она представлена в виде строки, часть задачи представления — форматирование этой строки, то есть перевод её в формат, привычный для пользователя.
Рассмотрим обе эти задачи подробнее.
Разбор строки
Простая задача на разбор строки может выглядеть так. Некоторая строка содержит текущее время в формате «11:34:45», то есть часы, минуты и секунды, разделённые двоеточием. Написать функцию, которая разберёт эту строку и рассчитает количество секунд, прошедшее с начала дня. В нашем случае ответ должен быть 11 * 3600 + 34 * 60 + 45 = 41685. Решение на Котлине может выглядеть так:
fun timeStrToSeconds(str: String): Int { val parts = str.split(":") var result = 0 for (part in parts) { val number = part.toInt() result = result * 60 + number } return result }
Данная функция начинается с разбиения строки на части. Для этой цели мы используем функцию str.split(":")
, у которой разбиваемая строка является получателем, а аргумент задаёт последовательность символов, по которой происходит разбиение. Результатом функции split
является СПИСОК строк, содержащий части исходной строки. В нашем случае строка содержит два двоеточия, поэтому частей будет три, например, «11», «34» и «45».
Получив части нашей строки — часы, минуты и секунды — нам необходимо каждую из них превратить из строки в целое число. Это преобразование осуществляется оператором val number = part.toInt()
. Похожие функции нам уже встречались в уроке 2 — они использовались для преобразования целых чисел в вещественные n.toDouble()
или вещественных в целые x.toInt()
. Оказывается, функции toInt()
и toDouble()
определены и для получателя типа String.
Мутирующая переменная result
используется для формирования результата. В процессе выполнения цикла for часы будут умножены на 60 дважды, минуты — один раз, а секунды оставлены как есть (проверьте этот факт). В результате мы получим количество секунд, прошедшее с начала дня.
Исключения
Откройте теперь тестовую функцию timeStrToSeconds
и попробуйте вызвать исходную функцию для некорректного аргумента — например, для строки "AA:00:00"
. Вы увидите сообщение test failed со следующим сообщением:
java.lang.NumberFormatException: For input string: "AA" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Integer.parseInt(Integer.java:580) at java.lang.Integer.parseInt(Integer.java:615) at lesson6.task1.ParseKt.timeStrToSeconds(Parse.kt:14) at lesson6.task1.Tests.timeStrToSeconds(Tests.kt:11)
Произошло так называемое исключение. Исключение — это особый тип переменных, который в языках Котлин и Java имеет название Exception
. У исключений есть множество разновидностей, или подтипов, конкретно в данном случае произошло исключение, тип которого называется NumberFormatException
, или «исключение формата числа». Математически мы имеем здесь переменную e
, причём e ∈ ENF (NumberFormatException), а ENF ⊂ E (Exception).
Переменные с типом «исключение» используются для описания произошедших в ходе программы ошибочных ситуаций. У таких переменных есть возможность, которой нет у других переменных — их можно бросать (throw). В Котлине это делается так:
fun throwExample() { // Создаём исключение val e = NumberFormatException("Description") // Бросаем исключение throw e }
или просто, без создания промежуточной переменной:
fun throwExample() { // Создаём И бросаем исключение throw NumberFormatException("Description") }
Вызов функции NumberFormatException("Description")
создаёт исключение. При этом вначале указывается название типа, а в скобках перечисляются необходимые для создания исключения аргументы. В данном случае аргумент один — это строка с описанием произошедшего; в примере выше таким описанием было For input string: "AA"
. Обратите внимание на необычность такой функции — её имя начинается с большой буквы и полностью совпадает с именем некоторого типа. Такие функции называются конструкторами, поскольку используются для создания новых элементов определённого типа. Позже мы ещё не раз с ними столкнёмся.
Любое исключение — это составной тип данных, и созданное исключение можно записать в какую-либо переменную или сразу же бросить с помощью оператора throw. При этом программа, если в ней не предпринять специальных усилий для обработки этой ситуации, будет немедленно прервана, на консоль будет выведено сообщение, подобное приведённому выше.
После описания исключения на консоль выводится порядок, в котором происходил вызов функций, программисты называют его стек вызовов функций:
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Integer.parseInt(Integer.java:580) at java.lang.Integer.parseInt(Integer.java:615) at lesson6.task1.ParseKt.timeStrToSeconds(Parse.kt:14) at lesson6.task1.Tests.timeStrToSeconds(Tests.kt:11)
Читается стек вызовов обычно снизу вверх следующим образом:
- Функция
timeStrToSeconds
из пакетаlesson6.task1
и классаTests
в строке 11 файлаTests.kt
вызвала… - Функцию
timeStrToSeconds
из пакетаlesson6.task1
и классаParseKt
, которая в строке 14 файлаParse.kt
, в свою очередь, вызвала… - Функцию
parseInt
из пакетаjava.lang
и классаInteger
, которая в строке 615 файлаInteger.java
, в свою очередь, вызвала…
и так далее. Читая стек вызовов, надо иметь в виду, что часть функций — например, toInt
— в Котлине являются встраиваемыми (inline) — такие функции не попадают в стек. Другая часть функций может находится в библиотеках, в том числе и библиотеках Java — в частности, parseInt
. Обычно, чтобы разобраться в ситуации, достаточно изучения собственных функций.
Обратите внимание, что строчки вроде Parse.kt:14
IDE подсвечивает синим цветом. Щелчком на них можно перейти к соответствующей строке. Видно, что там происходит вызов part.toInt()
. Из описания исключения можно сделать вывод, что оно произошло для исходной строки "AA"
, и понятно, о какой ошибке идёт речь — эта строка не соответствует никакому десятичному числу.
Поставим себя на минутку на место функции toInt()
. Ей передана некоторая строка, из которой следует сделать число. Если строка действительно соответствует числу, его следует сконструировать и вернуть как результат. Но что делать, если из строки нельзя сконструировать число? Мы могли бы вернуть некоторую специальную константу (во многих упражнения предлагалось делать так), но какую? Любое целое число может быть результатом преобразования какой-либо строки. Пусть, например, мы выбрали число -1, и пусть при вызове toInt()
мы получили такой результат. Как нам узнать — это произошла ошибка, или же наша строка действительно была равна "-1"
? Из-за возможности подобных неоднозначностей программисты придумали исключения.
Итак, если ранее функция обязана была всегда сформировать какой-нибудь результат, то с появлением исключений у неё появилась вторая альтернатива — бросить исключение. Такое поведение характерно для многих функций. Например, при обращении к элементу списка по индексу необходимо, чтобы индекс находился в пределах от 0 до list.size - 1
. В противном случае произойдёт исключение подтипа IndexOutOfBoundsException
.
Итак, исключения обеспечивают для функций возможность сделать что-то разумное в ситуации, когда они НЕ МОГУТ корректно сформировать свой результат. Кроме этого, они обеспечивают возможность для программиста разобраться, что же случилось, и исправить ошибку. Исправить её можно двумя способами: либо убрать причину возникновения исключения, либо обеспечить его обработку.
Благодарю!