Операции объединения
Коллекции 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().