Kotlin 中的作用域函数

340 阅读2分钟

let、apply、run、also、with 这些function,它们的主要功能是为调用者函数提供一个内部作用域。

它们看起来大同小异,用起来似乎也经常可以互换,稍作修改就可以让代码依照正确的逻辑执行。初期我以为有什么深层的原因,使得 kotlin 要加入这么多相似的东西。后来把 kotlin 的原始代码抓下来翻 log,从 commit 信息来看,似乎只是为了要增加 function literal 可读性。

也就是说,这些 function 的使用上,真的互相替换也没什么大不了,只要你觉得这样让你代码更易读就可以了。

了解这一点之后,我自己也比较放宽心去使用它们。

使用 Lambda 的惯例

开始之前先提 lambda 的惯例。Kotlin 在把 lambda 当成函数的参数之时,有个惯例:

当 lambda literal 是函数调用的最后一个参数时,可以放到括号外面。如果 lambda 是函数的唯一一个参数,甚至可以拿到括号。

举例来说,如果我有一个函数叫 foo,它接受一个参数,而且该参数是个 lambda。

val lambda = { x: Int -> println(x) }
foo(lambda)
//可以写成
foo{ x: Int -> println(x) }

前面提到的那些 function,全部都是用这种方式去运作。所以才会使 let、apply 等看起来像是关键字,用起来像是 kotlin 语言的一部分,其实只是函数调用。

使用 apply

上下文对象是 this,返回值也是 this。

主要用与操作对象的成员。当初始化一个新对象时,最常出现这种情况。下面的代码展示了这种情况:

val peter = Person().apply {
    // only access properties in apply block!
    name = "Peter"
    age = 18
}

不使用 apply() 函数的等效代码如下:

val clark = Person()
clark.name = "Clark"
clark.age = 18

使用 also

上下文对象是参数 it。返回值也是 it。

also 适合执行一些将上下文对象作为参数的操作。在你需要一个对象引用的时候使用 also,而不是用到这个对象的属性和方法。或者当你不想覆盖外部作用域的 this 对象。

当你在代码中看到 also 时,你可以理解为 “紧接着对这个对象执行以下操作”。

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

使用 let

上下文对象是参数 it,返回值是 lambda 表达式的结果。

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

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

使用 let 可以这么写:

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let { 
    println(it)
    // and more function calls if needed
} 

如果代码块是一个 it 作为作为参数且是唯一参数的函数,你可以使用方法引用(::)来代替 lambda:

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

let 通常用于执行具有非空值的代码块。使用安全操作符:?.

getNullablePerson()?.let {
    // only executed when not-null
    promote(it)
}

val driversLicence: Licence? = getNullablePerson()?.let {
    // convert nullable person to nullable driversLicence
    licenceService.getDriversLicence(it) 
}

val person: Person = getPerson()
getPersonDao().let { dao -> 
    // scope of dao variable is limited to this block
    dao.insert(person)
}

上面的等效代码如下所示:

val person: Person? = getPromotablePerson()
if (person != null) {
  promote(person)
}

val driver: Person? = getDriver()
val driversLicence: Licence? = if (driver == null) null else
    licenceService.getDriversLicence(it)

val person: Person = getPerson()
val personDao: PersonDao = getPersonDao()
personDao.insert(person)

还有一种情况情况,使用 let:引入有限范围的局部变量以提高代码的可读性。要为上下文对象定义新变量,来替代lambda 的默认参数 it。

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 + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")

使用with

with 是非扩展函数:上下文对象作为参数传递,但是在 lambda 内部,可以使用 this。返回值是 lambda 结果。

在不需要上下文对象的 lambda 结果时建议使用 with 函数。你可以理解为 “使用此对象,执行以下操作”。

val person: Person = getPerson()
with(person) {
    print(name)
    print(age)
}

上面的等效代码如下所示:

val person: Person = getPerson()
print(person.name)
print(person.age)

另一种使用 with 的情况是引入一个辅助对象(用上下文对象的属性和函数计算出来)。

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

使用run

上下文对象是 this,返回值是 lambda 结果。

当你的 lambda 既包含对象的初始化,又包含返回值的计算时,run() 是非常有用的。

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

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

// the same code written with let() function:
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

run 允许你在需要表达式的地方执行一个由多个语句组成的块:

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
    println(match.value)
}

决策树

image.png