【疯狂Android之Kotlin】关于Kotlin的高阶函数

2,491 阅读8分钟

Kotlin的高阶函数

高阶函数介绍

  1. 概念 相信许多同学都已经知道,所谓的高阶函数就是就是方法的参数 或 返回值 是函数类型的 函数
  2. 通过例子说明 List 集合的 forEach( )循环 , 该方法就是接收一个高阶函数类型变量作为参数 , 有点类似于C/C++中的函数指针(指向函数的指针)

函数作为函数参数

  1. 这里先介绍下sumBy()这个高阶函数,通过这个我们来看下如何将函数用作参数,源码如下:
// sumBy函数的源码
public inline fun CharSequence.sumBy(selector: (Char) -> Int): Int {
    var sum: Int = 0
    for (element in this) {
        sum += selector(element)
    }
    return sum
}
  1. 说明
  • 大家这里可以不必纠结inline,和sumBy函数前面的CharSequence.。因为这是Koltin中的内联函数与扩展功能。
  • 该函数返回一个Int类型的值。并且接受了一个selector()函数作为该函数的参数。其中,selector()函数接受一个Char类型的参数,并且返回一个Int类型的值。
  • 定义一个sum变量,并且循环这个字符串,循环一次调用一次selector()函数并加上sum。用作累加。其中this关键字代表字符串本身。
  1. 该函数的作用:把字符串中的每一个字符转换为Int的值,用于累加,最后返回累加的值
  • 举个例子
val testStr = "abc"
val sum = testStr.sumBy { it.toInt() }
println(sum)
  • 输出结果
294  // 因为字符a对应的值为97,b对应98,c对应99,故而该值即为 97 + 98 + 99 = 294

函数作为函数返回值

  1. 同样这里也是使用一个lock()函数来进行讲解,先看看源码:
fun <T> lock(lock: Lock, body: () -> T): T {
    lock.lock()
    try {
        return body()
    }
    finally {
        lock.unlock()
    }
}
  1. 说明
  • 这其中用到了kotlin中泛型的知识点,这里暂且不考虑。同学我会在后续的文章进行介绍。
  • 从源码可以看出,该函数接受一个Lock类型的变量作为参数1,并且接受一个无参且返回类型为T的函数作为参数2.
  • 该函数的返回值为一个函数,我们可以看这一句代码return body()可以看出。
  1. 使用
  • 下面的代码都是伪代码,我就是按照官网的例子直接拿过来用的
fun toBeSynchronized() = sharedResource.operation()
val result = lock(lock, ::toBeSynchronized)   

其中,::toBeSynchronized即为对函数toBeSynchronized()的引用,其中关于双冒号::的使用在这里不做讨论与讲解。

  • 上面的写法也可以写作:
val result = lock(lock, {sharedResource.operation()} )

函数作为函数类型变量

这里同学我使用匿名函数来简单讲解一下

  1. 函数变量需求 在上面的forEach()函数中, 需要传入一个 (String) -> Unit 函数类型的变量, 该函数类型的函数 参数是 String 类型 , 返回值是Unit空类型 ;

  2. 普通的函数声明 : 下面定义的函数 , 参数类型是 String , 返回值是 Unit 空类型 , 这个函数是 (String) -> Unit 类型的 , 但是 study 不能当做参数传入到 forEach 方法中;list.forEach(study),是错误的调用,编译不通过 ;

fun study(student : String) : Unit{
    println(student + " 在学习")
}
  1. 函数类型变量 : 可以使用匿名函数 , 赋值给一个变量 , 然后将这个变量当做参数传递给 forEach 当做参数 ;
  • 指定变量 : 为 (String) -> Unit 类型函数指定一个引用变量 var study2 ;
  • 匿名函数 : 顾名思义,就是没有函数名称 , 省略调上面普通函数的名称,赋值给变量 ; 具体用法如下 :
var study2 = fun (student : String) : Unit{
    println(student + " 在学习")
}

高阶函数的使用与示例

  • 在上面的这些例子中,我们出现了str.sumBy{ it.toInt },这样的写法这里主要讲高阶函数中对Lambda语法的简写。

  • 从上面的例子我们的写法应该是这样的:

str.sumBy( { it.toInt } )
  1. 但是根据Kotlin中的约定,即当函数中只有一个函数作为参数,并且您使用了lambda表达式作为相应的参数,则可以省略函数的小括号()。 故而我们可以写成:
str.sumBy{ it.toInt }
  1. 还有一个约定,即当函数的最后一个参数是一个函数,并且你传递一个lambda表达式作为相应的参数,则可以在圆括号之外指定它。故而上面例2中的代码,所以我们可写成:
val result = lock(lock){
     sharedResource.operation()
}

Kotlin常用标准高阶函数介绍

介绍几个Kotlin中常用的标准高阶函数。如果用好下面的几个函数,能减少很多的代码量,并增加代码的可读性。下面的几个高阶函数的源码几乎上都出自Standard.kt文件

TODO函数

其实严格来说,该函数不是一个高阶函数,只是一个抛出异常以及测试错误的一个普通函数

  1. 看下它的源码如下:
@kotlin.internal.InlineOnly
public inline fun TODO(): Nothing = throw NotImplementedError()

@kotlin.internal.InlineOnly
public inline fun TODO(reason: String): Nothing = 
throw NotImplementedError("An operation is not implemented: $reason")
  • 作用:显示抛出NotImplementedError错误
  • NotImplementedError错误类继承至Java中的Error
  1. 举个例子:
fun main(args: Array<String>) {
    TODO("测试TODO函数,是否显示抛出错误")
}
  • 如果调用TODO()时,不传参数的,则会输出An operation is not implemented.

with()函数

对于with函数,其实简单来说就是可以让用户省略点号之前的对象引用,针对with对象,在Standard.kt中语法如下

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

看出with是一个全局函数,并没有作为任何类的扩展方法,仔细查看block会发现它又是一个带接收者的字面函数,这是一种临时的扩展方法,只在调用过程中有效,调用结束之后就不再生效,所以block就成了receiver临时的扩展函数,临时扩展函数的内部调换用上下文就是receiver对象。

举个栗子

class MyLogger {
    var tag: String = "TAG"

    fun e(msg: String) {
        println("$tag  $i")
    }

    fun tag(tagStr: String) {
        tag = tagStr
    }
}

fun main(args: Array<String>) {
    val logger = MyLogger()
    with(logger) {
        tag("Kotlin")
        e("It is a good language")
    }
}

apply()函数

关于apply()函数用于lambda表达式里切换上下文,可以查看

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
  • 可以看到block这个函数,它不是一个普通函数,block其实就是带接收者的字面函数,这样传入的lambda表达式就临时扩展了T类,调用lambda表达式时的上下文就是调用方法的T类对象。
/**
 * @author : Jacky
 * @date: : 2021/2/24
 * 数据库链接
 **/
class DbConfig {
    var url: String = ""
    var username: String = ""
    var password: String = ""

    override fun toString(): String {
        return "url = $url, username = $username, password = $password"
    }
}

class DbConnection {
    fun config(conf: DbConfig) {
        println(conf)
    }
}

fun main(args: Array<String>) {
    val conn = DbConnection()
    
    //上下表达式
    conn.config(DbConfig().apply {
        url = "mysql://127.0.0.1:3306/hello"
        username = "root"
        password = "123456"
    })
}

这里使用apply函数,不但初始化了所有属性的值还可以把对象返回来用来配置数据库连接对象

also()函数

  1. 关于T.also函数来说,它和T.apply很相似。我们先看看其源码的实现:
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}
  • 上面的源码在结合T.apply函数的源码我们可以看出: T.also函数中的参数block函数传入了自身对象
  • 这个函数的作用是用用block函数调用自身对象,最后在返回自身对象
  1. 下面举个例子,用实例来说明一下和apply的区别
"kotlin".also {
    println("结果:${it.plus("-java")}")
}.also {
    println("结果:${it.plus("-php")}")
}

"kotlin".apply {
    println("结果:${this.plus("-java")}")
}.apply {
    println("结果:${this.plus("-php")}")
}
  • 输出结果如下
结果:kotlin-java
结果:kotlin-php

结果:kotlin-java
结果:kotlin-php
  • 可以看出,他们的区别在于:
  • T.also中只能使用it调用自身,而T.apply中只能使用this调用自身。
  • 因为在源码中T.also是执行block(this)后在返回自身。而T.apply是执行block()后在返回自身。
  • 这就是为什么在一些函数中可以使用it,而一些函数中只能使用this的关键所在

let()函数

  1. 首先查看Standard.kt源文件let函数的源代码
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

接收了调用者作为参数并且返回任意的类型的lambda表达式,最后以自己为参数调用lambda表达式

val arr = intArrayOf(1, 2, 4, 6)
arr.let {
    var sum = 0
    // 这个it就是arr对象
    for (i in it) {
        sum += i
    }
    println(sum)
}

对于系统标准高阶函数的总结

  • 一般我们使用最多就是also,let,apply这三个函数,一般在实际项目开发过程中都不会进行连贯着使用
  • 关于它们之间的区别,或者在什么情况下需要使用那个高阶函数,同学可以参考以下两篇文章

自定义高阶函数

  1. 多说不易,我们可以看下一个例子:
// 源代码
fun test(a : Int , b : Int) : Int{
   return a + b
}

fun sum(num1 : Int , num2 : Int) : Int{
   return num1 + num2
}

// 调用
test(10,sum(3,5)) // 结果为:18

// lambda
fun test(a : Int , b : (num1 : Int , num2 : Int) -> Int) : Int{
   return a + b.invoke(3,5)
}

// 调用
test(10,{ num1: Int, num2: Int ->  num1 + num2 })  // 结果为:18

以上我们可以看到直接写死了值,这在开发中是非常不合理的,上面的例子在阐述Lambda的语法,另举一个例子

  1. 传入两个参数,并传入一个函数实现不同的逻辑
private fun resultByOpt(num1 : Int , num2 : Int , result : (Int ,Int) -> Int) : Int{
    return result(num1,num2)
}

private fun testDemo() {
    val result1 = resultByOpt(1,2){
        num1, num2 ->  num1 + num2
    }

    val result2 = resultByOpt(3,4){
        num1, num2 ->  num1 - num2
    }

    val result3 = resultByOpt(5,6){
        num1, num2 ->  num1 * num2
    }

    val result4 = resultByOpt(6,3){
        num1, num2 ->  num1 / num2
    }

    println("result1 = $result1")
    println("result2 = $result2")
    println("result3 = $result3")
    println("result4 = $result4")
}
  • 输出结果为
result1 = 3
result2 = -1
result3 = 30
result4 = 2  
  • 根据传入不同的Lambda表达式,实现了两个数的(+、- 、 * 、/)。
  • 当然了,在实际的项目开发中,自己去定义高阶函数的实现是很少了,因为用系统给我们提供的高阶函数已经够用了。
  • 不过,当我们掌握了Lambda语法以及怎么去定义高阶函数的用法后。在实际开发中有了这种需求的时候也难不倒我们了。fighting

总结

  • 既然我们选择了Kotlin这门编程语言。那其高阶函数时必须要掌握的一个知识点,因为,在系统的源码中,实现了大量的高阶函数操作。
  • 除了上面讲解到的标准高阶函数外,对于字符串(String)以及集合等,都用高阶函数去编写了他们的一些常用操作。比如,元素的过滤、排序、获取元素、分组等等
  • 对于上面讲述到的标准高阶函数,同学我要多实践,因为它们真的能在实际的项目开发中减少大量的代码编写量。