Введение
Начиная с этого раздела мы переходим к изучению составных типов данных, включающих в себя несколько элементов простых типов. Такие типы очень часто необходимы в программировании. Вспомним, например, задачу про поиск минимального корня биквадратного уравнения 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 }
Данное решение построено по алгоритму, приведённому в конце второго урока, с той лишь разницей, что здесь мы ищем все имеющиеся корни:
- Первый if рассматривает тривиальный случай a = 0 и более простое уравнение bx2 = -c. Оно либо не имеет корней (с / b > 0), либо имеет один корень 0 (c / b = 0), либо два корня (c / b < 0).
- Затем мы делаем замену y = x2 и считаем дискриминант d = b2 — 4ac. Если он отрицателен, уравнение не имеет корней.
- Если дискриминант равен 0, уравнение ay2 + by + c = 0 имеет один корень. В зависимости от его знака, биквадратное уравнение либо не имеет корней, либо имеет один корень 0, либо имеет два корня.
- В противном случае дискриминант положителен и уравнение 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()
создаёт список того же размера, что и исходный, но его элементы будут упорядочены по возрастанию.
Примеры не жизненные какие-то. Для математиков, а не для программистов.
Прочел вашу фразу и вспомнил анекдот:
Урок в первом классе :
Учительница заносит и ставит на стол компьютер.
— Дети сколько компьютеров на столе?
— Один! — хором отвечают дети.
Учительница заносит и ставит на стол еще один компьютер
— Дети сколько компьютеров на столе?
— Два! — хором отвечают дети.
Идя за третьим компьютером учительница бурчит себе под нос :
— с яблоками было легче.