Обмен информацией с пользователем является одной из важнейших задач в программировании. Информация от пользователя может поступать в разных формах, здесь мы рассмотрим лишь одну из ситуаций — когда информация поступает в виде строки, содержимое которой необходимо распознать. Традиционное название этой задачи — разборстроки (или текста), либо парсинг (от английского parsing) строки. Разбираемой строкой может быть как что-то, непосредственно введённое пользователем с клавиатуры, так и содержимое какого-либо файла, а часто и строчка, пришедшая по сети (например, содержимое HTTP-запроса). Отметим, что анализ текстов программ также включает разбор текста программы как одну из решаемых задач.

Второй стороны обмена является представление информации пользователю. Опять-таки существует много разных форм представления информации. Если она представлена в виде строки, часть задачи представления — форматирование этой строки, то есть перевод её в формат, привычный для пользователя.

Рассмотрим обе эти задачи подробнее.

Разбор строки

Простая задача на разбор строки может выглядеть так. Некоторая строка содержит текущее время в формате «11:34:45», то есть часы, минуты и секунды, разделённые двоеточием. Написать функцию, которая разберёт эту строку и рассчитает количество секунд, прошедшее с начала дня. В нашем случае ответ должен быть 11 * 3600 + 34 * 60 + 45 = 41685. Решение на Котлине может выглядеть так:

Данная функция начинается с разбиения строки на части. Для этой цели мы используем функцию 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 имеет название Exception. У исключений есть множество разновидностей, или подтипов, конкретно в данном случае произошло исключение, тип которого называется NumberFormatException, или «исключение формата числа». Математически мы имеем здесь переменную e, причём e ∈ ENF (NumberFormatException), а ENF ⊂ E (Exception).

Переменные с типом «исключение» используются для описания произошедших в ходе программы ошибочных ситуаций. У таких переменных есть возможность, которой нет у других переменных — их можно бросать (throw). В Котлине это делается так:

или просто, без создания промежуточной переменной:

Вызов функции NumberFormatException("Description") создаёт исключение. При этом вначале указывается название типа, а в скобках перечисляются необходимые для создания исключения аргументы. В данном случае аргумент один — это строка с описанием произошедшего; в примере выше таким описанием было For input string: "AA". Обратите внимание на необычность такой функции — её имя начинается с большой буквы и полностью совпадает с именем некоторого типа. Такие функции называются конструкторами, поскольку используются для создания новых элементов определённого типа. Позже мы ещё не раз с ними столкнёмся.

Любое исключение — это составной тип данных, и созданное исключение можно записать в какую-либо переменную или сразу же бросить с помощью оператора throw. При этом программа, если в ней не предпринять специальных усилий для обработки этой ситуации, будет немедленно прервана, на консоль будет выведено сообщение, подобное приведённому выше.

После описания исключения на консоль выводится порядок, в котором происходил вызов функций, программисты называют его стек вызовов функций:

Читается стек вызовов обычно снизу вверх следующим образом:

  1. Функция timeStrToSeconds из пакета lesson6.task1 и класса Tests в строке 11 файла Tests.kt вызвала…​
  2. Функцию timeStrToSeconds из пакета lesson6.task1 и класса ParseKt, которая в строке 14 файла Parse.kt, в свою очередь, вызвала…​
  3. Функцию 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.

Итак, исключения обеспечивают для функций возможность сделать что-то разумное в ситуации, когда они НЕ МОГУТ корректно сформировать свой результат. Кроме этого, они обеспечивают возможность для программиста разобраться, что же случилось, и исправить ошибку. Исправить её можно двумя способами: либо убрать причину возникновения исключения, либо обеспечить его обработку.

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

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

Ловится исключение так. Часть функции, где может произойти исключение, оборачивается блоком 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" и, увидев наш ответ без нулей, он на мгновение придёт в ступор и задумается, а что же это вообще такое — время или же просто последовательность чисел. Именно поэтому важно всё-таки соблюдать ожидаемый формат.

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

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

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

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

Первым аргументом функции является форматная строка. Это обычный строковый литерал (константа), в которой, однако, особый смысл несёт символ процента %. Этот символ вместе с несколькими последующими образует модификатор формата, который функцией 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 или её русскоязычный аналог.

Консольный ввод

Разбор и форматирование строк может применяться, в том числе, для взаимодействия с пользователем в консольном приложении. Вам уже известна функция println, предназначенная для вывода информации на консоль. Комбинируя её с функцией String.format или со строковыми шаблонами, программа может обеспечить вывод на консоль в нужном пользователю формате.

Для ввода информации с консоли в Котлине применяется функция readLine(), считывающая одну строку с консоли. Строка заканчивается, когда пользователь нажимает клавишу Enter. Функция не имеет параметров, а результат её имеет тип String?. Знак вопроса после названия типа означает, что, помимо строки, результатом функции может быть также специальная константа null. Смысл этой константы в большинстве случаев — »некорректный результат», фактически это ещё один (в дополнении к исключениям) способ поведения в ошибочных ситуациях. Более точный смысл null — некорректная ссылка, не ссылающаяся никуда.

readLine() использует результат null, когда ввод строки по какой-либо причине завершился неудачно. Это может произойти при достижении особого символа конец файла, который в нормальной ситуации не встречается при вводе в консоли. Операционная система, однако, имеет возможность перенаправления консольного входа программы таким образом, чтобы вместо ввода информации пользователем с клавиатуры программа читала информацию из файла. В случае использования такого перенаправления действительно возможно достижение конца файла.

Пример использования readLine():

Здесь используется уже написанная нами функция timeStrToSeconds, которой передаётся на вход прочитанная строка line.

Упражнения

Откройте файл srс/lesson6/task1/Parse.kt в проекте KotlinAsFirst.

Как скачать и подключить проект KotlinAsFirst смотрите во Введении.

Выберите любую из задач в нём. Придумайте её решение и запишите его в теле соответствующей функции. Применяйте функцию split для разбора строк. Для их форматирования применяйте строковые шаблоны или, если их недостаточно — функцию String.format().

Откройте файл test/lesson6/task1/Tests.kt, найдите в нём тестовую функцию — её название должно совпадать с названием написанной вами функции. Запустите тестирование, в случае обнаружения ошибок исправьте их и добейтесь прохождения теста. Подумайте, все ли необходимые проверки включены в состав тестовой функции, добавьте в неё недостающие проверки.

Решите ещё хотя бы одну задачу из урока 6 на ваш выбор. Убедитесь в том, что можете решать такие задачи уверенно и без посторонней помощи. По возможности решите одну из задач, помеченных как «Сложная».

Попробуйте написать функцию main, осуществляющую консольный ввод входных данных для вашей задачи (хорошо подходит в тех случаях, когда параметром функции является строка), с последующим выводом на консоль результатов, попробуйте поиграть с получившейся программой в консоли.

Переходите к следующему разделу.

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