Kotlin高阶函数的理解与使用

Java基础

浏览数:96

2019-8-21

AD:资源代下载服务

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

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实例

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

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. Anko相关介绍

4.1 Anko库显示一个标准的对话框

alert("确定删除吗?","Alert") {
    positiveButton("OK") { Log.i(TAG, "你点了确定按钮")}
    negativeButton("Cancel") { Log.i(TAG, "你点了取消按钮") }
}.build().show()

4.2 Anko库包含的几个部分。

Anko consists of several parts:

  • Anko Commons: a lightweight library full of helpers for intents, dialogs, logging and so on;
  • Anko Layouts: a fast and type-safe way to write dynamic Android layouts;
  • Anko SQLite: a query DSL and parser collection for Android SQLite;
  • Anko Coroutines: utilities based on the [kotlinx.coroutines]

公共部分:intents,dialogs, logging等高阶函数。
布局部分:动态扩展函数快速添加layout,如4.1显示的标准对话框。
SQLite部分:查询解析集合DSL
协程部分:协程相关工具。

4.2.1 Anko Layouts (wiki)

Anko Layouts is a DSL for writing dynamic Android layouts. Here is a simple UI written with Anko DSL:

verticalLayout {
    val name = editText()
    button("Say Hello") {
        onClick { toast("Hello, ${name.text}!") }
    }
}

4.2.2 Anko SQLite (wiki)

Have you ever been tired of parsing SQLite query results using Android cursors? Anko SQLite provides lots of helpers to simplify working with SQLite databases.

For example, here is how you can fetch the list of users with a particular name:

fun getUsers(db: ManagedSQLiteOpenHelper): List<User> = db.use {
    db.select("Users")
            .whereSimple("family_name = ?", "John")
            .doExec()
            .parseList(UserParser)
}

详细的看Anko主页

5. 参考资源

Mastering Kotlin standard functions: run, with, let, also and apply
掌握Kotlin标准函数:run, with, let, also and apply
Anko: https://github.com/Kotlin/anko

作者:0xCAFEBABE51