4. Основы Kotlin. Списки

Введение

Начиная с этого раздела мы переходим к изучению составных типов данных, включающих в себя несколько элементов простых типов. Такие типы очень часто необходимы в программировании. Вспомним, например, задачу про поиск минимального корня биквадратного уравнения ax4 + bx2 + c = 0 из урока 2. В гораздо более распространённой формулировке она выглядела бы так: найти ВСЕ корни биквадратного уравнения.

Можно ли написать функцию, которая эту задачу решит? Конечно, да, но результатом подобной функции должен быть список найденных корней биквадратного уравнения. Список — это и есть один из очень распространённых составных типов со следующими свойствами:

  • список может включать в себя произвольное количество элементов (от нуля до бесконечности);
  • количество элементов в списке называется его размером;
  • все элементы списка имеют один и тот же тип (в свою очередь, этот тип может быть простым — список вещественных чисел, или составным — список строк, или список списков целых чисел, или любые другие варианты);
  • в остальном элементы списка независимы друг от друга.

Рассмотрим решение задачи о поиске корней биквадратного уравнения на Котлине:

fun biRoots(a: Double, b: Double, c: Double): List<Double> {
    if (a == 0.0) {
        if (b == 0.0) return listOf()
        val bc = -c / b
        if (bc < 0.0) return listOf()
        val root = sqrt(bc)
        return if (root == 0.0) listOf(root) else listOf(-root, root)
    }
    val d = discriminant(a, b, c)
    if (d < 0.0) return listOf()
    val y1 = (-b + sqrt(d)) / (2 * a)
    val y2 = (-b - sqrt(d)) / (2 * a)
    // part1: List<Double>
    val part1 = if (y1 < 0) listOf() else if (y1 == 0.0) listOf(0.0) else {
        val x1 = sqrt(y1)
        listOf(-x1, x1)
    }
    // part2: List<Double>
    val part2 = if (y2 < 0) listOf() else if (y2 == 0.0) listOf(0.0) else {
        val x2 = sqrt(y2)
        listOf(-x2, x2)
    }
    return part1 + part2
}

Данное решение построено по алгоритму, приведённому в конце второго урока, с той лишь разницей, что здесь мы ищем все имеющиеся корни:

  1. Первый if рассматривает тривиальный случай a = 0 и более простое уравнение bx2 = -c. Оно либо не имеет корней (с / b > 0), либо имеет один корень 0 (c / b = 0), либо два корня (c / b < 0).
  2. Затем мы делаем замену y = x2 и считаем дискриминант d = b2 — 4ac. Если он отрицателен, уравнение не имеет корней.
  3. Если дискриминант равен 0, уравнение ay2 + by + c = 0 имеет один корень. В зависимости от его знака, биквадратное уравнение либо не имеет корней, либо имеет один корень 0, либо имеет два корня.
  4. В противном случае дискриминант положителен и уравнение ay2 + by + c = 0 имеет два корня. Каждый из них, в зависимости от его знака, превращается в ноль, один или два корней биквадратного уравнения.

Посмотрите на тип результата функции biRoots — он указан как List<Double>List в Котлине — это и есть список. В угловых скобках <> указывается так называемый типовой аргумент — тип элементов списка, то есть List<Double>вместе — это список вещественных чисел.

Для создания списков, удобно использовать функцию listOf(). Аргументы этой функции — это элементы создаваемого списка, их может быть произвольное количество (в том числе 0). В ряде случаев, когда биквадратное уравнение не имеет корней, функция biRoots возвращает пустой список результатов.

В последнем, самом сложном случае, когда уравнение ay2 + by + c = 0 имеет два корня y1 и y2, мы формируем решения уравнений x2 = y1 и x2 = y2 в виде списков part1 и part2. Обе эти промежуточные переменные имеют тип List<Double> — в этом можно убедиться в IDE, поставив на них курсор ввода и нажав комбинацию клавиш Ctrl+Q. В последнем операторе return мы складываем два этих списка друг с другом: return part1 + part2, образуя таким образом третий список, содержащий в себе все элементы двух предыдущих.

Функцию biRoots можно несколько упростить, обратив внимание на то, что мы в ней четыре раза решаем одну и ту же задачу: поиск корней уравнения x2 = y. Для программиста такая ситуация должна сразу превращаться в сигнал — следует написать для решения этой задачи отдельную, более простую функцию:

fun sqRoots(y: Double) =
        if (y < 0) listOf()
        else if (y == 0.0) listOf(0.0)
        else {
            val root = sqrt(y)
            // Результат!
            listOf(-root, root)
        }

Посмотрите внимательнее на оператор if..else if..else. Первые две его ветки формируют результат сразу же, используя listOf() и listOf(0.0). А вот ветка else вначале создаёт промежуточную переменную root и уже потом формирует результат listOf(-root, root). Запомните: результат ветки в таких случаях формирует последний её оператор.

Эту же функцию можно переписать с использованием оператора when:

fun sqRoots(y: Double) =
        when {
            y < 0 -> listOf()
            y == 0.0 -> listOf(0.0)
            else -> {
                val root = sqrt(y)
                // Результат!
                listOf(-root, root)
            }
        }

С использованием sqRoots функция biRoots примет следующий вид:

fun biRoots(a: Double, b: Double, c: Double): List<Double> {
    if (a == 0.0) {
        return if (b == 0.0) listOf()
        else sqRoots(-c / b)
    }
    val d = discriminant(a, b, c)
    if (d < 0.0) return listOf()
    if (d == 0.0) return sqRoots(-b / (2 * a))
    val y1 = (-b + sqrt(d)) / (2 * a)
    val y2 = (-b - sqrt(d)) / (2 * a)
    return sqRoots(y1) + sqRoots(y2)
}

Из исходных 24 строчек осталось только 11, да и понимание текста функции стало существенно проще.

Напишем теперь тестовую функцию для проверки работы функции biRoots. Для этой цели последовательно решим с её помощью следующие уравнения:

  • 0x4 + 0x2 + 1 = 0 (корней нет)
  • 0x4 + 1x2 + 2 = 0 (корней нет)
  • 0x4 + 1x2 — 4 = 0 (корни -2, 2)
  • 1x4 — 2x2 + 4 = 0 (корней нет)
  • 1x4 — 2x2 + 1 = 0 (корни -1, 1)
  • 1x4 + 3x2 + 2 = 0 (корней нет)
  • 1x4 — 5x2 + 4 = 0 (корни -2, -1, 1, 2)
fun biRoots() {
    assertEquals(listOf<Double>(), biRoots(0.0, 0.0, 1.0))
    assertEquals(listOf<Double>(), biRoots(0.0, 1.0, 2.0))
    assertEquals(listOf(-2.0, 2.0), biRoots(0.0, 1.0, -4.0))
    assertEquals(listOf<Double>(), biRoots(1.0, -2.0, 4.0))
    assertEquals(listOf(-1.0, 1.0), biRoots(1.0, -2.0, 1.0))
    assertEquals(listOf<Double>(), biRoots(1.0, 3.0, 2.0))
    assertEquals(listOf(-2.0, -1.0, 1.0, 2.0), biRoots(1.0, -5.0, 4.0))
}

Обратите внимание, что здесь мы используем запись listOf<Double>() для создания пустого списка. Дело в том, что для вызовов вроде listOf(-2.0, 2.0) тип элементов создаваемого списка понятен из аргументов функции — это List<Double>. А вот вызов listOf() без аргументов не даёт никакой информации о типе элементов списка, в то же время, например, пустой список строк и пустой список целых чисел — с точки зрения Котлина не одно и то же.

Во многих случаях Котлин, тем не менее, может понять, о каком списке идёт речь. Например, функция biRoots имеет результат List<Double>, а значит, все списки, используемые в операторах return, должны иметь такой же тип. Случай с вызовом assertEquals, однако, не несёт достаточной информации, чтобы понять тип элементов, и мы вынуждены записать вызов функции более подробно — listOf<Double>(), указывая типовой аргумент <Double> между именем вызываемой функции и списком её аргументов в круглых скобках.

Запустим теперь написанную тестовую функцию. Мы получим проваленный тест из-за последней проверки:

org.opentest4j.AssertionFailedError: expected: <[-2.0, -1.0, 1.0, 2.0]> but was: <[-2.0, 2.0, -1.0, 1.0]>

То есть мы ожидали список корней -2, -1, 1, 2, а получили вместо этого -2, 2, -1, 1. Дело в том, что списки в Котлине считаются равными, если совпадают их размеры, и соответствующие элементы списков равны. Списки, состоящие из одних и тех же элементов, но на разных местах, считаются разными.

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

В обоих случаях нам придётся отсортировать список найденных корней перед сравнением. В Котлине это можно сделать, вызвав функцию .sorted():

fun biRoots() {
    // ...                                                               v
    assertEquals(listOf(-2.0, -1.0, 1.0, 2.0), biRoots(1.0, -5.0, 4.0).sorted())
}

В уроке 3 мы уже встречались с функциями с получателем .toInt() и .toDouble(). Функция .sorted() также требует наличия получателя: вызов list.sorted() создаёт список того же размера, что и исходный, но его элементы будут упорядочены по возрастанию.

Комментарии: 2
  1. Виктор Демихов

    Примеры не жизненные какие-то. Для математиков, а не для программистов.

    1. DegtjarenkoDW

      Прочел вашу фразу и вспомнил анекдот:
      Урок в первом классе :
      Учительница заносит и ставит на стол компьютер.
      — Дети сколько компьютеров на столе?
      — Один! — хором отвечают дети.
      Учительница заносит и ставит на стол еще один компьютер
      — Дети сколько компьютеров на столе?
      — Два! — хором отвечают дети.
      Идя за третьим компьютером учительница бурчит себе под нос :
      — с яблоками было легче.

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