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
网友评论