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)
}