查看原文
其他

Kotlin 高阶函数与 Standard.kt 源码详解

AndroidPub 2023-02-21

The following article is from Android开发编程 Author Android开发编程

前言

在Kotlin中,高阶函数是指将一个函数作为另一个函数的参数或者返回值。如果用f(x)、g(x)用来表示两个函数,那么高阶函数可以表示为f(g(x))。Kotlin为开发者提供了丰富的高阶函数,比如Standard.kt中的let、with、apply等,_Collectioins.kt中的forEach等。为了能够自如的使用这些高阶函数,我们有必要去了解这些高阶函数的使用方法

今天我们来讲解高阶函数

一、高阶函数详解

1、高阶函数是什么?

  • 如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。

  • 与java不同的是,在Kotlin中增加了一个函数类型的概念,如果我们将这种函数添加到一个函数的参数声明或返回值声明当中,那么这就是一个高阶函数了。

  • 函数类型语法基本规则:(String,Int) -> Unit添加到某个函数的参数声明

public fun test2(test:Int,block:()->Unit){ var v= block() DTLog.i("TestTest","Test1") } public fun <T>T.test22(block:()->T):T{ return block() } public fun <T>T.test26(block:T.()->Unit){ block() } public fun <T>T.test23(block:(T)->Unit):T{ return this } public fun <T,R> T.test3(block: (T) -> R):R{ var t=block(this) return t } public fun <T, W> T.test4(block: (T) -> W): W { return block(this) }

以上就是一个高阶函数,它接收了一个函数类型的参数,而调用高阶函数的方法与调用普通函数差异不大,只需要在参数名后面加上括号,并在括号中传入必要的参数即可;

高阶函数类型具有与函数签名相对应的特殊表示法,即它们的参数和返回值:

  • 所有函数类型都有一个圆括号括起来的参数类型列表以及一个返回类型:(A, B) -> C 表示接受类型分别为 A 与 B 两个参数并返回一个 C类型值的函数类型。参数类型列表可以为空,如 () -> A ,返回值为空,如(A, B) -> Unit;

  • 函数类型可以有一个额外的接收者类型,它在表示法中的点之前指定,如类型 A.(B) -> C 表示可以在 A 的接收者对象上,调用一个以 B 类型作为参数,并返回一个 C 类型值的函数。

  • 还有一种比较特殊的函数类型,挂起函数,它的表示法中有一个 suspend 修饰符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C 。

2、内联函数详解

 ①内联函数是什么

 inline(小心,不是online),翻译成“内联”或“内嵌”。意指:当编译器发现某段代码在调用一个内联函数时,它不是去调用该函数,而是将该函数的代码,整段插入到当前位置。这样做的好处是省去了调用的过程,加快程序运行速度。(函数的调用过程,由于有前面所说的参数入栈等操作,所以总要多占用一些时间)。这样做的不好处:由于每当代码调用到内联函数,就需要在调用处直接插入一段该函数的代码,所以程序的体积将增大。拿生活现象比喻,就像电视坏了,通过电话找修理工来,你会嫌慢,于是干脆在家里养了一个修理工。这样当然是快了,不过,修理工住在你家可就要占地儿了。内联函数并不是必须的,它只是为了提高速度而进行的一种修饰。要修饰一个函数为内联型

使用如下格式: 

inline 函数的声明或定义 

简单一句话,在函数声明或定义前加一个 inline 修饰符。 

inline int max(int a, int b) { return (a>b)? a : b; }


内联函数的本质是,节省时间但是消耗空间。

②内联函数规则

inline函数的规则

(1)、一个函数可以自已调用自已,称为递归调用(后面讲到),含有递归调用的函数不能设置为inline;

(2)、使用了复杂流程控制语句:循环语句和switch语句,无法设置为inline;

(3)、由于inline增加体积的特性,所以建议inline函数内的代码应很短小。最好不超过5行。

(4)、inline仅做为一种“请求”,特定的情况下,编译器将不理会inline关键字,而强制让函数成为普通函数。出现这种情况,编译器会给出警告消息。

(5)、在你调用一个内联函数之前,这个函数一定要在之前有声明或已定义为inline,如果在前面声明为普通函数,而在调用代码后面才定义为一个inline函数,程序可以通过编译,但该函数没有实现inline。比如下面代码片段: 

//函数一开始没有被声明为inline: 

void foo(); 

//然后就有代码调用它: 

foo(); 

//在调用后才有定义函数为inline: 

inline void foo() { ...... }

代码是的foo()函数最终没有实现inline;

(6)、为了调试方便,在程序处于调试阶段时,所有内联函数都不被实现

③内联函数时应注意以下几个问题

(1) 在一个文件中定义的内联函数不能在另一个文件中使用。它们通常放在头文件中共享。 

(2) 内联函数应该简洁,只有几个语句,如果语句较多,不适合于定义为内联函数。 

(3) 内联函数体中,不能有循环语句、if语句或switch语句,否则,函数定义时即使有inline关键字,编译器也会把该函数作为非内联函数处理。 

(4) 内联函数要在函数被调用之前声明。关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。

3、高阶函数中使用内联函数

直使用的 Lambda 表达式在底层被转换成了匿名类的实现方式。这就表明,我们每调用一次 Lambda 表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。为了解决这个问题,Kotlin 提供了内联函数的功能,它可以将使用 Lambda 表达式带来的运行时开销完全消除,只需要在定义高阶函数时加上 inline 关键字的声明即可

inline fun test111(num1: Int, num2: Int, block: (Int, Int) -> Int): Int { val result = block(num1, num2) return result }

4、闭包函数

闭包函数 一个函数的返回值是函数,函数的内部包含另一个函数,可以是有参无参的匿名函数

fun main(args: Array<String>) { val mm = aaa() println(mm()) println(mm()) println(mm()) println(mm()) println(mm()) val kk = bbb() println(kk("shadow")) //shadow --- 1 println(kk("shadow")) //shadow --- 2 println(kk("shadow")) //shadow --- 3 println(kk("shadow")) //shadow --- 4 println(kk("shadow")) //shadow --- 5}//闭包函数 就是函数作为返回参数fun aaa(): () -> (Int) { var current = 10 return fun(): Int { return current++ }}fun bbb(): (String) -> (String) { var current = 0; return fun(str: String): String { current++; return "$str --- $current"; }}


二、kotin中标准库Standard.kt源码讲解

在 Kotlin 源码的Standard.kt标准库中提供了一些便捷的内置高阶函数( let、also、with、run、apply ),可以帮助我们写出更简洁优雅的 Kotlin 代码,提高开发效率,学习源码可以更快的帮助我们理解和应用

1、apply

@kotlin.internal.InlineOnlypublic inline fun <T> T.apply(block: T.() -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block() return this}
  • 传递this作为block函数参数(调用时可以省略),且apply函数的返回值是调用者本身;

  • 执行一个 T 类型中的方法,变量等,然后返回自身 T;

  • 注意参数 block: T.(),但凡看到 block: T.() -> 这种代码块,意味着在大括号 {} 中可以直接调用T内部的 API 而不需要在加上 T. 这种【实际上调用为 this. ,this. 通常省略】

val str = "hello"

str.apply { length }    //可以省略 str.

str.apply { this.length } //可以这样

2、let

@kotlin.internal.InlineOnlypublic inline fun <T, R> T.let(block: (T) -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block(this)}
  • let 方法是传递类型 T 返回另外一个类型 R 形式;

  • 传递it作为block函数参数,且let函数的返回值是由block函数决定;

3、also

@kotlin.internal.InlineOnly@SinceKotlin("1.1")public inline fun <T> T.also(block: (T) -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block(this) return this}

执行一个 T 类型中的方法,变量等,然后返回自身 T;

传递it作为block函数参数(调用时不可以省略),且also函数的返回值是调用者本身;

这个方法与上面的 apply 方法类似,只是在大括号中执行 T 自身方法的时候,必须要加上 T. 否则无法调用 T 中的 API,什么意思呢?看下面代码:

val str = "hello"

str.also { str.length }  //str.必须加上,否则编译报错

str.also { it.length }   //或者用 it.

4、with


@kotlin.internal.InlineOnlypublic inline fun <T, R> with(receiver: T, block: T.() -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return receiver.block()}
  • with() 方法接收一个类型为 T 的参数和一个代码块

  • 经过处理返回一个 R 类型的结果

val str = "hello"val ch = with(str) { get(0)}println(ch) //打印 h

5、run

@kotlin.internal.InlineOnlypublic inline fun <R> run(block: () -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block()}
  • 要求传递的是一个代码块,同时返回一个任意类型;

  • 但凡函数接收的是一个代码块时,使用的时候一般都建议使用 {} 来包含代码块中的逻辑,只有在一些特殊情况下可以参数 (::fun) 的形式进行简化

@kotlin.internal.InlineOnlypublic inline fun <T, R> T.run(block: T.() -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block()}
  • 此处是执行一个 T 类型的 run 方法,传递的依然是一个代码块,

  • 只是内部执行的是 T 的内部一个变量 或 方法等,返回的是 一个 R 类型

run { println(888)}val res = run { 2 + 3 }fun runDemo() { println("测试run方法")}//我们可以这么干run(::runDemo)

6、takeIf

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? { contract { callsInPlace(predicate, InvocationKind.EXACTLY_ONCE) } return if (predicate(this)) this else null}
  • 根据传递的参数 T 做内部判断,根据判断结果返回 null 或者 T 自身;

  • 传递的是【一元谓词】代码块,像极了 C++ 中的一元谓词:方法只含有一个参数,并且返回类型是Boolean类型;

  • 源码中,通过传递的一元谓词代码块进行判断,如果是 true 则返回自身,否则返回 null;

val str = "helloWorld"str.takeIf { str.contains("hello") }?.run(::println)


7、takeUnless

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? { contract { callsInPlace(predicate, InvocationKind.EXACTLY_ONCE) } return if (!predicate(this)) this else null}

这个方法跟 takeIf() 方法类似,只是内部判断为false的时候返回自身T ,而 true 的时候返回 null,因此不过多说明,使用参考 takeIf() 方法。

8、repeat()

public inline fun repeat(times: Int, action: (Int) -> Unit) { contract { callsInPlace(action) } for (index in 0 until times) { action(index) }}

分析:repeat 方法包含两个参数:

  • 第一个参数int类型,重复次数,

  • 第二个参数,表示要重复执行的对象

  • 该方法每次执行的时候都将执行的次数传递给要被重复执行的模块,至于重复执行模块是否需要该值,需要根据业务实际需求考虑,例如:

//打印从0 到 100 的值,次数用到了内部的index

repeat(100) { print(it)}//有比如,单纯的打印helloworld 100 次,就没有用到index值repeat(100){ println("helloworld")}

三、高阶函数选择

  • 如果需要返回自身调用者本身(即return this),可以选择 apply also

  • 如果需要传递this作为参数,可以选择 apply run with

  • 如果需要传递it作为参数,可以选择 let also

  • 如果返回值需要函数决定(即return block()),可以选择 run with let


总结

不管是 Kotlin 中内置的高阶函数,还是我们自定义的,其传入的代码块样式,无非以下几种:

1、block: () -> T 和 block: () -> 具体类型

这种在使用 (::fun) 形式简化时,要求传入的方法必须是无参数的,返回值类型如果是T则可为任意类型,否则返回的类型必须要跟这个代码块返回类型一致

2、block: T.() -> R 和 block: T.() -> 具体类型

这种在使用 (::fun) 形式简化时,要求传入的方法必须包含一个T类型的参数,返回值类型如果是R则可为任意类型,否则返回的类型必须要跟这个代码块返回类型一致。例如 with 和 apply 这两个方法

3、block: (T) -> R 和 block: (T) -> 具体类型

这种在使用 (::fun) 形式简化时,要求传入的方法必须包含一个T类型的参数,返回值类型如果是R则可为任意类型,否则返回的类型必须要跟这个代码块返回类型一致。例如 let 和 takeIf 这两个方法


更多阅读:

Kotlin 编程技巧:为函数添加作用域

深入理解 Kotlin 之 Sealed Class & Interface

Kotlin 标准库随处可见的 contract 到底是什么?

Kotlin DSL 实战:像 Compose 一样写代码


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存