0%

Kotlin高阶函数

什么是高阶函数?

按照定义,高阶函数就是以另外一个函数作为参数或者返回值的函数。在Kotlin中,函数可以用lambda或者函数引用来表示。因此,任何以lambda或者函数引用作为参数的函数,或者返回值为lambda或者函数引用的函数,或者两者都满足的函数都是高阶函数。

将函数用作函数参数的高阶函数

这里介绍的kotlin中let{}高阶函数。先看一看源码

1
2
3
4
5
6
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}

其中参数block就是一个函数类型,下面会对let函数有专门的介绍

将函数用作一个函数的返回值的高阶函数

这里使用官网上的一个例子来讲解。lock()函数,先看一看他的源码实现

1
2
3
4
5
6
7
8
9
10
fun <T> lock(lock: Lock, body: () -> T): T {
    lock.lock()    
try {        
return body()
   }    
finally {
        lock.unlock()
    }
}

  1. 从源码可以看出,该函数接受一个Lock类型的变量作为参数1,并且接受一个无参且返回类型为T的函数作为参数2.
  2. 该函数的返回值为一个函数,我们可以看这一句代码return body()可以看出。

自定义高阶函数

我们先看了一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
//声明
fun debug(code: () -> Unit){
if (BuildConfig.DEBUG) {
code()
}
}
//调用
fun onCreate(savedInstanceState: Bundle?) {
debug {
showDebugTools();
}
}

上面定义了一个debug函数,这个函数的参数code也是一个函数,当只有是debug模式的时候,才会执行code函数里面的代码

tip:当函数中只有一个函数作为参数,并且使用了lambda表达式作为对应的参数,那么可以省略函数的小括号()。
函数的最后一个参数是函数类型时,可以使用lambda表达式将函数参数写在参数列表括号外面。

标准高阶函数

标准高阶函数的声明

标准高阶函数声明在Standard.kt文件中,其中有TODOrunwithapplyalsolettakeIftakeUnlessrepeat函数。我们将功能类似的函数放在一块对比,如run & withapply & alsotakeIf & takeUnlesslet & 扩展函数版本run

run&with函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Calls the specified function [block] and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

/**
* Calls the specified function [block] with `this` value as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

/**
* Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}

run函数的版本有两个版本,一个是普通版本的定义,一种是扩展函数版本从代码定义可以看到,run函数接受一个函数引用作为参数(高阶函数),在内部仅仅只是调用了一下这个代码块并且返回block代码块的返回值。可以发现withrun都是返回了block(是个函数引用)的返回值。区别在哪:区别在于有个run是扩展函数,如果在使用之前需要判空,那么扩展函数版本的run函数的使用会比with函数优雅,如:

1
2
3
4
5
6
7
8
9
10
// Yack!
with(webview.settings) {
this?.javaScriptEnabled = true
this?.databaseEnabled = true
}
// Nice.
webview.settings?.run {
javaScriptEnabled = true
databaseEnabled = true
}

可以看到扩展函数版本的run函数在调用前可以先判断webview.settings是否为空,否则不进入函数体调用。

apply&also

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Calls the specified function [block] with `this` value as its receiver and returns `this` value.
*/
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

/**
* Calls the specified function [block] with `this` value as its argument and returns `this` value.
*/
@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
}

apply&also最后都会返回接收者自身T,所以可以实现链式调用细化代码粒度,让代码更清晰,它们的区别是一个applyblock是用T(也就是apply的接收者本身)作为接收者,因此在applyblock内部可以访问到T这个thisalsoT是被当做参数传入block的,所以在alsoblock内部需要用it(lambda的唯一参数)代表这个also的接收者T

使用上:一般来说,lambda参数为it的函数比较适合用做读值多的场合,也可以使用命名参数将it改成合适的名字提升代码可读性,将T传入block(T)的场合比较适合用于写值,因为省了很多T变量的重复声明。【推荐】lambda表达式的block中,如果主要进行对某个实例的写操作,则该实例声明为Receiver**;如果主要是读操作,则该实例声明为参数所以在写值操作多时使用apply代码块,在读值操作多时使用also代码块。**

1
2
3
4
5
6
7
8
9
10
11
12
13
inline fun <T> T.apply(block: T.() -> Unit): T//对T进行写操作,优先使用apply

tvName.apply {
text = "Jacky"
textSize = 20f
}

inline fun <T> T.also(block: (T) -> Unit): T //对T进行读操作 优先使用also

user.also {
tvName.text = it.name
tvAge.text = it.age
}

let函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Calls the specified function [block] with `this` value as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

/**
* Calls the specified function [block] with `this` value as its argument and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)

let这个函数和扩展版本的run函数非常像,所以在上面的代码我把它们放在一起对比,他们的区别在runblock参数是个带run接收者T的函数引用,而letblock参数是let的接收者T当做参数传给block,因此他们的调用区别是使用run时,block内部的this是指向T的,而在letblock内部需要使用it来指向Tletblock内部的this指的是T外部的this,意思是类似于你在Activity里面用letletblock里面的this就是这个Activity实例

run函数比较适合写值多的代码块,let函数比较适合读值多的代码块。

let与also的返回值区别

let返回的是block的返回值,also返回的是接收者T自身,因此他们的链式调用有本质区别。
let能实现类似RxJava的map的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
val original = "abc"
// Evolve the value and send to the next chain
original.let {
println("The original String is $it") // "abc"
it.reversed() // evolve it as parameter to send to next let
}.let {
println("The reverse String is $it") // "cba"
it.length // can be evolve to other type
}.let {
println("The length of the String is $it") // 3
}
// Wrong
// Same value is sent in the chain (printed answer is wrong)
original.also {
println("The original String is $it") // "abc"
it.reversed() // even if we evolve it, it is useless
}.also {
println("The reverse String is ${it}") // "abc"
it.length // even if we evolve it, it is useless
}.also {
println("The length of the String is ${it}") // "abc"
}
// Corrected for also (i.e. manipulate as original string
// Same value is sent in the chain
original.also {
println("The original String is $it") // "abc"
}.also {
println("The reverse String is ${it.reversed()}") // "cba"
}.also {
println("The length of the String is ${it.length}") // 3
}

在上面看来T.also好像毫无意义,因为我们可以很容易地将它们组合成一个功能块。但仔细想想,它也有一些优点:

它可以在相同的对象上提供一个非常清晰的分离过程,即制作更小的功能部分。在使用之前,它可以实现非常强大的自我操纵,**实现链条建设者操作(builder 模式)**。

takeIf&takeUnless

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Returns `this` value if it satisfies the given [predicate] or `null`, if it doesn't.
*/
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
return if (predicate(this)) this else null
}

/**
* Returns `this` value if it _does not_ satisfy the given [predicate] or `null`, if it does.
*/
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
return if (!predicate(this)) this else null
}

这两个函数是用来做有条件判断时使用的,takeIf是在predicate条件返回true时返回接收者自身,否者返回nulltakeUnless则刚好相反,是在predicatefalse时返回接收者自身,否则返回null

repeat函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Executes the given function [action] specified number of [times].
*
* A zero-based index of current iteration is passed as a parameter to [action].
*
* @sample samples.misc.ControlFlow.repeat
*/
@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
contract { callsInPlace(action) }

for (index in 0 until times) {
action(index)
}
}

这个函数就是把action代码块重复执行times次,action的参数就是当前执行的index(第几次)。

This vs. it参数

如果你检查T.run函数签名,你会注意到T.run只是作为扩展函数调用block: T.()。因此,所有的范围内,T可以被称为this。在编程中,this大部分时间可以省略。因此,在我们上面的例子中,我们可以在println声明中使用$length,而不是${this.length}。我把这称为传递this参数。然而,对于T.let函数签名,你会注意到T.let把自己作为参数传递进去,即block: (T)。因此,这就像传递一个lambda参数。它可以在作用域范围内使用it作为引用。所以我把这称为传递it参数。从上面看,它似乎T.run是更优越,因为T.let更隐含,但是这是T.let函数有一些微妙的优势如下:

T.let相比外部类函数/成员,使用给定的变量函数/成员提供了更清晰的区分在this不能被省略的情况下,例如当它作为函数的参数被传递时itthis更短,更清晰。在T.let允许使用更好的变量命名,你可以转换it为其他名称。

1
2
3
4
stringVariable?.let {
nonNullString ->
println("The non null string is $nonNullString")
}

这几个函数的选择

Kotlin%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0%203896d474b54440ebb3699b4acf5ee205/WX20210222-173846.png

函数的闭包

什么是闭包

我们都知道,程序的变量分为全局变量局部变量,全局变量,顾名思义,其作用域是当前文件甚至文件外的所有地方;而局部变量,我们只能再其有限的作用域里获取。

那么,如何在外部调用局部变量呢?答案就是——闭包,与此给闭包下个定义:闭包就是能够读取其他函数内部变量的函数

首先看个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//这是一个返回值为一个函数的高阶函数
fun makeFun():()->Unit{
var conut = 0
return fun(){ //返回一个匿名函数,这个函数持有count的状态
println(++conut)
}
}

fun main() {

val makeFun = makeFun() //函数调用,返回一个函数
makeFun() //调用这个返回的函数,此时makeFun持有makeFun()内部变量的状态
makeFun()
makeFun()
}

运行结果:

https://img-blog.csdnimg.cn/20190611150950647.png

在比如一个稍微复杂一点的例子,实现斐波那契数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//斐波那契数列
fun fibonacci():()->Long{
var first = 0L
var second = 1L
return fun():Long{ //返回返回值为Long类型的函数
val result = second
second += first
first = second - first
return result
}
}

fun main() {
val fibo = fibonacci() //此时,这个返回的函数fibo持有fibonnacci()函数内部变量的状态
println(fibo())
println(fibo())
println(fibo())
println(fibo())
println(fibo())
}

测试运行

https://img-blog.csdnimg.cn/20190611151101358.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pzcDc2NTA5ODA4NA==,size_16,color_FFFFFF,t_70

参考资料

高阶函数与 lambda 表达式

Kotlin:高阶函数

Kotlin 高阶函数 - 码上开学