Операции объединения
Коллекции 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()
:
* reduceOrNull()
* reduceRightOrNull()
* reduceIndexedOrNull()
* reduceRightIndexedOrNull()
Для случаев, когда вы хотите сохранить промежуточное накопленное значение, существуют функции
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()
.