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
    Edgar

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

Додати коментар

Цей сайт використовує Akismet для зменшення спаму. Дізнайтеся, як обробляються ваші дані коментарів.