Kotlin Scope Function

203 阅读7分钟

在 Kotlin 中,经常会发现使用 let 、apply 关键字后面带有代码块的写法。这些用法是 Kotlin 标准库提供的作用域函数。它们的典型特点是:

通过一个对象调用这些函数,这个对象称为**上下文对象 **。这些函数是 lambda 表达式的形式使用。在 lambda 表达式中,可以访问上下文对象

上下文对象.函数名 { 上下文对象引用 ->
    ...
}

当然也有比较特殊的 with 和 run 等。实际上有 6 个作用域函数:letrun 、 非拓展函数的 runwithapplyalso

这些函数的作用基本上是一样的:在一个对象上执行一段代码。不同的是这个对象如何在代码块中使用,以及代码块最终的返回结果。

例如,下面这个典型的用法:

Person("Alice", 20, "Amsterdam").let { it ->
    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)

作用域函数没有引入任何新的技术能力,但它们可以让代码更加简洁易读。

由于作用域函数的性质相似,在实际情况中选择正确的函数可能有点棘手。下面我们将详细描述作用域函数之间的区别及其使用约定。

不同之处

为了帮助您为您的目的选择正确的作用域函数,我们提供了它们之间的主要区别表。

函数对象引用返回值是否是拓展函数
letitLambda resultYES
runthisLambda resultYES
run-Lambda resultNO,在没有上下文对象的情况下调用
withthisLambda resultNO,需要一个上下文对象作为参数
applythisContext 对象本身YES
alsoitContext 对象本身YES

为了方便区分,可以从以下角度进行记忆:

  • run 函数有两个。分别是拓展函数的情况,此时需要通过一个上下文对象调用;另一种情况是直接使用 run 方法,此时不需要依靠任何对象来调用。
  • **非拓展函数的作用域函数有两个,run 和 with ,其他都是拓展函数。**它们都比较特殊,不需要依赖上下文对象进行调用。run 是无需任何对象就可以调用;with 需要将一个对象作为参数调用。
  • apply 和 also 的返回值是上下文对象本身,其他的都是 lambda 表达式的返回结果。
  • 只有 let 和 also 函数的 lambda 表达式中,通过 it 对上下文对象进行使用。
  • 非拓展函数 run 不需要依赖一个对象进行调用,所以它没有对象引用。

总体来说,除了拓展函数的区别,作用域函数最重要的区别就是 lambda 中引用上下文对象的方式不同和返回值不同。

上下文对象的引用方式

在作用域函数的 lambda 内部,上下文对象可通过一个代称来进行引用而不是实际变量名本身。每个作用域函数使用两种方式之一来访问上下文对象:

  • 作为 lambda 接收器 (this)
  • 作为 lambda 参数 (it)。

两者都提供相同的功能,因此我们将针对不同情况描述每种功能的优缺点,并就它们的使用提供建议。

fun main() {
    val str = "Hello"
    // this
    str.run {
        println("The string's length: $length")
    }

    // it
    str.let {
        println("The string's length is ${it.length}")
    }
}

this

runwithapply 将上下文对象称为 lambda 接收器,在它们的 lambda 中使用通过 this 关键字访问上下文对象。在类中使用自身,也是可以通过 this 关键字访问自身实例的。因此,该对象就像在普通类的函数中一样使用。

this 的好处是,可以直接省略 this 本身,直接访问对象,代码更短。但这样也会让你不好区分访问的是类本身中的内容,还是上下文对象中的内容。

因此,对于主要对上下文对象的属性或方法进行操作的 lambda,建议将上下文对象作为接收器 (this):主要是调用上下文对象自身的函数或为其分配属性。

val adam = Person("Adam").apply { 
    age = 20                       // same as this.age = 20
    city = "London"
}
println(adam)

it

反过来, let 也将上下文对象作为 lambda 参数。如果未指定参数名称,则通过隐式默认名称访问对象。it 比 this 更短,并且带有 it 的表达式通常比 this 表达式更容易理解。但是 it 不支持 this 一样的隐式调用,不可以省略。因此,当对象主要用作函数调用中的参数时,将上下文对象作为 it 是更合适的。

总结

  • it 更适用于将上下文对象作为参数,或代码块存在多个变量时使用。
  • this 更适用于对上下文对象本身的属性和方法进行操作时使用。

返回值

作用域函数因返回的结果而异,可以分为两组:

  • apply 和 also 返回上下文对象本身。
  • let 、run 和 with 返回 lambda 表达式的结果。

这两个选项可让您根据代码中的下一步操作来选择正确的函数。

上下文对象

apply 和 also 的返回值是上下文对象本身。因此,它们可以使用一个调用链来做一些附加操作。调用一些链式调用方法在同一个对象上附加额外功能。

val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
    .apply {
        add(2.71)
        add(3.14)
        add(1.0)
    }
    .also { println("Sorting the list") }
    .sort()

它们也可以用于返回上下文对象的函数的返回语句中。

fun getRandomInt(): Int {
    return Random.nextInt(100).also {
        writeToLog("getRandomInt() generated value $it")
    }
}

val i = getRandomInt()

Lambda 结果

Let 、run 和 with 返回 lambda 表达式结果。因此你可以在需要将结果分配给变量时使用这些方法。

val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run { 
    add("four")
    add("five")
    count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")

此外,您可以忽略返回值并使用范围函数为变量创建临时范围。

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    val firstItem = first()
    val lastItem = last()        
    println("First item: $firstItem, last item: $lastItem")
}

选择困难

这些函数看起来功能并没有太多差别,如何在实际的代码逻辑中选择,官方给我们提供了一些规则:

  • 在非空对象上执行 lambda :let ,经常配合可空安全运算符 ? 使用。
  • 在局部范围内将表达式作为变量使用: let
  • 对象配置:apply
  • 对象配置并计算一个结果:run
  • 在需要一个表达式的地方运行语句:非拓展函数的 run
  • 附加一些效果:also
  • 对对象进行分组函数调用:with

尽管作用域函数是一种使代码更简洁的方法,但请避免过度使用它们:它会降低代码的可读性并导致错误。

避免嵌套作用域函数并在链接它们时要小心:很容易对当前上下文对象和 this 或 it 的值感到困惑。

let

上下文对象可用作参数 (it)。返回值是 lambda 结果。let 通常用于执行仅具有非空值的代码块。要对可空对象执行操作,需要配合 ? 进行使用。使用 let 的另一种情况是引入具有有限范围的局部变量以提高代码可读性。可以将参数 it 命名为其他名称,更容易理解。

with

with 不是拓展函数,上下文对象需要作为参数传递给 with 。with 方法推荐在不需要 lambda 返回结果时使用,意义是 “使用此对象,执行一些操作” 。

拓展函数 run

拓展函数的 run 做的事情和 with 相同。

非拓展函数的 run

非拓展函数的 run 不依赖上下文对象,只是执行一个 lambda 代码块。

apply

上下文对象可用作接收器 (this)。返回值是对象本身。 apply 的常见情况是对象配置。此类调用可以理解为“将以下分配应用于对象”。

also

上下文对象可用作参数 (it)。返回值是对象本身。适用于执行一些将上下文对象作为参数的操作。用于需要引用对象而不是其属性和函数的操作,或者当您不想从外部范围隐藏 this 引用时。