Kotlin 之旅5 高阶函数

####高阶函数的基本概念

类似于数学中的高阶函数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)
    }
}
复制代码

总结一下:

  1. 包级函数作为高阶函数的参数的时候,直接按照参数、返回值来判断是否合适。
  2. 类的成员函数(带有Receiver的函数)作为高阶函数的参数的时候,需要使用实例来进行引用。
  3. 扩展方法作为高阶函数的参数的时候,需要注意的是扩展方法有一个默认的参数就是实例本身。

####常用的高阶函数

#####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,我拉你进群交(♂)流(♀)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值