2. Основы Kotlin. Ветвления

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

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

fun minBiRoot(a: Double, b: Double, c: Double): Double {
    // 1: в главной ветке if выполняется НЕСКОЛЬКО операторов
    if (a == 0.0) {
        if (b == 0.0) return Double.NaN // ... и ничего больше не делать
        val bc = -c / b
        if (bc < 0.0) return Double.NaN // ... и ничего больше не делать
        return -sqrt(bc)
        // Дальше функция при a == 0.0 не идёт
    }
    val d = discriminant(a, b, c)   // 2
    if (d < 0.0) return Double.NaN  // 3
    // 4
    val y1 = (-b + sqrt(d)) / (2 * a)
    val y2 = (-b - sqrt(d)) / (2 * a)
    val y3 = max(y1, y2)       // 5
    if (y3 < 0.0) return Double.NaN // 6
    return -sqrt(y3)           // 7
}

Данная реализация активно использует оператор 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' must have both main and 'else' branches if used as an expression.

В переводе с английского — оператор 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.

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

@Test
fun minBiRoot() {
    assertEquals(Double.NaN, minBiRoot(0.0, 0.0, 1.0), 1e-2)
    assertEquals(Double.NaN, minBiRoot(0.0, 1.0, 2.0), 1e-2)
    assertEquals(-2.0, minBiRoot(0.0, 1.0, -4.0), 1e-10)
    assertEquals(Double.NaN, minBiRoot(1.0, -2.0, 4.0), 1e-2)
    assertEquals(Double.NaN, minBiRoot(1.0, 3.0, 2.0), 1e-2)
    assertEquals(-1.41, minBiRoot(1.0, -3.0, 2.0), 1e-2)
}

Обратите внимание, что функция 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.

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

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

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

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

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

Комментарии: 1
  1. Edgar

    не много подумал над последней задачей, не понимал условие, пока не нарисовал наглядно на системе координат

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