Kotlin | 作用域函数

什么是作用域函数(Scope Functions)?

Kotlin 标准库包含了几个特殊的函数,其目的是在调用对象的上下文环境(context)中执行代码块。当你在提供了 lambda 表达式的对象上调用此类函数时,它会形成一个临时作用域。在此作用域内,你可以在不使用其名称的情况下访问该对象,这些函数被称为作用域函数。在 Kotlin 中,作用域函数总共有五个,分别是:letrunwithapplyalso。接下来我们逐个详细分析。

开始分析之前,你可能需要简单了解下它大概长什么样,下面是个简单示例

data class Person(var name:String){
    fun say(words:String){
        println("$name says $words")
    }
}

fun main() {
    Person("skyrin").let{
        it.say("hello")
        println(it)
    }
}

如果不使用 let 的话,你需要先创建出对象,然后再执行调用

val person = Person("skyrin")
person.say("hello")
println(person)

所以,作用域函数的目的就是尽可能的让你的代码变得更简洁更具可读性,尽可能少的创建对象,仅此而已。

由于这 5 个作用域函数的性质有些相似,所以大家可能经常不知道在哪种情况下该使用哪个函数,以至于最终放弃使用作用域函数?,所以为了避免类似悲剧发生,我们首先来讨论一下他们之间的区别以及使用场景。

区别

由于作用域函数本质上非常相似,因此理解它们之间的差异非常重要。每个作用域函数有两个主要区别:

  • 引用上下文对象的方式
  • 返回值
上下文对象(Context):this 还是 it
this

runwithapply 通过 this 关键字引用一个 context 对象作为 lambda 接收者。于是,在他们的 lambda 中,this 对象可用于普通类函数中。大多数情况下,在访问接收者的成员时,可以省略 this 关键字,让代码保持简洁。另一方面,如果省略了 this ,你就很难区分你操作的函数或变量是外部对象的还是接收者的了,所以,context 对象作为一个接收者(this)这种方式推荐用于调用接收者(this) 的成员变量或函数。示例如下

data class Person(var name: String,var age: Int = 0,var city: String = "")
fun main() {
    val person = Person("Skyrin").apply {
        age = 18    // 等价于 this.age = 18 或闭包外部的 person.age = 18
        city = "Beijing"
    }
    // 如上写法可替代如下写法
    // person.age = 18
    // person.city = "Beijing"
    println(person)
}
it

letalso 有一个作为 lambda 参数传入的 context 对象,如果不指定参数名,则可以通过该 context 对象的隐式默认名称 it 来访问它,itthis 看上去更简洁,用于表达式中也会使代码更加清晰易读。但是,当你访问 context 对象的函数或者属性时,不能像 apply 那样省略 this ,因此,当 context 对象主要用作参数被其他函数调用时,用 it 更好一些。

import kotlin.random.Random
fun writeToLog(message: String) {
    println("INFO: $message")
}
fun getRandomInt(): Int {
    return Random.nextInt(100).also {
        writeToLog("getRandomInt() generated value $it")
    }
 }
fun main() {
    val i = getRandomInt()
}

你也可以为 context 对象指定任意参数名

import kotlin.random.Random
fun writeToLog(message: String) {
    println("INFO: $message")
}
fun getRandomInt(): Int {
    return Random.nextInt(100).also { value -> // use value replace it
        writeToLog("getRandomInt() generated value $value")
    }
}
fun main() {
    val i = getRandomInt()
}
返回值:Context 对象还是 Lambda 结果

作用域函数的返回值不同:

  • applayalso 返回 context 对象
  • letrunwith 返回闭包的运算结果
返回 Context 对象

applayalso 返回 context 对象,因此,它们可以结合起来进行链式调用

fun main() {
    val memberList = mutableListOf<Int>()
    memberList.also {
        println("填充 $it")
    }.apply {
        add(35)
        add(98)
        add(1)
        add(18)
    }.also {
        println("排序并打印 $it")
    }.also {
        it.sort()
        println(it)
    }
}

也可以在 return 语句中使用,将 context 对象作为函数的返回值

import kotlin.random.Random
fun main() {
    fun getRandomInt(): Int {
        return Random.nextInt(100).also { value ->
            writeToLog("getRandomInt() generated value $value")
        }
    }
    val i = getRandomInt()
}
fun writeToLog(message: String) {
    println("INFO: $message")
}
返回 Lambda 闭包结果

letrunwith 返回 lambda 闭包结果。所以,你可以将其执行结果赋值给变量,链式操作上一个闭包返回的结果

fun main() {
    val numbers = mutableListOf(1, 3, 5, 6, 7, 9)
    val biggerThan6 = numbers.run {
        add(10)
        add(12)
        filter { it > 6 }
    }
    println("The result of bigger than 6 is $biggerThan6")
}

此外,你可以忽略返回值,使用 with 作用域函数来为变量创建一个临时作用域

fun main() {
    val numbers = mutableListOf(1, 3, 5, 6, 7, 9)
    with(numbers){
        val first = first()
        val last = last()
        println("first item is $first and last item is $last")
    }
}

使用场景

下面介绍如何适当的选择作用域函数,从技术上来说,它们的功能在很多情况下都是可以互相转换的,所以下面的例子只是展示了一种通用做法,具体选择还是要看你的业务场景更适合哪种情况。

let

context 对象作为闭包参数(it)传入,返回值是闭包结果。

let 可用于在调用链的结果上调用一个或多个函数。例如,以下代码打印集合上的两个操作的结果

fun main() {
    val numbers = mutableListOf("one", "two", "three", "four", "five")
    val resultList = numbers.map { it.length }.filter { it > 3 }
    println(resultList)
}

使用 let 可以重写为

fun main() {
    val numbers = mutableListOf("one", "two", "three", "four", "five")
    numbers.map { it.length }.filter { it > 3}.let {
        println(it)
        // 执行更多方法调用
    }
}

如果闭包模块只有一个函数将 context 作为参数传入,你可以使用(::)替换 lambda

fun main() {
    val numbers = mutableListOf("one", "two", "three", "four", "five")
    numbers.map { it.length }.filter { it > 3}.let(::print)
}

let 也经常被用于执行闭包代码块中使用非空值的函数,要对非空对象执行操作,使用安全调用操作符 ?. 后跟 let 闭包,在此闭包中,原来的可空对象就可以被转换为非空对象执行操作

fun processNonNullString(str: String) {
    println(str.length)
}
fun main() {
    val str: String? = "Hello"
//    processNonNullString(str)       // 编译错误: str 为可空对象,要求参数为不可空对象
    val length = str?.let {
        println("let() called on $it")
        processNonNullString(it)      // 正常执行: 'it' 在 '?.let { }' 中为不可空对象
        it.length
    }
    println("result for let is $length")
}

使用 let 的另一种情况是引入局部变量,限制其作用域范围,以提高代码可读性。

fun main() {
    val numbers = listOf("one", "two", "three", "four")
    val modifiedFirstItem = numbers.first().let { firstItem ->
        println("The first item of the list is '$firstItem'")
        if (firstItem.length >= 5) firstItem else "!$firstItem!"
    }.toUpperCase()
    println("First item after modifications: '$modifiedFirstItem'")
}
with

非拓展函数。context 对象作为参数传递,但在 lambda 内部,它可用作接收器(this),返回值为 lambda 结果

官方建议是使用 context 对象调用函数而不提供 lambda 结果。在代码中,你可以简单的把 with 函数理解为 “使用此对象,执行以下操作”

fun main() {
    val numbers = mutableListOf("one", "two", "three")
    with(numbers) { // 使用 numbers 对象,执行 {} 中的操作 
        println("'with' is called with argument $this")
        println("It contains $size elements")
    }
}

with 的另一个用例是引入一个辅助对象,我们可以方便的使用此对象的属性或函数来计算值

fun main() {
    val numbers = mutableListOf("one", "two", "three")
    val firstAndLast = with(numbers) {
        "The first element is ${first()}," +
                " the last element is ${last()}"
    }
    println(firstAndLast)
}
run

context 对象可用作接收器(this),返回值为 lambda 结果

runwith 的作用类似,但是调用方法和 let 一样 —— 作为 context 对象的拓展函数

当你的 lambda 同时包含了对象初始化和返回值计算时,run 函数非常适合

lass MultiportService(var url: String, var port: Int) {
    fun prepareRequest(): String = "Default request"
    fun query(request: String): String = "Result for query '$request'"
}

fun main() {
    val service = MultiportService("https://example.kotlinlang.org", 80)

    val result = service.run {
        port = 8080
        query(prepareRequest() + " to port $port")
    }
  
    // 同样的代码使用 let() 函数重写:
    val letResult = service.let {
        it.port = 8080
        it.query(it.prepareRequest() + " to port ${it.port}")
    }
    println(result)
    println(letResult)
}

除了在接收器对象上调用run之外,还可以将其用作非扩展函数。非扩展 run 允许你执行需要表达式的多个语句块。

fun main() {
    val hexNumberRegex = run {
        val digits = "0-9"
        val hexDigits = "A-Fa-f"
        val sign = "+-"
        Regex("[$sign]?[$digits$hexDigits]+")
    }
    for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
        println(match.value)
    }
}
apply

context 对象可用作接收器(this),返回调用者本身

使用apply不会返回值的代码块,主要对接收器对象的成员进行操作。 apply的常见情况是对象配置。此类调用可以读作“将以下赋值应用于对象”。

data class Person(var name: String,var age: Int = 0,var city: String = "")
fun main() {
    val person = Person("Skyrin").apply {
        age = 18
        city = "Beijing"
    }
}

将接收器作为返回值,你可以轻松进行链式调用以处理更复杂的操作。

also

context 对象作为参数传入,返回调用者本身

also 适用于执行将 context 对象作为参数进行的一些操作。还可用于不更改对象的其他操作,例如记录或打印调试信息。通常,你可以在不破坏程序逻辑的情况下从调用链中删除 also 调用。

fun main() {
    val numbers = mutableListOf("one", "two", "three")
    numbers
        .also { println("The list elements before adding new one: $it") }
        .add("four")
}

函数选择

以下是它们之间的差异表,以帮助你选择合适的作用域函数

函数

对象引用

返回值

扩展函数

let

it

lambda 结果

run

this

lambda 结果

run

-

lambda 结果

否:无 context 对象

with

this

lambda 结果

否:将 context 对象作为参数

apply

this

调用者本身(context)

also

it

调用者本身(context)

以下是根据预期目的选择范围功能的简短指南:

  • 在非 null 对象上执行 lambda:let
  • 将表达式作为局部范围中的变量引入:let
  • 对象配置:apply
  • 对象配置并计算结果:run
  • 运行需要表达式的语句:非扩展 run
  • 附加效果:also
  • 对函数进行分组调用:with

takeIf 和 takeUnless

除了作用域函数之外,标准库还包含函数 takeIf 和 takeUnless。这些函数允许你在调用链中嵌入对象状态的检查。

这两个函数的作用是对象过滤器,takeIf 返回满足条件的对象或 null。takeUnless 则刚好相反,它返回不满足条件的对象或 null。过滤条件位于函数的 {} 中。

import kotlin.random.*

fun main() {
    val number = Random.nextInt(100)

    val evenOrNull = number.takeIf { it % 2 == 0 }
    val oddOrNull = number.takeUnless { it % 2 == 0 }
    println("偶数: $evenOrNull, 奇数: $oddOrNull")
}

在 takeIf 和 takeUnless 之后链接其他函数时,不要忘记执行空检查或安全调用(?.),因为它们的返回值是可空的。

fun main() {
    val str = "Hello"
    val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
    //val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() // 编译出错
    println(caps)
}

takeIf 和 takeUnless 与作用域函数一起使用特别有用。一个很好的例子是使用 let 来链接它们,以便在与给定条件匹配的对象上运行代码块。

fun main() {
    fun displaySubstringPosition(input: String, sub: String) {
        input.indexOf(sub).takeIf { it >= 0 }?.let {
            println("The substring $sub is found in $input.")
            println("Its start position is $it.")
        }
    }

    displaySubstringPosition("010000011", "11")
    displaySubstringPosition("010000011", "12")
}

总结

以上,就是所有作用域函数的功能及简单使用场景介绍,你可能已经发现,这其中有几个函数的功能相似甚至重叠,有人甚至觉得有这个时间去弄明白它们,早就用其它常规方式实现功能了,但有人就觉得这些函数非常简洁实用,用过就再也回不去了。我觉得这就是 Kotlin 的一种优点和缺点的体现,优点是它很灵活,灵活的不像 Native 语言,缺点是它太灵活了,太多的语法糖导致你容易忘记写这些代码要实现的目的,所以,虽然作用域函数是使代码更简洁的一种方法,但是要避免过度使用它们。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券