Kotlin系列 - 高阶函数与标准库中的常用函数(三)

Kotlin细节文章笔记整理更新进度:
Kotlin系列 - 基础类型结构细节小结(一)
Kotlin系列 - 函数与类相关细节小结(二)

1.高阶函数

基本概念: 传入或者返回函数的函数
函数引用:引用的函数名前加上 ::

  • 有以下几种类型:
  • 类成员方法引用:类名::成员方法名
  • 扩展函数引用:类名::扩展函数名
  • 实例函数引用:实例名::成员方法名
  • 包级别函数引用:::函数名

第一个例子:

打印数组中的元素(传入包级别函数)

fun main(args:Array<String>) {
        args.forEach(::println) //函数引用
}

public actual inline fun println(message: Any?) {
    System.out.println(message)
}

public inline fun <T> Array<out T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

forEach(action: (T) -> Unit):要求传入了一个函数,参数为(action: (T) -> Unit),类型为一个参数T,返回值为Unit
println(message: Any?):类型为一个参数T,返回值为Unit
我们调用args.forEach(::println)println函数传入给forEach

第二个例子:

过滤数组中的空字符串(传入类成员函数)

fun main(args:Array<String>) {
    args.filter(String::isNotEmpty)
}

public inline fun <T> Array<out T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

public inline fun CharSequence.isNotEmpty(): Boolean = length > 0

这里有个点要注意下: filter要求传入的函数类型为(predicate: (T) -> Boolean),但是我们传入的String::isNotEmpty这个方法并没有参数!!!public inline fun CharSequence.isNotEmpty(): Boolean = length > 0 只有一个返回值Boolean为什么可以呢???

答案:因为类名::成员方法名默认就有一个参数,这个函数类型就是类名这个类型的。比如上面的String::isNotEmpty相当于isNotEmpty(String)

第三个例子:

打印数组中的元素(传入实例函数)

fun main(args:Array<String>) {
    val t = Test()
    args.forEach(t::testName)
}
class Test{
    fun testName(name:String){
        println(name)
    }
}
public inline fun <T> Array<out T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

这里传入的是t::testName,实例名::成员方法名就不会默认多出一个参数。如果使用Test::testName会显示报错信息,也验证了我们上面说的类名::成员方法名默认就有一个参数。

image.png

这里总结一下:函数引用,就是将函数作为参数变量传入具体某个方法中,也可以赋值给变量。注意的是,如果是类成员函数、扩展函数引用(类名:函数名),默认参数会多一个就是类本身这个参数

2. 闭包

  • 函数运行的环境
  • 持有函数运行状态
  • 函数内部可以定义函数/类
fun add(x: Int): (Int) -> Int {
    return fun(y: Int): Int {
        return x + y
    }
}
fun main() {
    var add2 = add(2)
    println(add2(10))
}

函数的定义方法可以传入函数,也可以返回函数,函数内的作用域包含了函数内的子函数跟子类等。
格式 : fun 方法名(形参:函数类型) 函数类型{}
函数类型基本写法:
() -> Unit
(多个参数) -> 返回类型

3. 函数复合

  • f(g(x)) 函数传入函数
//定义两个函数
val add5 = { i: Int -> i + 5 }

val multiplyBy2 = { i: Int -> i * 2 }

fun main() {
    println(multiplyBy2(add5(9)))
}
-----打印出来的Log
28

上面是基本的展示,函数中传入函数。
下面扩展一下函数:

//定义三个函数
val add5 = { i: Int -> i + 5 }
val multiplyBy2 = { i: Int -> i * 2 }
val sum = { q: Int, w: Int -> q + w }
//关键点1:
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))
    }
}

// 关键点2:
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))
    }
}
//关键点3:
fun <P1, P2, P3, R> Function2<P2, P3, R>.toAllSum(
    function: Function1<P1, P2>,
    function1: Function1<P2, P3>
): Function2<P1, P2, R> {
    return fun(p1: P1, p2: P2): R {
        return this.invoke(function.invoke(p1), function1.invoke(p2))
    }
}

fun main() {
    // 关键点3:
    val add5AndMulti2 = add5 andThen multiplyBy2
    // 关键点4:
    val add5ComposeMulti2 = add5 compose multiplyBy2
    //关键点5:
    val sum = sum.toAllSum(add5, multiplyBy2)

    println(add5AndMulti2(10))
    println(add5ComposeMulti2(10))
    println(sum(10,10))
}

-----打印出来的Log
30
25
35

上面实际上就是扩展函数,然后在函数中传入函数跟返回函数,只有一个参数的则使用了infix中缀关键字。

关键点1、2、3都是扩展了函数类型,其中关键点1跟2 扩展函数类型为传入一个函数参数,关键点3扩展函数传入两个函数参数

举例:关键点1:函数类型为Function<P1,P2>扩展函数andThen,传入函数类型Function1<P2, R>,返回函数类型Function1<P1, R>
第一个return:返回函数类型为fun(p1: P1): R
第二个return:function.invoke(this.invoke(p1)),先是传入this.invoke(p1)再将这里返回的值传入
function.invoke().
先调用了了andThen前的函数,再调用andThen后面的函数。
大家可以根据这些写法自定义多种扩展函数~~

4. run、let、with、apply、also等语法糖部分解析

  • 先以run为例子
//方法一
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
//方法二
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

方法一函数签名:run(block: () -> R): R直接传入代码块,并返回R
方法二函数签名:T.run(block: T.() -> R): R,传入的代码块block: T.() -> R,也就是调用者本身的引用,则在block中则直接可以使用T中的成员变量及函数等

//使用
var sum = run { 5+3 }
println(sum)

var aList = arrayListOf("小明", "小红", "小黑")
var aListSize = aList.run { size }
println(aListSize)
--------------------打印出来的
8
3

这个 contract {...}看不懂可以暂时不用管它,kotlin中契约的一种写法,详情可以看一下https://kotlinlang.org/docs/reference/whatsnew13.html#contracts

  • withapplyalsolet
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
  • with:接受两个参数,一个是自己本身,一个block: T.() -> R,返回return receiver.block()
    with用法:
var aList = arrayListOf("小明", "小红", "小黑")
var l = with(aList){
  add("小黄")
  removeAt(0)
  forEach {
    print("$it、")
  }
  size
}
println(l)
---------------------打印
小红、小黑、小黄、3
  • apply:方法的扩展函数,传入block: T.() -> Unit,返回调用者本身,用法与run一致,但是最后返回的是调用者本身。
  • also:方法的扩展函数,传入block: (T) -> Unit,这里更前面几个方法有点不一样,block传入了T这个调用者本身,并且函数最后返回调用者本身。
    also用法:
var aList = arrayListOf("小明", "小红", "小黑")
val  sizeFinally = aList.also {
  println(it.size)
  it.add("小黄")
  it.add("小绿")
}.size
println(sizeFinally)
---------打印
3
5
  • let:方法的扩张函数,传入block: (T) -> Rlet方法返回R
    let用法:
val  sizeFinally = aList.let {
        println(it.size)
        it.add("小黄")
        it.add("小绿")
        it.size
}
  println(sizeFinally)
---------------打印
3
5

补充: 尾递归优化 tailrec

  • tailrec关键字添加到fun前提示编译器尾递归优化。
    尾递归:是递归的一种形式,递归中在调用完自己后没有其他操作的称为尾递归。
  • 尾递归与迭代的关系:尾递归可以直接转换成迭代(好吧,其实这个我也不是很清楚~)
//符合尾递归 可以加tailrec 关键字优化
tailrec fun findListNode(head: ListNode?, value: Int): ListNode? {
    head ?: return null
    if (head.value == value) return head
    return findListNode(head.next, value)
}
// 不符合尾递归 因为最后调用完自己还跟n相乘 
fun factorial(n:Long):Long{
  return n * factorial(n-1)
}

上面的方法中第一种是符合尾递归的形式,这种我们可以加tailrec关键字,有什么好处呢?

fun main() {
    var listNode = ListNode(0)
    var p =listNode
    for (i in 1..100000) {
        p.next = ListNode(i)
        p = p.next!!
    }
   println(findListNode(listNode,99998)?.value)
}
-----------有加了关键字tailrec -打印出来的Log
99998
----------没有加关键字打印出来的Log
Exception in thread "main" java.lang.StackOverflowError

因为加了tailrec关键字,实际上是优化成了迭代相比递归减低了内存空间的开销。

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

推荐阅读更多精彩内容