作用域函数——let、run、with、apply、also

398 阅读3分钟

参考文章

作用域函数 - Kotlin 语言中文站

概述

这5个作用域函数,包括通用的扩展函数和普通函数,它们的唯一目的是在对象的上下文中执行代码块。当对一个对象调用这样的函数并提供一个 lambda 表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称——即扩展函数的作用。

典型用法:

Person("Alice", 20, "Amsterdam").let {
    println(it)
    it.moveTo("London")
    it.incrementAge()
    println(it)
}

如果不使用 let 来写这段代码,就必须引入一个新变量,并在每次使用它时重复其名称。

val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)

区别:

由于作用域函数本质上都非常相似,每个作用域函数之间有两个主要区别:

  • 引用上下文对象的方式
  • 返回值

调用方式

run函数是唯一一个可以直接使用的:run{}

with函数是唯一一个以传入参数的方式调用的:with(T){}

作用域

this作用域:run、with、apply

  • 作为 lambda 表达式的接收者

it作用域:let、also

  • 作为 lambda 表达式的参数
  • 当将上下文对象作为参数传递时,可以为上下文对象指定在作用域内的自定义名称
fun getRandomInt(): Int {
    return Random.nextInt(100).also { value ->
        writeToLog("getRandomInt() generated value $value")
    }
}

val i = getRandomInt()

返回值

返回程序执行的结果:run、let、with

  • 使用其结果给一个变量赋值
  • 忽略返回值,仅使用作用域函数为变量创建一个临时作用域

返回调用者本身:also、apply

  • 可以作为辅助步骤包含在作用链中
  • 可以用在return 语句中

let

  • 上下文对象作为 lambda 表达式的参数(it)来访问。
  • 返回值是 lambda 表达式的结果,即最后一行

T.let{}

fun main() {
    val str = "Hello"
    str.let {
        println("The receiver string's length is ${it.length}")
    }
}

with

  • 非扩展函数
  • 上下文对象作为参数传递,但是在 lambda 表达式内部,作为接收者(this)使用
  • 返回值是 lambda 表达式的结果,即最后一行

with(T){ }

  • with 可以理解为“对于这个对象,执行以下操作”
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}
  • 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 表达式结果

run{ }

用作非扩展函数,作为一个具有独立作用域的代码块进行一些操作,并返回该代码块最后执行的结果。

//这里str = "abc"
val str = run {
   1
   2
   "abc"
}

//在实际开发中可能会这样用
//比如如果用户登录了就弹奖励弹窗,没有登录就弹去登录弹窗
run {
   if(isLogin) {
      rewardDialog("去领奖")
   }else {
      return@run loginDialog("去登录“)
   }

}.show()

T.run{ }

runwith 做同样的事情,但是调用方式和 let 一样——作为上下文对象的扩展函数,并返回最后的结果。

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

apply

  • 上下文对象作为 lambda 表达式的参数(it)来访问
  • 返回值是调用者对象本身。

T.apply{ }

可以理解为“将以下赋值操作应用于对象”。

val adam = Person("Adam").apply {
    age = 32
    city = "London"        
}
println(adam)

also

  • 上下文对象作为 lambda 表达式的参数(it)来访问
  • 返回值是上下文对象本身。

T.also{ }

  • 可以将其理解为“并且用该对象执行以下操作”。
val numbers = mutableListOf("one", "two", "three")

numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")

补充:takeIf 、takeUnless

takeIf 函数和 takeUnless 函数允许在链式调用中加入对象的状态检查。

在写if语句的时候会遇到这样的场景:前面调用了一个函数计算得出了一个结果,现在需要对这个结果做一个分支判断,并且我们只需要用到if的一个分支时,可以用takeIf和takeUnless代替

fun testWithoutTakeIf() {
    val name = "test"
    val t = name.indexOf("t")
    Log.i(TAG, "testWithoutTakeIf: t = $t")
    if (hasYan >= 0) {
        Log.i(TAG, "testWithoutTakeIf: has t")

    }
    Log.i(TAG, "testWithoutTakeIf: $name")

}

输出:
I: testWithoutTakeIf: t = 0
I: testWithoutTakeIf: has t
I: testWithoutTakeIf: test

可以用takeIf这样写:

fun testTakeIf() {
    val name = "test"
    name.indexOf("t")
        .takeIf {
            Log.i(TAG, "testTakeIf: it = $it")
            it >= 0
        }
        ?.let {
           Log.i(TAG, "testTakeIf: has t")
        }
    Log.i(TAG, "testTakeIf: $name")
}

输出:
I: testTakeIf: it = 0
I: testTakeIf: has t
I: testTakeIf: test

takeIf

  • 是扩展函数
  • 上下文对象的引用方式:it
  • 返回值:如果代码块predicate里面返回为true,则返回这个对象本身,否则返回空
  • 使用注意:结果要用?判空

takeUnless

  • 是扩展函数
  • 上下文对象的引用方式:it
  • 返回值:如果代码块predicate里面返回为false,则返回这个对象本身,否则返回空
  • 使用注意:结果要用?判空