Kotlin高阶函数的理解与使用
1. 基础定义
1.1 什么是高阶函数
按照定义,高阶函数就是以另外一个函数作为参数或者返回值的函数。
在Kotlin中,函数可以用lambda或者函数引用来表示。
因此,任何以lambda或者函数引用作为参数的函数,或者返回值为lambda或者函数引用的函数,或者两者都满足的函数都是高阶函数。
1.2 lambda的约定:
要熟悉Kotlin函数,首先得看懂代码中的lambda表达式,这里首先就得清楚一些约定,如:
当函数中只有一个函数作为参数,并且使用了lambda表达式作为对应的参数,那么可以省略函数的小括号()。
函数的最后一个参数是函数类型时,可以使用lambda表达式将函数参数写在参数列表括号外面。
例如:
str.sumBy( { it.toInt } )
可以省略成
str.sumBy{ it.toInt }
Anko的Context扩展alert函数,可以注意到positiveButton方法第一个参数是text,
第二个参数是监听器lambda表达式,写在了参数列表圆括号外面。
alert("确定删除吗?","Alert") {
positiveButton("OK") { Log.i(TAG, "你点了确定按钮")}
negativeButton("Cancel") { Log.i(TAG, "你点了取消按钮") }
}.build().show()
1.3 函数类型变量与对应的Java代码
在Kotlin中,变量的类型可以是函数类型,例如下面的代码中sum变量的类型是Int类型,而predicate变量是函数类型,也就是说这个变量代表一个函数。
声明一个名字为sum的Int类型变量(这个sum变量的类型是Int)
var sum:Int
声明一个名字为predicate的函数类型变量(这个predicate变量的类型是函数)
predicate是一个以Char为参数,返回值为Boolean的函数。
var predicate: (Char) -> Boolean
声明一个以predicate函数为参数的函数(高阶函数),这个函数的返回类型是String
fun filter(predicate: (Char) -> Boolean) :String
让上面这个函数带上接受者,其实就是给String声明了一个扩展函数。
带上了接收者的函数,函数内部可以直接访问String的其他方法属性,相当于函数内部的this就是String
fun String.filter(predicate: (char) -> Boolean) :String
Kotlin和Java代码是可以混合调用的,因此Kotlin的函数引用在Java是有一种对应的形式,那就是Function引用,Function1<P, R>代表只有一个参数P的返回值为R的引用。
2. 标准高阶函数
2.1 标准高阶函数的声明
标准高阶函数声明在Standard.kt文件中,其中有TODO、run、with、apply、also、let、takeIf、takeUnless、repeat函数。
我们将功能类似的函数放在一块对比,如run & with、apply & also、takeIf & takeUnless、let & 扩展函数版本run。
2.2 run&with函数
/**
* 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代码块的返回值。
可以发现with和run都是返回了block(是个函数引用)的返回值。
区别在哪:
区别在于有个run是扩展函数,如果在使用之前需要判空,那么扩展函数版本的run函数的使用会比with函数优雅,如:
// Yack!
with(webview.settings) {
this?.javaScriptEnabled = true
this?.databaseEnabled = true
}
// Nice.
webview.settings?.run {
javaScriptEnabled = true
databaseEnabled = true
}
可以看到扩展函数版本的run函数在调用前可以先判断webview.settings是否为空,否则不进入函数体调用。
2.3 apply&also
/**
* 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,所以可以实现链式调用细化代码粒度,让代码更清晰,它们的区别是一个apply的block是用T(也就是apply的接收者本身)作为接收者,因此在apply的block内部可以访问到T这个this,also的T是被当做参数传入block的,所以在also的block内部需要用it(lambda的唯一参数)代表这个also的接收者T。
使用上:
【推荐】lambda表达式的block中,如果主要进行对某个实例的写操作,则该实例声明为Receiver;如果主要是读操作,则该实例声明为参数。
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
}
2.4 let函数
/**
* 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函数非常像,所以在上面的代码我把它们放在一起对比,他们的区别在run的block参数是个带run接收者T的函数引用,而let的block参数是把let的接收者T当做参数传给block,因此他们的调用区别是使用run时,block内部的this是指向T的,而在let的block内部需要使用it来指向T,let的block内部的this指的是T外部的this,意思是类似于你在Activity里面用let,let的block里面的this就是这个Activity实例。
2.4.1 let与also的返回值区别
let返回的是block的返回值,also返回的是接收者T自身,因此他们的链式调用有本质区别。
let能实现类似RxJava的map的效果
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 模式)。
2.5 takeIf&takeUnless
/**
* 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时返回接收者自身,否者返回null,takeUnless则刚好相反,是在predicate为false时返回接收者自身,否则返回null。
2.6 repeat函数
/**
* 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(第几次)。
2.7 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不能被省略的情况下,例如当它作为函数的参数被传递时it比this更短,更清晰。
在T.let允许使用更好的变量命名,你可以转换it为其他名称。
stringVariable?.let {
nonNullString ->
println("The non null string is $nonNullString")
}
2.8 这几个函数的选择:
| 调用链中保持原类型(T -> T) | 调用链中转换为其他类型(T -> R) | 调用链起始(考虑使用) | 调用链中应用条件语句 | |
|---|---|---|---|---|
| 多写操作 | T.apply { ... } | T.run{ ... } | with(T) { ... } | T.takeIf/T.takeUnless |
| 多读操作 | T.also { ... } | T.let{ ... } |
根据自己的需要选择适合的标准高阶函数
3. 自定义高阶函数
3.1 debug环境才运行的代码
//声明:
inline fun debug(code: () -> Unit){
if (BuildConfig.DEBUG) {
code()
}
}
//用法:
fun onCreate(savedInstanceState: Bundle?) {
debug {
showDebugTools();
}
}
函数声明为inline内联则会在编译时将代码复制粘贴到对应调用的地方,如果函数体很大很复杂,不建议使用内联,否则会使包体积增大。
未完待续...
4. 参考资源
Mastering Kotlin standard functions: run, with, let, also and apply
掌握Kotlin标准函数:run, with, let, also and apply
Anko: https://github.com/Kotlin/anko