Kotlin高阶函数的理解与使用

作者: 0xCAFEBABE51 | 来源:发表于2019-05-31 13:06 被阅读6次

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文件中,其中有TODOrunwithapplyalsolettakeIftakeUnlessrepeat函数。
我们将功能类似的函数放在一块对比,如run & withapply & alsotakeIf & takeUnlesslet & 扩展函数版本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代码块的返回值。
可以发现withrun都是返回了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,所以可以实现链式调用细化代码粒度,让代码更清晰,它们的区别是一个applyblock是用T(也就是apply的接收者本身)作为接收者,因此在applyblock内部可以访问到T这个thisalsoT是被当做参数传入block的,所以在alsoblock内部需要用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函数非常像,所以在上面的代码我把它们放在一起对比,他们的区别在runblock参数是个带run接收者T的函数引用,而letblock参数是let的接收者T当做参数传给block,因此他们的调用区别是使用run时,block内部的this是指向T的,而在letblock内部需要使用it来指向Tletblock内部的this指的是T外部的this,意思是类似于你在Activity里面用letletblock里面的this就是这个Activity实例

2.4.1 let与also的返回值区别

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

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时返回接收者自身,否者返回nulltakeUnless则刚好相反,是在predicatefalse时返回接收者自身,否则返回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不能被省略的情况下,例如当它作为函数的参数被传递时itthis更短,更清晰。
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

相关文章

网友评论

    本文标题:Kotlin高阶函数的理解与使用

    本文链接:https://www.haomeiwen.com/subject/bujvtctx.html