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

Функции высшего порядка над списками

Вернёмся ещё раз к задаче формирования списка из отрицательных чисел в исходном списке. На Котлине, данная задача допускает ещё и такое, очень короткое решение:

fun negativeList(list: List<Int>) = list.filter { it < 0 }

Это короткое решение, однако, является довольно ёмким в плане его содержания. Попробуем в нём разобраться.

list.filter — это один из примеров так называемой функции высшего порядка. Суть функции filter в том, что она фильтрует содержимое списка-получателя. Её результатом также является список, содержащий все элементы списка-получателя, удовлетворяющие определённому условию.

Как же она это делает и что такое вообще функция высшего порядка? Это тоже функция, которая, однако, принимает в качестве параметра другую функцию. Более подробная запись вызова filter выглядела бы так:

fun negativeList(list: List<Int>) = list.filter(fun(it: Int) = it < 0)

Функция-аргумент в данном случае должна иметь параметр it того же типа, что и элементы списка, и результат типа Boolean. В этой записи она отличается от обычной функции только отсутствием имени. Функция filter передаёт функции-аргументу каждый элемент списка. Если функция-аргумент вернула true, элемент помещается в список-результат, если false — он фильтруется.

Более короткая запись list.filter({ it < 0 }) использует так называемую лямбду { it < 0 } в качестве аргумента функции filter. Этот краткий синтаксис не включает в себя не только имени функции, но и ключевого слова fun, а также явного указания имён и типов параметров. При этом предполагается, что:

  • параметр называется it; если параметру хочется дать другое имя, лямбда записывается как, например, { element -> element < 0 }
  • тип параметра — ровно тот, который требуется функции высшего порядка, для filter это тип элементов списка
  • тип результата — опять-таки ровно тот, что требуется
  • в фигурные скобки помещается блок, определяющий результат функции; в идеале он состоит из одного оператора, в данном случае это it < 0

Наконец, в том случае, если лямбда является последним аргументом функции, при вызове функции разрешается вынести её за круглые скобки: list.filter() { it < 0 }. Если других аргументов у функции нет, разрешается опустить в этой записи круглые скобки, получив запись из исходного примера: list.filter { it < 0 }

Функции высшего порядка с первого взгляда могут показаться очень сложными, но реально это довольно простая вещь, позволяющая свести запись алгоритмов к более компактной. Рассмотрим другую типичную задачу: из имеющегося массива целых чисел сформировать другой массив, содержащий квадраты чисел первого массива. Задача решается в одну строчку с помощью функции высшего порядка map:

fun squares(list: List<Int>) = list.map { it * it }

list.map предназначена для преобразования списка list в другой список такого же размера, при этом над каждым элементом списка list выполняется преобразование, указанное в функции-аргументе map. Тип параметра функции-аргумента совпадает с типом элементов списка list, а вот тип результата может быть произвольным. Например, преобразование list.map { "$it" } создаст из списка чисел вида [0, 1, 2] список строк [«0», «1», «2»].

Чуть более сложный пример: проверка числа на простоту.

fun isPrime(n: Int) = n >= 2 && (2..n/2).all { n % it != 0 }

Функция высшего порядка all в данном примере вызывается для получателя-интервала: 2..n/2. Применима она и для списка, как и для любого другого объекта, элементы которого можно перебрать с помощью for. Функция allимеет логический результат и возвращает true, если функция-аргумент возвращает true для ВСЕХ элементов списка. Тип параметра функции-аргумента совпадает с типом элементов списка, тип результата — Boolean. Аналогично можно было бы применить функцию высшего порядка any:

fun isNotPrime(n: Int) = n < 2 || (2..n/2).any { n % it == 0 }

Функция высшего порядка any возвращает true, если функция-аргумент возвращает true ХОТЯ БЫ для одного элемента списка.

Наконец, функция высшего порядка fold предназначена для «сворачивания» списка в один элемент или значение. Например:

fun multiplyAll(list: List<Int>) = list.fold(1.0) {
    previousResult, element -> previousResult * element
}

Функция fold работает следующим образом. Изначально она берёт свой первый аргумент (в данном примере 1.0) и сохраняет его как текущий результат. Далее перебираются все элементы списка получателя и для каждого из них применяется указанная лямбда, которая из текущего результата previousResult с предыдущего шага и очередного элемента element делает текущий результат этого шага (в данном примере предыдущий результат домножается на очередной элемент). По окончании элементов списка последний текущий результат становится окончательным. В данном примере результатом будет произведение всех элементов списка (или 1.0, если список пуст).

Строки

Строки String во многих отношениях подобны спискам, хотя формально и не являются ими. Список состоит из однотипных элементов, к которым можно обращаться по индексу и перебирать с помощью цикла for. Строки же точно так же состоят из символов Char, к которым также можно обращаться по индексу и которые также можно перебирать с помощью цикла for.

Напомним, что строковый литерал (явно указанная строка) в Котлине записывается в двойных кавычках. Переменную name произвольного типа можно преобразовать в строку, используя запись "$name" — строковый шаблон, или чуть более сложную запись name.toString() с тем же самым результатом.

Как мы видим, $ внутри строкового литерала имеет специальный смысл — вместо $name в строку будет подставлено содержимое переменной name. Как быть, если мы хотим просто включить в строку символ доллара? В этом случае следует применить так называемое экранирование, добавив перед символом доллара символ \. Например: "The price is 9.99 \$".

Экранирование может применяться и для добавления в строку различных специальных символов, не имеющих своего обозначения либо имеющих специальный смысл внутри строкового литерала. Например: \n — символ новой строки, \t — символ табуляции, \\ — символ «обратная косая черта», \" — символ «двойная кавычка».

Строки в Котлине являются неизменяемыми, то есть изменить какой-либо символ в уже созданной строке нельзя, можно только создать новую. В этом смысле они аналогичны немутирующим спискам List.

Перечислим наиболее распространённые операции над строками:

  1. string1 + string2 — сложение или конкатенация строк, приписывание второй строки к первой.
  2. string + char — сложение строки и символа (с тем же смыслом).
  3. string.length — длина строки, то есть количество символов в ней.
  4. string.isEmpty()string.isNotEmpty() — получение признаков пустоты и непустоты строки (Boolean).
  5. string[i] — индексация, то есть получение символа по целочисленному индексу (номеру) i в диапазоне от 0 до string.length - 1.
  6. string.substring(from, to) — создание строки меньшего размера (подстроки), в который войдут символы строки string с индексами fromfrom + 1, …​, to - 2to - 1. Символ с индексом to не включается.
  7. char in string — проверка принадлежности символа char строке string.
  8. for (char in list) { …​ } — цикл for, перебирающий все символы строки string.
  9. string.first() — получение первого символа строки.
  10. string.last() — получение последнего символа строки.
  11. string.indexOf(char, startFrom) — найти индекс первого символа char в строке, начиная с индекса startFrom.
  12. string.lastIndexOf(char, startFrom) — найти индекс первого символа char в строке, идя с конца и начиная с индекса startFrom.
  13. string.toLowerCase() — преобразование строки в нижний регистр (то есть, замена прописных букв строчными).
  14. string.toUpperCase() — преобразование строки в верхний регистр (замена строчных букв прописными).
  15. string.capitalize() — замена первой буквы строки прописной.
  16. string.trim() — удаление из строки пробельных символов в начале и конце: " ab c " преобразуется в "ab c"

В качестве примера рассмотрим функцию, проверяющую, является ли строка палиндромом. В палиндроме первый символ должен быть равен последнему, второй предпоследнему и т.д. Пример палиндрома: «А роза упала на лапу Азора». Из этого примера видно, что одни и те же буквы в разном регистре следует считать равными с точки зрения данной задачи. Кроме этого, не следует принимать во внимание пробелы. Решение на Котлине может быть таким:

fun isPalindrome(str: String): Boolean {
    val lowerCase = str.toLowerCase().filter { it != ' ' }
    for (i in 0..lowerCase.length / 2) {
        if (lowerCase[i] != lowerCase[lowerCase.length - i - 1]) return false
    }
    return true
}

Обратите внимание, что мы с самого начала переписываем исходную строку str в промежуточную переменную lowerCase, преобразуя все буквы в нижний регистр и удаляя из строки все пробелы. Функция filter работает для строк точно так же, как и для списков — в строке оставляются только те символы, для которых функция-аргумент { it != ' '} вернёт true. Затем мы перебираем символы в первой половине строки, сравнивая каждый из них с символом из второй половины. Символ с индексом 0 (первый) должен соответствовать символу с индексом length - 1(последнему) и так далее.

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

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

    1. DegtjarenkoDW

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

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