今天我们一起看看几个起到汇总作用的高阶函数。
count()
数组、集合、区间和序列都扩展了 count() 函数,它有两个重载形式,一是无参的形式,它返回总数,一般直接通过调用 size 属性实现;二是接受一个 Boolean 函数作为参数的形式,它 返回符合条件的元素数目。Array.count((T) -> Boolean) 的实现如下:
inline fun Array.count(predicate: (T) -> Boolean): Int {
var count = 0
for (element in this) if (predicate(element)) count++
return count
}
与我们在 Java 中手工写的循环完全一致。这个函数在使用时也很简单:
fun Int.countOfBinaryOnes() =
(0..31).count { (this shr it) and 1 == 1 }
我们这里写一个小函数,它扩展了 Int 类型,返回这个数字二进制中 1 的数量。如果用 Java 写,需要这样:
int count = 0;
for (int i = 0; i < 32; i++)
if (((num >> i) & 1) == 1)
count++;
return count;
从处理函数的长度来说,Java 因为支持位运算符,比 Kotlin 用中缀函数的形式要短一些。不过,Java 需要定义一个临时变量 count ,也没有封装循环,比 Kotlin 函数式编程更长也更容易出错。
joinTo() 与 joinToString()
joinToString() 函数是对 joinTo() 函数的封装,可以方便地联结字符串,我们看一下 Array.joinTo() 函数的参数列表和实现:
fun Array.joinTo(
buffer: A,
separator: CharSequence = ", ",
prefix: CharSequence = "",
postfix: CharSequence = "",
limit: Int = -1,
truncated: CharSequence = "...",
transform: ((T) -> CharSequence)? = null
): A {
buffer.append(prefix)
var count = 0
for (element in this) {
if (++count > 1) buffer.append(separator)
if (limit < 0 || count <= limit) {
buffer.appendElement(element, transform)
} else break
}
if (limit >= 0 && count > limit) buffer.append(truncated)
buffer.append(postfix)
return buffer
}
它总共有 7 个参数,除了第一个 Appendable 类型的 buffer 参数外,后面 6 个参数都有默认值。我们依次看看它们怎么用:buffer:Appendable 类型,写入数据的目标。java.lang.Appendable 接口表示“可追加字符序列”的对象,常用的 StringBuilder、StringBuffer、Writer、CharBuffer 都实现了这个接口。这里用到的 Appendable.appendElement(T, ((T) -> CharSequence)?) 是 Kotlin 为 Appendable 接口扩展的高阶函数,会根据 tranform 函数处理 element 并追加到 Appendable 里。
separator:CharSequence 类型,默认为 ", ",分隔各元素的字符串。
prefix、postfix:CharSequence 类型,默认为空字符串,目标的前缀和后缀。
limit:Int 类型,默认为 -1,联结元素限制数。
根据源码发现:当 limit < 0 或 limit > 元素数时,联结所有元素,不添加删节符号;当 limit = 0 时,联结所有元素,添加删节符号;当 0 < limit < 元素数时,联结前 limit 个元素,并在最后添加删节符号。
truncated:CharSequence 类型,默认为 "...",删节符号,与 limit 配合使用。
transform:((T) -> CharSequence)? 类型,默认为 null,处理元素的函数。每个元素会经这个函数处理后添加到 buffer 内。
比起 joinTo(),joinToString() 更加常用,它是对 joinTo() 的封装:
fun Array.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}
比如,可以这样列出文件夹内的所有文件:
fun File.listFilesReadable(): String {
return if (isDirectory)
listFiles().joinToString("\n") { it.name }
else name
}
如果被扩展的 File 对象是文件夹,就调用 listFiles() 方法列出文件夹内的所有文件,再用 \n 字符将文件名联结起来;如果是文件则返回文件名。
sumBy() 和 sumByDouble()
这两个函数很简单,都接受一个函数,将元素转换为数字再加和。我们看一下 Array.sumBy() 函数的实现:
inline fun Array.sumBy(selector: (T) -> Int): Int {
var sum: Int = 0
for (element in this) {
sum += selector(element)
}
return sum
}
sumByDouble() 则是把 sum 变量定义为 Double 类型,实现方式是一样的。
此外,要进行简单的汇总,只需要调用 sum() 函数即可。
最后来个复杂一丁点的例子吧:给 File 类型扩展一个函数 fullSize(),返回这个 File 的总体积。
这个问题需要处理是否为 文件夹 的情况,要遍历文件夹内文件并返回体积之和,我的实现如下:
fun File.fullSize(): Long {
return if (isDirectory)
listFiles().map { it.fullSize() }.sum()
else length()
}
这里用迭代处理文件夹,如果是文件夹则将子文件的 fullSize() 映射到一个 List 里,然后用 sum() 求和。可以思考一下,我们为什么不用 sumBy() 函数呢?因为 sumBy() 函数返回 Int 类型,而 File.length() 返回 Long 类型,可能发生溢出,我们这里用的 map() 和 sum() 都提供了 Long 类型返回值的重载,没有溢出的危险。