目录
1. 前言
学习使用 Kotlin 有一段时间了。对于常用的几个函数:let
,run
,with
,apply
,also
却不是非常清楚了解。
本文主要内容包括:介绍 let
,run
,with
,apply
,also
函数的用法,以及它们之间的区别;并说明在实际开发中如何选用。
2. 正文
这几个函数都是包含在 Kotlin
的标准库中,它们的唯一目的就是在一个对象的上下文中执行一段代码。当我们使用提供的 lambda 表达式在一个对象上调用这样的一个函数时,它就形成了一个暂时的域。在这个域中,我们可以在不使用变量名的情况下获取到对象。所以,这些函数也被称为域函数。
这些函数做的事情基本上是一样的:在一个对象上执行一段代码。不同之处在于如何引用上下文对象(是 this
还是 it
)以及整个表达式的结果是什么。
2.1 上下文对象(context object):this
还是 it
在域函数的 lambda 内部,上下文对象不是通过它实际的变量名来获取的,而是通过一个短的引用来获取的。具体来说有两种方式:
this
,作为 lambda 的接受者;it
,作为 lambda 的参数。
需要说明的是 this
是可以省略的;it
是隐含的默认参数名,可以显式地指定其他名字。
官方文档上推荐:使用 this
,用来操作对象成员,如调用函数或者给属性赋值;使用 it
,作为方法调用的参数。
run
,with
,apply
函数引用上下文对象为 this
;而 let
,also
函数把上下文对象作为 it
。
请看下面的例子:
fun main(args: Array<String>) {
val hello = "Hello, World!"
// this
hello.run { println("The receiver string's length is $length") }
// 这行与上面是等效的
hello.run { println("The receiver string's length is ${this.length}") }
// it
hello.let { println("The receiver string's length is ${it.length}") }
}
/*
Result:
The receiver string's length is 13
The receiver string's length is 13
The receiver string's length is 13
*/
2.2 返回值
这些域函数的返回值也是有区分的,具体来看一下:
apply
,also
:返回上下文对象;let
,with
,run
:返回 lambda 结果。
下面具体来看每一个函数:
2.3 let
上下文对象作为 it
来使用,返回值是 lambda 结果,它是一个扩展函数。
看一个例子:
data class Person1(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) {
city = newCity
}
fun incrementAge() {
age++;
}
}
fun main(args: Array<String>) {
// 使用 let 函数
Person1("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
// 不使用 let 函数
val person1 = Person1("Alice", 20, "Amsterdam")
println(person1)
person1.moveTo("London")
person1.incrementAge()
println(person1)
}
上面的例子:通过使用 let
函数,省去了一个变量的声明。
let
实际使用案例1:和安全调用运算符(?.
)结合使用,这是一种很典型的应用。
fun processNotNullString(str: String) {}
fun main(args: Array<String>) {
val str: String? = "hello"
val length = str?.let {
println("let() called on $it")
it.length
}
}
let
实际使用案例2:在有限的域内引入一个局部变量,来改变代码可读性。
fun main(args: Array<String>) {
// do not use let
val numbers1 = listOf("one", "two", "three", "four", "five")
var first = numbers1.first()
println("The first item of the list is '$first'")
first = if (first.length > 5) first else "!$first!"
val modifiedFirst = first.toUpperCase()
println("First item after modification: '$modifiedFirst'")
println("------------------------------------------------------------")
// use let
val numbers = listOf("one", "two", "three", "four", "five")
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 modification: '$modifiedFirstItem'")
}
/*
The first item of the list is 'one'
First item after modification: '!ONE!'
------------------------------------------------------------
The first item of the list is 'one'
First item after modification: '!ONE!'
*/
2.4 with
上下文对象是作为 with
函数的参数传入的。在 lambda 内部,上下文对象作为 this
来使用,返回值是 lambda 结果,它不是一个扩展函数。
with
实际使用案例1:在这个对象上调用它自己的函数和属性,而不提供 lambda 结果。可以这样理解:带着这个对象,做如下的操作。
下面的例子是 Android 中在Adapter
中绑定数据和视图的代码,这也是一个典型使用场景:
data class DownloadInfo(
val fileName: String,
var select: Boolean = false
)
fun bindItem(downloadInfo: DownloadInfo, position: Int) {
with(downloadInfo) {
itemView.tv_url.text = fileName
itemView.cb_download_url.isChecked = select
}
}
with
实际使用案例2:引入一个辅助对象,它的属性和方法会被用于计算一个值。
fun main(args: Array<String>) {
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()},"+
" the last element is ${last()}"
}
println(firstAndLast)
}
2.5 run
run
函数可以是扩展函数,也可以不是扩展函数。
作为扩展函数时,在 lambda 内部,上下文对象作为 this
来使用,返回值是 lambda 结果。
run
函数做的事情和 with
函数是一样的,调用起来和 let
函数是一样的。
run
实际使用案例:在 lambda 内部,包含对象初始化以及返回值的计算。
class MultiportService(var url: String, var port: Int) {
fun prepareRequest(): String = "Default request"
fun query(request: String): String = "Result from query '$request'"
}
fun main(args: Array<String>) {
val service = MultiportService("https://example.kotlinlang.org", 80)
// use run,和 let 调用方式一样,lambda 内部和 with 是一样的
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// use let
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
// use with
val withResult = with(service) {
port = 8080
query(prepareRequest() + " to port $port")
}
println(result)
println(letResult)
println(withResult)
}
/*
Result:
Result from query 'Default request to port 8080'
Result from query 'Default request to port 8080'
Result from query 'Default request to port 8080'
*/
作为非扩展函数时,返回结果还是 lambda 结果,但是没有接收对象了。简言之,就是运行一段代码而已。
2.6 apply
在 lambda 内部,上下文对象作为 this
来使用,返回值是 对象本身,它是一个扩展函数。
apply
的典型应用是对象配置,可以这样说把如下的操作应用在这个对象上。
data class Person2(var name: String, var age: Int = 0, var city: String = "")
fun main(args: Array<String>) {
val adam = Person2("Adam").apply {
age = 32
city = "London"
}
println(adam)
}
实际开发案例,配置 Intent
:
fun play(context: Context, url: String, title: String){
val intent = Intent(context, PlayerActivity::class.java).apply {
putExtra(INTENT_URL, url)
putExtra(INTENT_TITLE, title)
}
context.startActivity(intent)
}
2.7 also
在 lambda 内部,上下文对象作为 it
来使用,返回值是 对象本身,它是一个扩展函数。
also
用于把上下文对象作为参数来执行一些操作。这些是不会改变上下文对象的操作,例如打印日志,给其他变量赋值。
双重校验锁的单例写法的例子:
class TabNoteRepository {
companion object {
@Volatile
private var instance: TabNoteRepository? = null
fun getInstance() =
instance ?: synchronized(this) {
instance ?: TabNoteRepository().also { instance = it}
}
}
}
3. 最后
上面虽然对 let
,run
,with
,apply
,also
函数一一作了介绍,但是在实际开发中,还是很容易混淆使用的。所以,这里把它们之间的关键区别放在表里:
Scope function | Object reference | Return value | is extension function? |
---|---|---|---|
let | it | lambda result | Yes |
run | this | lambda result | Yes |
run | - | lambda result | No |
with | this | lambda result | No |
apply | this | object itself | Yes |
also | it | object itself | Yes |
官方文档也给出一些使用指导:
- 在非空对象上执行一个 lambda:
let
; - 在局部域中引入一个表达式作为变量:
let
; - 对象配置:
apply
; - 对象配置并计算结果:
run
; - 额外的效果:
also
; - 在一个对象上组合函数调用:
with
。
参考
1.https://kotlinlang.org/docs/reference/scope-functions.html