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

2,049 阅读7分钟

Kotlin细节文章笔记整理更新进度:
Kotlin系列 - 基础类型结构细节小结(一)
Kotlin系列 - 函数与类相关细节小结(二)
Kotlin系列 - 高阶函数与标准库中的常用函数(三)
Kotlin系列 - 进阶深入泛型协变逆变从java到Kotlin(四)
Koltin系列 - 协程从认识到安卓中的使用(五)

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中契约的一种写法,详情可以看一下kotlinlang.org/docs/refere…

  • 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方法返回Rlet用法:
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关键字,实际上是优化成了迭代相比递归减低了内存空间的开销。