Kotlin中的高阶函数深入讲解

时间:2021-05-20

前言

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

函数类型

在介绍常见高阶函数的使用之前,有必要先了解函数类型,这对我们理解高阶函数很有帮助。Kotlin 使用类似 (Int) -> String 的一系列函数类型来处理函数的声明,这些类型具有与函数签名相对应的特殊表示法,即它们的参数和返回值:

  • 所有函数类型都有一个圆括号括起来的参数类型列表以及一个返回类型:(A, B) -> C 表示接受类型分别为 A 与 B 两个参数并返回一个 C类型值的函数类型。参数类型列表可以为空,如 () -> A ,返回值为空,如(A, B) -> Unit;
  • 函数类型可以有一个额外的接收者类型,它在表示法中的点之前指定,如类型 A.(B) -> C 表示可以在 A 的接收者对象上,调用一个以 B 类型作为参数,并返回一个 C 类型值的函数。
  • 还有一种比较特殊的函数类型,挂起函数,它的表示法中有一个 suspend 修饰符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C 。

常用高阶函数

Kotlin提供了很多高阶函数,这里根据这些高阶函数所在文件的位置,分别进行介绍,先来看一下常用的高阶函数,这些高阶函数在Standard.kt文件中。

1.TODO

先来看一下TODO的源码:

/** * Always throws [NotImplementedError] stating that operation is not implemented. */@kotlin.internal.InlineOnlypublic inline fun TODO(): Nothing = throw NotImplementedError()/** * Always throws [NotImplementedError] stating that operation is not implemented. * * @param reason a string explaining why the implementation is missing. */@kotlin.internal.InlineOnlypublic inline fun TODO(reason: String): Nothing = throw NotImplementedError("An operation is not implemented: $reason")

TODO函数有两个重载函数,都会抛出一个NotImplementedError的异常。在Java中,有时会为了保持业务逻辑的连贯性,对未实现的逻辑添加TODO标识,这些标识不进行处理,也不会导致程序的异常,但是在Kotlin中使用TODO时,就需要针对这些标识进行处理,否则当代码逻辑运行到这些标识处时,就会出现程序的崩溃。

2.run

先给出run函数的源码:

/** * Calls the specified function [block] and returns its result. */@kotlin.internal.InlineOnlypublic 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.InlineOnlypublic inline fun <T, R> T.run(block: T.() -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block()}

这两个run函数都接收一个lambda表达式,执行传入的lambda表达式,并且返回lambda表达式的执行结果。区别是T.run()是作为泛型T的一个扩展函数,所以在传入的lambda表达式中可以使用this关键字来访问这个泛型T中的成员变量和成员方法。

比如,对一个EditText控件,进行一些设置时:

//email 是一个EditText控件email.run { this.setText("请输入邮箱地址") setTextColor(context.getColor(R.color.abc_btn_colored_text_material))}

3.with

先看一下with函数的源码:

/** * Calls the specified function [block] with the given [receiver] as its receiver and returns its result. */@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类型的receiver,和一个lambda表达式,这个表达式会作为receiver的扩展函数来执行,并且返回lambda表达式的执行结果。

with函数与T.run函数只是写法上的不同,比如上面的示例可以用with函数:

with(email, { setText("请输入邮箱地址") setTextColor(context.getColor(R.color.abc_btn_colored_text_material)) }) //可以进一步简化为 with(email) { setText("请输入邮箱地址") setTextColor(context.getColor(R.color.abc_btn_colored_text_material)) }

4.apply

看一下apply函数的源码:

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

apply函数作为泛型T的扩展函数,接收一个lambda表达式,表达式的receiver是泛型T,没有返回值,apply函数返回泛型T对象本身。可以看到T.run()函数也是接收lambda表达式,但是返回值是lambda表达式的执行结果,这是与apply函数最大的区别。

还是上面的示例,可以用apply函数:

email.apply { setText("请输入邮箱地址") }.apply { setTextColor(context.getColor(R.color.abc_btn_colored_text_material)) }.apply { setOnClickListener { TODO() } }

5.also

看一下also函数的源码:

/** * 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函数类似,也是作为泛型T的扩展函数,接收一个lambda表达式,lambda表达式没有返回值。also函数也返回泛型T对象本身,不同的是also函数接收的lambda表达式需要接收一个参数T,所以在lambda表达式内部,可以使用it,而apply中只能使用this。

关于this和it的区别,总结一下:

  • 如果泛型T,作为lambda表达式的参数,形如:(T) -> Unit,此时在lambda表示内部使用it;
  • 如果泛型T,作为lambda表达式的接收者,形如:T.() -> Unit,此时在lambda表达式内部使用this;
  • 不论this,还是it,都代表T对象,区别是it可以使用其它的名称代替。

还是上面的示例,如果用also函数:

email.also { it.setText("请输入邮箱地址") }.also { //可以使用其它名称 editView -> editView.setTextColor(applicationContext.getColor(R.color.abc_btn_colored_text_material)) }.also { it.setOnClickListener { //TODO } }

6.let

看一下let函数的源码:

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

let函数作为泛型T的扩展函数,接收一个lambda表达式,lambda表达式需要接收一个参数T,存在返回值。lambda表达式的返回值就是let函数的返回值。由于lambda表达式接受参数T,所以也可以在其内部使用it。

let应用最多的场景是用来判空,如果上面示例中的EditText是自定义的可空View,那么使用let就非常方便:

var email: EditText? = null TODO() email?.let { email.setText("请输入邮箱地址") email.setTextColor(getColor(R.color.abc_btn_colored_text_material)) }

7.takeIf

看一下takeIf函数的源码:

/** * 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}

takeIf函数作为泛型T的扩展函数,接受一个lambda表达式,lambda表达式接收一个参数T,返回Boolean类型,takeIf函数根据接收的lambda表达式的返回值,决定函数的返回值,如果lambda表达式返回true,函数返回T对象本身,如果lambda表达式返回false,函数返回null。

还是上面的示例,假设用户没有输入邮箱地址,进行信息提示:

email.takeIf { email.text.isEmpty() }?.setText("邮箱地址不能为空")

8.takeUnless

给出takeUnless函数的源码:

/** * 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}

takeUnless函数与takeIf函数类似,唯一的区别是逻辑相反,takeUnless函数根据lambda表达式的返回值决定函数的返回值,如果lambda表达式返回true,函数返回null,如果lambda表达式返回false,函数返回T对象本身。

还是上面的示例,如果用takeUnless实现,就需要调整一下逻辑:

email.takeUnless { email.text.isNotEmpty() //与takeIf的区别 }?.setText("邮箱地址不能为空")

9.repeat

给出repeat函数的源码:

/** * Executes the given function [action] specified number of [times]. * * A zero-based index of current iteration is passed as a parameter to [action]. */@kotlin.internal.InlineOnlypublic inline fun repeat(times: Int, action: (Int) -> Unit) { contract { callsInPlace(action) } for (index in 0 until times) { action(index) }}

repeat函数接收两个参数,一个Int型参数times表示重复次数,一个lambda表达式,lambda表达式接收一个Int型参数,无返回值。repeat函数就是将我们传入的lambda表达式执行times次。

repeat(3) { println("执行第${it + 1}次") } //运行结果执行第1次执行第2次执行第3次

由于repeat函数接收的lambda表达式,需要一个Int型参数,因此在表达式内部使用it,其实it就是for循环的索引,从0开始。

总结

最后对这些高阶函数做一下总结,TODO对比Java中的TODO,需要实现业务逻辑,不能放任不理,否则会出现异常,导致崩溃。takeIf、takeUnless这一对都是根据接收lambda表达式的返回值,决定函数的最终返回值是对象本身,还是null,区别是takeIf,如果lambda表达式返回true,返回对象本身,否则返回null;takeUnless与takeIf的逻辑正好相反,如果lambda表达式返回true,返回null,否则返回对象本身。repeat函数,见名知意,将接收的lambda表达式重复执行指定次。

run、with、apply、also、let这几个函数区别不是很明显,有时候使用其中一个函数实现的逻辑,完全也可以用另外一个函数实现,具体使用哪一个,根据个人习惯。需要注意的是:

  • 对作为扩展函数的高阶函数,使用前需要判断接收的对象是否为空,比如T.run,apply,also,let在使用前需要进行空检查;
  • 对于返回对象本身的函数,比如apply,also可以形成链式调用;
  • 对于在函数内部能够使用it的函数,it可以用意思更加清晰的变量代替,比如T.run,also,let。

对这几个函数的区别做一个对比:

函数名称 是否作为扩展函数 是否返回对象本身 在函数内部使用this/ it run no no - T.run yes no it with no no this apply yes yes this also yes yes it let yes no it

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。

声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。

相关文章