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

[toc]

前言

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

map

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

 val list = listOf(2, 8, 4, 5, 9, 7)
 //Kotlin 写法 等价于 newList的转化
 val newList1 = list.map {
        it * 3 + 2
    }
flatmap

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

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

嵌套使用:

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

求list的和、求阶乘
求和:

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

求阶乘:

    
    //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 相对更强大,且对返回值无要求

 println((0..6).map(::factorial).fold(100) { acc, i -> acc + i })//100+873=973

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

    println((0..6).map(::factorial).fold(StringBuilder())
    { acc, i -> acc.append(i).append(",") })
joinToString

字符串拼接

 println((0..6).joinToString("/", ".", ";"))
filter/takeWhile

根据条件筛选

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

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

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

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. 函数内部也可以定义类
复合函数

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

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个参数补齐,并真正使用。
例子:

//正常下的函数编写:  
fun log1(tag: String, target: OutputStream, message: Any?) {
    target.write("[$tag] $message\n".toByteArray())
}

上面函数变化:

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

这就是柯里化函数。

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

//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)

调用:

// ::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!!!")

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

偏函数

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

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

    val consoleLogWithTag = (::log1.curried())("ggxiaozhi")(System.out)
    consoleLogWithTag("Hello World Tag")//偏函数

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

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

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

   //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 来实现

结语

下篇我们说下反射和泛型

Github源码直接运行,包含全部详细笔记

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 原文链接:https://github.com/EasyKotlin 值就是函数,函数就是值。所有函数都消费函数,...
    JackChen1024阅读 5,836评论 1 17
  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,348评论 0 5
  • 函数参数的默认值 基本用法 在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。 上面代码检查函数l...
    陈老板_阅读 433评论 0 1
  • 1.函数参数的默认值 (1).基本用法 在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
    赵然228阅读 648评论 0 0
  • 一、函数参数的默认值 1.1、基本用法 ES6 允许为函数的参数设置默认值,直接写在参数定义的后面 ES6 的写法...
    了凡和纤风阅读 247评论 0 0