高阶函数的基本概念
类似于数学中的高阶函数f(g(x)),高阶函数的概念是:
以函数作为参数或者返回值的函数
在Kotlin中,函数可以自由传递、赋值、在合适的时候调用,Lambda,并且赋值给一个变量,所有符合参数和返回值的任意Lambda以及函数都可以作为高阶函数的参数。
高阶函数中的函数引用
先看下面的例子:
fun main(args: Array<String>) {
//包级函数可以直接通过引用的方式来引用
args.forEach(::println)
//带有Receiver的引用,第一个参数就是实例
//实例::函数名字也可以引用成员方法
val t = Test()
args.forEach(t::println)
//扩展方法有一个隐含的参数--实例
//Kotlin1.1才开始支持
args.filter(String::isEmpty)
}
class Test {
fun println(any: Any) {
//写上全名防止冲突
kotlin.io.println(any)
}
}
总结一下:
- 包级函数作为高阶函数的参数的时候,直接按照参数、返回值来判断是否合适。
- 类的成员函数(带有Receiver的函数)作为高阶函数的参数的时候,需要使用实例来进行引用。
- 扩展方法作为高阶函数的参数的时候,需要注意的是扩展方法有一个默认的参数就是实例本身。
常用的高阶函数
forEach
forEach通常用于遍历集合,例如:
val list = listOf(1, 2, 3, 4, 5)
list.forEach(::println)
map
map通常用于集合的映射,例如:
val oldList = listOf(1, 2, 3, 4, 5)
val newList = ArrayList<Int>()
println("集合映射--传统写法")
oldList.forEach {
val newElement = it * 2 + 3
newList.add(newElement)
}
println("集合映射--Kotlin写法,使用map")
val newList1 = oldList.map {
it * 2 + 3
}
map还可以用于集合的转换,例如:
println("集合映射--转换")
val newList2 = oldList.map(Int::toDouble)
flatMap扁平化集合
flatMap通常用于扁平化集合,就是把集合的集合扁平化成集合,例如:
println("扁平化集合")
val list = listOf(
1..20,
2..5,
3..4
)
val newList3 = list.flatMap {
it
}
newList3.forEach(::println)
flatMap还可以结合map进行一些变换,例如:
println("扁平化集合+变换")
val newList4 = list.flatMap {
it.map {
"NO.$it"
}
}
newList4.forEach(::println)
reduce
reduce通常用于求和,例如:
println("求和")
var sum = oldList.reduce { acc, i -> acc + i }
println(sum)
reduce使用例子,求阶乘:
var res2 = (1..5).map(::factorial)
println(res2)
//求n的阶乘:n!=1×2×3×...×n
fun factorial(n: Int): Int {
if (n == 0) {
return 1
} else {
return (1..n).reduce { acc, i -> acc * i }
}
}
fold
fold通常用于求和并且加上一个初始值,例如:
sum = (1..5).fold(5) { acc, i -> acc + i }
println(sum)
因为fold不同于map,fold对初始值没有严格限制,因此fold还可以进行变换,例如下面这个字符串连接的例子:
var res = (1..5).fold(StringBuilder()) { acc, i ->
acc.append(i).append(",")
}
println(res)
//也可以这样写
var res1 = (1..5).joinToString(",")
println(res1)
同类型的还有foldRight等方法。
filter
filter用于过滤,如果传入的表达式的值为true的时候就保留:
//表达式为true的时候保留元素
val newList5 = oldList.filter {
it == 2 || it == 4
}
println(newList5)
filter还有一些类似的方法,例如带index的:
oldList.filterIndexed { index, i ->
index == 1
}
takeWhile
takeWhile通常用于带有条件的循环遍历,例如下面的例子就是从头遍历,当元素!=2的时候保留,知道元素==2的时候结束循环。因此结果就是[1, 3]
val oldList = listOf(1, 3, 2, 3, 4, 5)
val res = oldList.takeWhile {
it != 2
}
println(res)
同类的方法还有takeLastWhile,就是从尾部开始倒序遍历。
let、apply、with
使用let、apply可以简化代码
data class Person(val name: String, val age: Int) {
fun work() {
println("$name is working")
}
}
//返回可空类型的对象
fun findPerson(): Person? {
return null
}
fun main(args: Array<String>) {
val p = findPerson()
//通常我们每次调用的时候都需要带上?
println(p?.name)
println(p?.age)
//使用let之后,只在外层写一个?即可
p?.let {
println(it.name)
println(it.age)
it.work()
}
//使用apply之后,只在外层写一个?即可
//并且可以直接使用类的成员
p?.apply {
println(name)
println(age)
work()
}
}
同样地,还有一个with方法:
with(BufferedReader(FileReader("test.txt"))) {
var line: String?
while (true) {
line = readLine() ?: break
println(line)
}
close()
}
use
使用use可以简化一些Closeable的操作,例如close、try/catch,统一使用模板,例如上面的例子我们可以简化:
BufferedReader(FileReader("test.txt")).use {
var line: String?
while (true) {
line = it.readLine() ?: break
println(line)
}
}
尾递归优化
尾递归
先来看看什么是尾递归:
data class ListNode(val value: Int, var next: ListNode? = null)
tailrec fun findListNode(head: ListNode?, value: Int): ListNode? {
head ?: return null
if (head.value == value) {
return head
} else {
return findListNode(head.next, value)
}
}
像函数findListNode一样就是一种尾递归,调用完自己以后没有任何操作了,例如这里的直接return。
下面看两个不是尾递归的例子:
//不属于尾递归,因为调用完自己以后还有其它操作
fun factorial(n:Long):Long{
return n * factorial(n - 1)
}
data class TreeNode(val value: Int, var left: TreeNode? = null, var right: TreeNode? = null)
//不属于尾递归,因为调用完自己,还有可能再一次调用自己
fun findTreeNode(root: TreeNode?, value: Int): TreeNode? {
root ?: return null
if (root.value == value) {
return root
} else {
return findTreeNode(root.left, value) ?: return findTreeNode(root.right, value)
}
}
尾递归优化
Kotlin中,尾递归优化可以通过添加tailrec关键字实现:
tailrec fun findListNode(head: ListNode?, value: Int): ListNode? {
head ?: return null
if (head.value == value) {
return head
} else {
return findListNode(head.next, value)
}
}
编译器在遇到tailrec关键字的时候,会将尾递归优化为迭代,因此下面的调用是不会发生stack overflow的:
fun main(args: Array<String>) {
//已经优化为循环
val MAX_LIST_NODE_COUNT = 100000
val head = ListNode(0, null)
var p = head
for (i in 1..MAX_LIST_NODE_COUNT) {
p.next = ListNode(i, null)
p = p.next!!
}
val res = findListNode(head, MAX_LIST_NODE_COUNT - 4)
println(res!!.value)
}
Tips:不属于尾递归的话,添加tailrec关键字的时候IDE会提示不属于尾递归,添加tailrec关键字也没有用。
闭包
闭包:函数的运行环境,持有函数的运行状态,函数内部可以定义函数、类
闭包是支持函数式编程的基础。
fun main(args: Array<String>) {
val x = makeFun()
x()
x()
x()
x()
x.invoke()
//打印结果会不断++
}
//函数的状态、本地类、本地变量都可以得到保存
fun makeFun(): () -> Unit {
var count = 0
return fun() {
println(++count)
}
}
函数复合
函数复合,类似于数学中的f(g(x))或者g(f(x))的形式。
函数复合需要结合中缀表达式、FunctionN(扩展函数)的使用:
//f(x)
val add5 = { i: Int -> i + 5 }
//g(x)
val mul2 = { i: Int -> i * 2 }
infix fun <P1, P2, R> Function1<P1, P2>.addThen(function: Function1<P2, R>): Function1<P1, R> {
return fun(p1: P1): R {
return function(this(p1))
}
}
infix fun <P1, P2, R> Function1<P2, R>.compose(function: Function1<P1, P2>): Function1<P1, R> {
return fun(p1: P1): R {
return this(function(p1))
}
}
fun main(args: Array<String>) {
//m1(x) = f(g(x))
val add5AddThenMul2 = add5 addThen mul2
println(add5AddThenMul2(8))
println(mul2(add5(8)))
//m2(x) = g(f(x))
val add5ComposeMul2 = add5 compose mul2
println(add5ComposeMul2(8))
println(add5(mul2(8)))
}
科理化
多元函数变成一元函数调用链的形式,科理化只需要了解即可。例子如下:
fun log(tag: String, target: OutputStream, msg: Any) {
target.write("[$tag]:$msg\n".toByteArray())
}
//通过科理化将多元函数变成一元函数调用链
fun log(tag: String)
= fun(target: OutputStream)
= fun(msg: Any)
= target.write("[$tag]:$msg\n".toByteArray())
fun main(args: Array<String>) {
log("huannan", System.out, "我爱你")
log("huannan")(System.out)("哈哈哈")
}
当然,除了上面的手动科理化之外,我们也可以通过下面的扩展方法来实现科理化:
fun log(tag: String, target: OutputStream, msg: Any) {
target.write("[$tag]:$msg\n".toByteArray())
}
fun <P1, P2, P3, R> Function3<P1, P2, P3, R>.curried()
= fun(p1: P1)
= fun(p2: P2)
= fun(p3: P3)
= this(p1, p2, p3)
fun main(args: Array<String>) {
log("huannan", System.out, "我爱你")
::log.curried()("wuhuannan")(System.out)("嗯嗯嗯")
}
偏函数
偏函数就是一个多元函数传入了部分参数之后的得到的新的函数。
Tips:当然,我们也完全可以使用默认参数+具名参数的方式来实现参数的固定。如果需要固定的参数在中间,虽然说可以通过具名参数来解决,但是很尴尬,因为必须使用一大堆具名参数。因此偏函数就诞生了。
下面介绍通过科理化来实现偏函数:
fun log(tag: String, target: OutputStream, msg: Any) {
target.write("[$tag]:$msg\n".toByteArray())
}
fun <P1, P2, P3, R> Function3<P1, P2, P3, R>.curried()
= fun(p1: P1)
= fun(p2: P2)
= fun(p3: P3)
= this(p1, p2, p3)
fun main(args: Array<String>) {
//通过科理化之后给函数传入一些默认的参数
val logWithTag = ::log.curried()("wuhuannan")(System.out)
//偏函数
logWithTag("我爱你")
}
当然,也可以通过扩展函数的方式来实现:
fun makeString(byteArray: ByteArray, charset: Charset): String {
return String(byteArray, charset)
}
//第一个参数固定的偏函数
fun <P1, P2, R> Function2<P1, P2, R>.partial1(p1: P1) = fun(p2: P2) = this(p1, p2)
//第二个参数固定的偏函数
fun <P1, P2, R> Function2<P1, P2, R>.partial2(p2: P2) = fun(p1: P1) = this(p1, p2)
fun main(args: Array<String>) {
//通过偏函数固定第二个参数为charset("GBK"),其中charset是一个包级函数
val makeStringFromGBK = ::makeString.partial2(charset("GBK"))
makeStringFromGBK("我爱你".toByteArray())
}
小例子
高阶函数是函数式编程的基础,下面通过一个字符串个数统计的小例子进行总结:
fun main(args: Array<String>) {
File("test.txt")
.readText()
//过滤空白字符
.filterNot(Char::isWhitespace)
//以每个字符作为key,进行分组,返回的是map
.groupBy { it }
//映射,以分组出来的map的key作为key,以map的value的数量作为值
//返回的是一个新的map
.map {
it.key to it.value.size
}
.forEach(::println)
}
如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:
我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)。
网友评论