До этого мы рассматривали ситуации, когда вычисление результата функций происходило без рассмотрения вариантов. Это возможный, но сравнительно редкий случай в программировании. Существует очень много задач, где приходится рассматривать различные варианты, и для этой цели программисты придумали ветвления.

Максимум из двух

В качестве простого примера рассмотрим функцию, результат которой равен наибольшему из её параметров.

Здесь if..else — оператор или конструкция ветвления, переводится с английского как если..иначе. После ключевого слова if в скобках следует условие ветвления m > n. Если условие истинно, в качестве результата используется выражение сразу за условием ветвления, в данном случае m. Если же условие ложно, используется выражение за ключевым словом else, в данном случае n. Конструкцию можно прочитать по-русски как «Если m > n, (то) m, иначе n». Функция max, аналогичная данной, имеется в стандартной библиотеке Котлина.

Число корней квадратного уравнения

Рассмотрим более сложный пример. Следующая функция рассчитывает число корней квадратного уравнения ax2 + bx + c = 0. Напомним, что квадратное уравнение имеет два корня, если его дискриминант больше 0, один корень, если дискриминант равен 0, и ноль корней в противном случае. Для реализации этого алгоритма следует вначале рассчитать дискриминант, а затем применить конструкцию if..else. Функция на Котлине может быть записана так:

Здесь мы применили запись функции в виде блока и использовали промежуточную переменную d. Последняя строчка функции читается как «вернуть: если d > 0.0, (то) 2, иначе, если d = 0.0, (то) 1, иначе 0». Обратите внимание на возможность так называемой «каскадной» записи конструкции if..else в виде if..else if..else if..else if..else (с неограниченным количеством промежуточных элементов).

Операции сравнения

В обоих примерах в условиях мы использовали операции сравнения. Таких операций имеется восемь:

  • > строго больше;
  • >= больше или равно, аналог математического ≥
  • < строго меньше;
  • <= меньше или равно, аналог математического ≤
  • x in a..b — x принадлежит интервалу от a до b, аналог математического a ≤ x ≤ b;
  • x !in a..b — x НЕ принадлежит интервалу от a до b;
  • != не равно, аналог математического ≠
  • == равно (используется два знака равенства, чтобы не путать данную операцию с инициализацией / вычислением результата =).

Операции == и != в Котлине применимы для сравнения аргументов произвольных типов. В частности, разрешается сравнивать на равенство строки — они равны, если имеют равную длину, и соответствующие их символы совпадают: «abc» != «cba».

Остальные операции применимы только в том случае, если для аргументов определена специальная функция сравнения compareTo — об этом мы будем говорить позже. Первое время, нам придётся применять их только для числовых типов.

Математически, результат всех операций сравнения имеет тип Boolean с ровно двумя возможными значениями: truefalse.

Табличная форма ветвлений (when)

Каскадную запись if..else if..else часто можно представить более изящно в табличной форме, используя конструкцию when (когда). Для примера quadraticRootNumber это делается так:

Конструкция when состоит из последовательности записей вида условие -> результат. В последней записи условие заменяется на ключевое слово else (иначе).

Частый случай применения when — ситуация, когда одно и то же выражение необходимо последовательно сравнить на равенство с несколькими другими. Для примера, рассмотрим задачу формирования словесной нотации для оценки. Согласно принятым сейчас стандартам, оценка «5» записывается как «отлично», «4» как «хорошо», «3» как «удовлетворительно» и «2» как «неудовлетворительно». Представим подобное преобразование в виде функции на Котлине, используя when:

Эта функция принимает на вход целочисленную оценку (grade) и формирует на выходе соответствующую ей строку. Напомним, что строкам в Котлине соответствует тип String и записываются они в двойных кавычках.

Для проверки возможного значения grade мы используем конструкцию when (grade), в которой оно последовательно сравнивается с 5, 4, 3 и 2. Обратите внимание, что в нашей записи when имеется и пятый случай (else). Его присутствие необходимо, так как функция должна знать, какой результат ей следует вернуть на выход, для любого допустимого значения входа (в данном случае это тип Int с его диапазоном допустимых значений). Строго говоря, ветка else здесь соответствует ошибочной ситуации, которая может предусматривать специальную обработку — но об этом позже. В функции gradeNotation в этой ситуации мы формируем строку «несуществующая оценка», дописывая к ней значение переданной оценки, например: «несуществующая оценка 0».

Логические функции и операции

Условие в операторе if часто в свою очередь вычисляется с помощью функции с результатом типа Boolean. Пусть, например, имеется круг на плоскости с центром в точке (x0, y0) и радиусом r, а также точка на плоскости с координатами (x, y). Необходимо определить, лежит ли точка внутри круга. Особенность данной задачи в том, что у неё есть только два ответа: ДА или НЕТ, либо, более формально, ИСТИННО (true) или ЛОЖНО (false).

Для решения данной задачи необходимо воспользоваться неравенством круга: (x-x0)2 + (y-y0)2 ≤ r2. Если точка (x, y) удовлетворяет этому неравенству, то она лежит внутри круга, если же нет, то она находится снаружи. Функция очень проста и записывается так:

Здесь вновь используется функция sqr из урока 1 для вычисления квадратов чисел. Тип результата функции pointInsideCircle — Boolean. При написании тестовых функций для неё удобно использовать готовые функции assertTrue и assertFalse, например:

Обе функции имеют один параметр типа BooleanassertTrue (проверить на истину) приводит к неудачному исходу теста, если её аргумент равен false, и продолжает выполнение теста, если он равен trueassertFalse (проверить на ложь) работает с точностью до наоборот.

Функцию pointInsideCircle в свою очередь можно использовать для решения более сложных задач. Например, условие принадлежности точки пересечению или объединению двух кругов может выглядеть так:

В этом примере используются логические операции:

  • && — логическое И, результат равен true, если ОБА аргумента true
  • || — логическое ИЛИ, результат равен true, если ХОТЯ БЫ ОДИН из аргументов равен true
  • ! — логическое НЕ, результат равен true, если аргумент false

Сложный пример: биквадратное уравнение

Рассмотрим теперь более сложный случай. Пусть нам необходимо написать функцию, рассчитывающую минимальный из имеющихся корней биквадратного уравнения: ax4 + bx2 + c = 0. Данное уравнение решается путём замены y = x2, решения квадратного уравнения ay2 + by + c = 0 и последующего решения уравнения x2 = y с подставленными корнями квадратного уравнения y1 и y2. Попробуем сначала записать алгоритм решения задачи в виде последовательности действий:

  1. Если a равно 0, уравнение вырождается в bx2 + c = 0. Вырожденное уравнение:
    • при b равном 0 не имеет решений (или имеет бесконечно много)
    • при c / b > 0 также не имеет решений
    • в противном случае минимальный корень — это x = -sqrt(-c / b)
  2. Рассчитаем дискриминант d = b2 - 4ac.
  3. Если d меньше 0, у квадратного уравнения нет решений, как и у биквадратного.
  4. В противном случае найдём корни квадратного уравнения y1 = (-b + sqrt(d))/(2a) и y2 = (-b - sqrt(d))/(2a).
  5. Вычислим y3 = Max(y1, y2).
  6. Если y3 < 0, у уравнения x2 = y3 нет решений.
  7. В противном случае, минимальный корень биквадратного уравнения — это x = -sqrt(y3).

Запишем теперь то же самое на Котлине. Для обозначения ситуации, когда решений нет, будем использовать специальную константу Double.NaN, так называемое не-число. На практике она может получиться как результат некоторых некорректных действий с вещественными числами, например, после вычисления квадратного корня из -1.

Данная реализация активно использует оператор return. Если в предыдущих примерах он использовался исключительно в конце функций, то в этом примере он встречается в теле функции многократно в конструкции вида if (something) return result. Такая конструкция читается как «если что-то, результат функции равен тому-то (и дальше ничего делать не надо)». Заметьте, что в данном случае вторая часть оператора if — ветка else — отсутствует. Это эквивалентно записи if (something) return result else {}, то есть в ветке «иначе» не делается ничего. В случае, если условие в if не выполнено, функция пропускает оператор return и выполняет оператор, следующий за оператором if.

Всегда ли может отсутствовать ветка else? Нет, не всегда. Это зависит от контекста, то есть конкретного варианта использования if..else. В примере вроде val x = if (condition) 1 else 2 исчезнование ветки else не позволит функции «понять», чему же должно быть равно значение x, что приведёт к ошибке:

В переводе с английского — оператор if должен иметь как главную ветку, так и ветку else, если он используется как выражение. Два наиболее распространённых случая такого рода — val x = if …​ или return if …​. В обоих случаях у if есть результат, который затем используется для записи в x или для формирования результата функции.

Обратите также внимание на самый первый оператор if в minBiRoot. Он выглядит как if (a == 0.0) { …​ } с несколькими операторами в фигурных скобках. По умолчанию, if может иметь только один оператор как в главной ветке, так и в ветке else. Если в случае истинности или ложности условия необходимо выполнить несколько операторов, их следует заключить в фигурные скобки, образуя блок операторов. Блок операторов выполняется последовательно, так же, как и тело функции. Блок может содержать любые операторы, в том числе и другие операторы if.

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

  1. a = b = 0, например 0x4 + 0x2 + 1 = 0 — корней нет.
  2. a = 0, c / b > 0, например 0x4 + 1x2 + 2 = 0 — корней нет.
  3. a = 0, c / b < 0, например 0x4 + 1x2 — 4 = 0 — корни есть, в данном случае минимальный из них -2.
  4. d < 0, например 1x4 -2x2 + 4 = 0 — корней нет.
  5. d > 0, но оба корня y отрицательны, например 1x4 + 3x2 + 2 = 0, y1 = -2, y2 = -1, корней нет.
  6. d > 0, хотя бы один корень y положителен, например 1x4 — 3x2 + 2 = 0, y1 = 1, y2 = 2, минимальный корень -1.41.

Тестовая функция может выглядеть так:

Обратите внимание, что функция assertEquals при работе с типом Double имеет третий аргумент — максимально допустимую погрешность. Учитывая, что расчёты с вещественными числами выполняются приближённо, это важная часть теста. Например, заменив в последнем вызове 1e-2 на 1e-3 (0.01 на 0.001), мы обнаружим, что тест перестал проходить — точное значение корня будет -1.41421356…​, а заданное нами -1.41 с погрешностью 0.00421356…​, что больше по модулю, чем 0.001.

Упражнения

Упражнения для урока 2 разбиты на две задачи — одну про if..else и другую про логические функции. Откройте вначале файл srс/lesson2/task1/IfElse.kt в проекте KotlinAsFirst.

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

Выберите любую из задач в нём. Придумайте её решение и запишите его в теле соответствующей функции.

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

Внимательно прочитайте текст тестовой функции. Какие случаи ей проверяются и как? Существуют ли другие важные случаи, которые следовало бы проверить? Проверьте ещё один или два случая, добавив в текст тестовой функции новые вызовы assertEquals.

Откройте теперь файл srс/lesson2/task2/Logical.kt, содержащий задачи на написание логических функций. Решите одну из них, обратите внимание на имеющиеся тестовые функции — они находятся в файле test/lesson2/task2/Tests.kt.

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

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