Kotlin学习笔记(五)-常见高阶函数

2019-12-19 19:20:04 浏览数 (1)

[toc]

前言

这一节我们主要说下Kotlin中关于数据集合中的常用高阶函数

map

map是遍历一个数组遍历的过程可以对数组item进行操作(筛选、数据转换等) ,返回一个新的数据集合 例子:

代码语言:javascript复制
 val list = listOf(2, 8, 4, 5, 9, 7)
 //Kotlin 写法 等价于 newList的转化
 val newList1 = list.map {
        it * 3   2
    }
flatmap

就是把几个小的list转换到一个大的list中 例子:

代码语言:javascript复制
    val flatList = listOf(
        2..10,
        5..25,
        100..200
    )
    //flatten()  flatMap方法中无其他操作可以用flatten()
    val flatMapList = flatList.flatMap { intRange: IntRange ->
        intRange
    }

嵌套使用:

代码语言:javascript复制
    //上面flatMapList2表达式的完整写法
    val flatMapList3 = flatList.flatMap(fun(intRange: IntRange): List<String> {
        return intRange.map(fun(intElement: Int): String {
            return "No.$intElement"
        })
    })
reduce

求list的和、求阶乘 求和:

代码语言:javascript复制
/reduce求list的和 acc是累加的结果 i是每次遍历出来的元素
    val int: Int = list.reduce { acc, i -> acc   i }

求阶乘:

代码语言:javascript复制
    //0->0 1->(1*1)*1 2->(1*1)*2 3->(1*2)*3
    (0..6).map(::factorial).forEach(::println)
    
    fun factorial(n: Int): Int {
    if (n == 0) return 1
    //相当于 n=3是 1*1,(1 2)*2,(1 2 3)*3,(1 2 3 4)*4
    return (1..n).reduce { acc, i -> acc * i }
    }
fold

是带初始值的reduce 相对更强大,且对返回值无要求

代码语言:javascript复制
 println((0..6).map(::factorial).fold(100) { acc, i -> acc   i })//100 873=973

字符串拼接: 这里传入的类型初始值是StringBuilder()

代码语言:javascript复制
    println((0..6).map(::factorial).fold(StringBuilder())
    { acc, i -> acc.append(i).append(",") })
joinToString

字符串拼接

代码语言:javascript复制
 println((0..6).joinToString("/", ".", ";"))
filter/takeWhile

根据条件筛选

代码语言:javascript复制
    println((0..6).map(::factorial))
    println((0..6).map(::factorial).filter { it % 2 == 1 })
    println((0..6).map(::factorial).takeWhile { it < 130 })//遇到第一个不满足条件的停止输出
尾递归优化

Kotlin 支持一种称为尾递归的函数式编程⻛格。这允许一些通常用循环写的算法改用递归函数来写,而无堆栈溢出的⻛险。当一个函数用tailrec修饰符标记并满足所需的形式时,编译器会优化该递归,留下一个快速而高效的基于循环的版本。

这是官网的说法。反正我是觉得有些晦涩。我的理解,首先理解什么是尾递归。下看下下面的三个例子:

代码语言:javascript复制
data class TreeNode(val value: Int) {
    var left: TreeNode? = null
    var right: TreeNode? = null
}


//尾递归
tailrec fun findListNode(head: ListNode?, value: Int): ListNode? {
    head ?: return null
    if (head.value == value) return head
    return findListNode(head.next, value)
}

//返回中存在 * 运算 所以是非尾递归
fun factorial(n: Long): Long {
    return n * factorial(n - 1)
}


//这个也是非 尾递归
fun findTreeNode(root: TreeNode?, value: Int): TreeNode? {
    root ?: return null
    if (root.value == value) return root
    return findTreeNode(root.left, value) ?: findTreeNode(root.right, value)

}

调用完自己之后没有任何操作的递归就是尾递归尾递归优化就是在方法_上加tailrec关键地提示编译器进行优化(将递归转化味迭代进行处理)

若非尾递归加上tailrec也会提示(提示黄色警告)。

闭包

在函数为一等公民的语言中,都具有闭包的特性。我的理解就是函数里面声明函数,函数里面返回函数,这就是闭包。在Java中调用完方法,方法内部的状态是不会被记住的,但是在Kotlin中,函数的状态在调用后不会被销毁。闭包有点像java的内部类,内部类持有外部类的引用,会导致外部类无法释放,也就是java中的内存泄漏。我个人觉的在Kotlin中闭包也会带来消耗。

  1. 函数的运行环境
  2. 持有函数运行状态
  3. 函数内部可以定义函数
  4. 函数内部也可以定义类
复合函数

本身不是语法上的关键字或是格式,是按照以前现有的知识,只不过在编写上有点难以理解。这个只是函数的复合 没有新的知识点 结合例子说明:

代码语言:javascript复制
val add5 = { i: Int -> i   5 }//g(x)

val multiplyBy2 = { i: Int -> i * 2 }//f(x)
fun main(args: Array<String>) {

    println(multiplyBy2(add5(8)))

    val add5AndMultiplyBy2 = add5 andThen multiplyBy2 //m(x)=f(g(x))  2*(8 5)=26
    println(add5AndMultiplyBy2(8))

    val add5AndMultiplyByCopy = multiplyBy2 andThen add5//m(x)=g(f(x))  2*8 5=21//前后参数类型相同可以置换位置 否则是不可以的 所以置换后的结果也是不同的
    println(add5AndMultiplyByCopy(8))

    val add5ComposeThen = add5 compose multiplyBy2
    println(add5ComposeThen(8))//m(x)=g(f(x)) 21

    val complexFunX = funFx complexFun funGxy
//    val complexFunXCopy =funGxy  complexFun funFx //这个就不可以 类型参数是要根据条件
    println(complexFunX(3, 2))//3*3 50 2=61
}


//m(x)=f(g(x))   add5  andThen multiplyBy2相当于g(x).andThen(f(g(x)))=Function1<P1, P2>.andThen(f(g(x)))
//复合函数 扩展Function1的扩展方法 infix 中缀表达式
//Function1 传入1个参数的函数 P1 接收的参数类型 P2返回的参数类型
//扩展方法andThen接收 一个参数的函数 他的参数 是add5的返回值 再返回最终结果
//andThen左边的函数  Function1<P1,P2> 接收一个参数P1 返回结果P2
//andThen右边的函数 function:Function1<P2,R> 参数为左边函数的返回值P2 返回结果R
//聚合的结果返回函数Function1<P1,R> 是以P1作为参数 R做为结果的函数
//相当于P1,P2 聚合 P2,R 返回 P1,R
//f(g(x))  P1相当于x P2 相当于g(x)返回值 返回的结果Function1<P1,R> R相当于f(g(x)) 的返回值
//Function1<P1,P2> 相当于g(x)
//function:Function1<P2,R> 相当于x
//
infix fun <P1, P2, R> Function1<P1, P2>.andThen(function: Function1<P2, R>): Function1<P1, R> {
    return fun(p1: P1): R {
        return function.invoke(this.invoke(p1))
    }
}

//compose左边函数接收参数P2 返回R
//compse右边函数 接收参数P1 返回P2
//返回结果函数P1,R
//相当于先执行右边返回了P1,P2  在执行P2,R函数 聚合成P1,R
//g(f(x))
//f(x).compose(g(f(x)))
infix fun <P1, P2, R> Function1<P2, R>.compose(function: Function1<P1, P2>): Function1<P1, R> {
    return fun(p1: P1): R {
        return this.invoke(function.invoke(p1))
    }
}

//课外扩展  m(x,y) = f(g(x,y)
val funFx = { i: Int -> i   2 }
val funGxy = { i: Int, j: Int -> 3 * i   100 / j }

//m(x,y) = f(g(x,y))
infix fun <P1, P2, P3, R> Function1<P3, R>.complexFun(function: Function2<P1, P2, P3>): Function2<P1, P2, R> {
    return fun(p1: P1, p2: P2): R {
        return this.invoke(function.invoke(p1, p2))
    }
}
柯里化函数(currying) -函数的链式调用
  • 柯里化函数就是把多个函数转话成一个一个参数传入
  • 柯里化就是将具有多个参数的函数,变成多个单个参数的函数,然后链式调用。注意调用时参数的顺序不能颠倒

个人觉得 柯里化的意义在于:允许调用者分段调用。因为Kotlin是函数为一等公民的语言。那么假设有一个方法需要传10个参数,可能A模块传了2个,然后返回函数,B模块调用A模块的方法并将其8个参数补齐,并真正使用。 例子:

代码语言:javascript复制
//正常下的函数编写:  
fun log1(tag: String, target: OutputStream, message: Any?) {
    target.write("[$tag] $messagen".toByteArray())
}

上面函数变化:

代码语言:javascript复制
//这是另外一种表达方式 与之前的函数表达结果相同
fun log2(tag: String) = fun(target: OutputStream) = fun(message: Any?) = target.write("[$tag] $messagen".toByteArray())

这就是柯里化函数。

再讲将新的函数表达抽象就变成柯里化函数

代码语言:javascript复制
//kotlin中柯里化链式调用的含义
fun <P1, P2, P3, R> Function3<P1, P2, P3, R>.curried() = fun(p1: P1) = fun(p2: P2) = fun(p3: P3) = this(p1, p2, p3)

调用:

代码语言:javascript复制
// ::log1与 { tag: String, target: OutputStream, message: Any? -> log1(tag, target, message) } 是等价的 表示对函数的引用
//    { tag: String, target: OutputStream, message: Any? -> log1(tag, target, message) }.curried()("ggxiaozhi")(System.out)("Hello World!")
    log1("ggxiaozhi", System.out, "Hello World!")
    log2("ggxiaozhi")(System.out)("Hello World!!")
    //一个函数的参数复合柯里化版本 那么就可以使用::方法名字 如:::log1 拿到引用使用.curried()方法
    ::log1.curried()("ggxiaozhi")(System.out)("Hello World!!!")

这里封装成扩展方法,是为了方便以后调用

偏函数

偏函数其实就是给多个参数的函数设置默认参数,那么再使用的时候只需要传入部分参数即可。

在上面柯里化函数的例子中,如果默认参数在前面,也可以使用偏函数,如:

代码语言:javascript复制
    val consoleLogWithTag = (::log1.curried())("ggxiaozhi")(System.out)
    consoleLogWithTag("Hello World Tag")//偏函数

consoleLogWithTag方法就是一个偏函数。首先经过柯里化后,将第一个参数和第二个参数固定得到consoleLogWithTag一个新的函数。那个这个函数其实就是偏函数

所以偏函数与柯里化函数存在一定的联系,当柯里化函数最前面的参数想设置默认值的时候可以使用偏函数

下面我们来看下真正的偏函数:

代码语言:javascript复制
   //partial2
    val bytes = "我是中国人".toByteArray(charset("GBK"))
    val stringFormGBK = makeStringFromGBKBytes(bytes)
    println(stringFormGBK)

    //partial1
    val stringFormGBKP1=makeStringFromGBKBytesp1(charset("GBK"))
    println(stringFormGBKP1)
    
    //偏函数  1-3
fun <P1, P2, R> Function2<P1, P2, R>.partial2() = fun(p2: P2) = fun(p1: P1) = this(p1, p2)//第一个参数默认 传入第二个参数

fun <P1, P2, R> Function2<P1, P2, R>.partial1() = fun(p1: P1) = fun(p2: P2) = this(p1, p2)//第二个参数默认 传入第一个

完全可以使用默认参数 具名参数的方式来实现参数的固定。如果需要固定的参数在中间,虽然说可以通过具名参数来解决,但是很尴尬,因为必须使用一大堆具名参数。因为默认参数你不传就用默认参数,但是你传入了,如果不使用具名参数那么函数就会以为你传参数的位置是要覆盖默认参数,所以必须具名函数因此偏函数就诞生了。偏函数就是一个多元函数传入了部分参数之后的得到的新的函数。

总结:

  1. 当柯里化后的函数 如果默认函数位置在参数的前面 那么 可以直接使用偏函数
  2. 如果函数的默认函数在气其他位置 那么可以使用扩展方法 FunctionN 来实现

0 人点赞