Skip to content

Latest commit

 

History

History
208 lines (174 loc) · 17.5 KB

collection-aggregate.md

File metadata and controls

208 lines (174 loc) · 17.5 KB
type layout category title url
doc
reference
Collections
Объединение

Операции объединения

Коллекции Kotlin содержат функции для часто используемых операций объединения (ориг. aggregate operations), которые возвращают одно значение на основе содержимого коллекции. Большинство из них хорошо известны и работают так же, как и на других языках:

  • minOrNull() и maxOrNull() возвращают наименьший и наибольший элемент коллекции соответственно. Если коллекция пуста, то вернётся null.
  • average() возвращает среднее значение элементов в коллекции чисел.
  • sum() возвращает сумму элементов в коллекции чисел.
  • count() возвращает количество элементов в коллекции.
fun main() {
    val numbers = listOf(6, 42, 10, 4)

    println("Count: ${numbers.count()}") // Count: 4
    println("Max: ${numbers.maxOrNull()}") // Max: 42
    println("Min: ${numbers.minOrNull()}") // Min: 4
    println("Average: ${numbers.average()}") // Average: 15.5
    println("Sum: ${numbers.sum()}") // Sum: 62
}

Также существуют функции для получения самых маленьких и самых больших элементов в коллекции с помощью определённой функции-селектора или пользовательского Comparator:

  • maxByOrNull() и minByOrNull() принимают функцию-селектор и возвращают элемент, для которого эта функция возвращает наибольшее или наименьшее значение.
  • maxWithOrNull() и minWithOrNull() принимают объект Comparator и возвращают наибольший или наименьший элемент, соответствующий этому Comparator.

Если коллекция пуста, то эти функции вернут null. У функций maxByOrNull() и minByOrNull() есть аналоги: maxOf() и minOf(), которые работают аналогично, но бросают исключение NoSuchElementException, если коллекция пуста.

fun main() {
    val numbers = listOf(5, 42, 10, 4)
    val min3Remainder = numbers.minByOrNull { it % 3 }
    println(min3Remainder) // 42

    val strings = listOf("one", "two", "three", "four")
    val longestString = strings.maxWithOrNull(compareBy { it.length })
    println(longestString) // three
}

Помимо обычного sum(), существует расширенная функция sumOf(). Она принимает функцию-селектор, которая применяет заданную операцию к каждому элементу коллекции, и возвращает сумму всех элементов с учётом этих изменений. Функция-селектор может возвращать различные числовые типы: Int, Long, Double, UInt, и ULong (а также BigInteger и BigDecimal из JVM).

fun main() {
    val numbers = listOf(5, 42, 10, 4)
    println(numbers.sumOf { it * 2 }) // 122
    println(numbers.sumOf { it.toDouble() / 2 }) // 30.5
}

Fold и reduce

Для более специфичных случаев существуют функции reduce() и fold(), которые последовательно применяют предоставленную операцию к элементам коллекции и возвращают накопленный результат. Операция принимает два аргумента: ранее накопленное значение и элемент коллекции.

Разница между этими двумя функциями состоит в том, что fold() принимает начальное значение и использует его как накопленное значение на первом шаге, тогда как reduce() на первом шаге в качестве аргументов операции использует первый и второй элементы.

fun main() {
    val numbers = listOf(5, 2, 10, 4)

    val simpleSum = numbers.reduce { sum, element -> sum + element }
    println(simpleSum) // 21
    val sumDoubled = numbers.fold(0) { sum, element -> sum + element * 2 }
    println(sumDoubled) // 42

    // некорректно: первый элемент не будет удвоен
    // val sumDoubledReduce = numbers.reduce { sum, element -> sum + element * 2 }
    // println(sumDoubledReduce)
}

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

Существуют функции reduceRight() и foldRight(), которые работают аналогично fold() и reduce(), но в обратном порядке: начинают с последнего элемента, а затем переходят к предыдущему. Обратите внимание, что при использовании foldRight() или reduceRight() аргументы операции меняются местами: сначала идёт элемент, а затем накопленное значение.

fun main() {
    val numbers = listOf(5, 2, 10, 4)
    val sumDoubledRight = numbers.foldRight(0) { element, sum -> sum + element * 2 }
    println(sumDoubledRight) // 42
}

Также вы можете использовать функции reduceIndexed() и foldIndexed(), которые дополнительно дают доступ к индексам элементов в качестве первого аргумента операции. Либо функции reduceRightIndexed() и foldRightIndexed(), которые работают аналогично, но в обратном порядке.

fun main() {
    val numbers = listOf(5, 2, 10, 4)
    val sumEven = numbers.foldIndexed(0) { idx, sum, element -> if (idx % 2 == 0) sum + element else sum }
    println(sumEven) // 15

    val sumEvenRight = numbers.foldRightIndexed(0) { idx, element, sum -> if (idx % 2 == 0) sum + element else sum }
    println(sumEvenRight) // 15
}

Все reduce-операции бросают исключение, если коллекция пуста. Чтобы вместо исключения возвращался null, используйте их аналоги с постфиксом *OrNull():

Для случаев, когда вы хотите сохранить промежуточное накопленное значение, существуют функции runningFold() (или её синоним scan()) и runningReduce().

fun main() {
    val numbers = listOf(0, 1, 2, 3, 4, 5)
    val runningReduceSum = numbers.runningReduce { sum, item -> sum + item }
    val runningFoldSum = numbers.runningFold(10) { sum, item -> sum + item }

    val transform = { index: Int, element: Int -> "N = ${index + 1}: $element" }
    println(runningReduceSum.mapIndexed(transform).joinToString("\n", "Sum of first N elements with runningReduce:\n"))
    println(runningFoldSum.mapIndexed(transform).joinToString("\n", "Sum of first N elements with runningFold:\n"))

    // В логе будет:
    // Sum of first N elements with runningReduce:
    // N = 1: 0
    // N = 2: 1
    // N = 3: 3
    // N = 4: 6
    // N = 5: 10
    // N = 6: 15
    // Sum of first N elements with runningFold:
    // N = 1: 10
    // N = 2: 10
    // N = 3: 11
    // N = 4: 13
    // N = 5: 16
    // N = 6: 20
    // N = 7: 25
}

Если вам нужен индекс в качестве параметра операции, используйте runningFoldIndexed() или runningReduceIndexed().