[译]学会 Kotlin 标准函数: run, with, let, also and apply

497

翻译说明:

原标题: Mastering Kotlin standard functions: run, with, let, also and apply

原文地址: medium.com/@elye.proje…

原文作者: Elye

有些Kotlin的标准函数非常相似,以至于我们不确定该使用哪个。在这里,我将介绍一种简单的方法来清楚区分它们的差异,并选择使用哪个。

作用域函数

我将重点介绍的函数是run、with、T.run、T.let、T.also和T.apply。我将它们称为作用域函数,因为我认为它们的主要功能是为调用者函数提供一个内部作用域。

最简单的说明作用域的方法是使用run函数。

fun test() {
    var mood = "I am sad"

    run {
        val mood = "I am happy"
        println(mood) // I am happy
    }
    println(mood)  // I am sad
}

通过这种方式,在test函数内部,您可以拥有一个独立的作用域,在打印之前重新定义mood为"I am happy",并且它完全封闭在run作用域内部。

这个作用域函数本身似乎并不是很有用。但它有一个很好的特点,除了作用域之外,它还返回一些东西,即作用域内的最后一个对象。

因此,下面的代码将会很简洁,我们可以将 show()应用于两个视图,而不需要调用两次。

 run {
        if (firstTimeView) introView else normalView
    }.show()

作用域函数的3个属性

为了使作用域函数更有趣,让我将它们的行为归类为3个属性。我将使用这些属性来区分它们之间的差异。

1.普通函数 vs. 扩展函数

如果我们看 with 和 T.run 函数,它们非常相似。下面的示例做的是相同的事情。


with(webview.settings) {
    javaScriptEnabled = true
    databaseEnabled = true
}

// similarly
webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

然而,它们的区别在于一个是普通函数,即 with,而另一个是扩展函数,即 T.run。

那么,每个函数的优势是什么呢?

想象一下,如果 webview.settings 可能为 null ,它们将如下所示。


// Yack!
with(webview.settings) {
      this?.javaScriptEnabled = true
      this?.databaseEnabled = true
}

// Nice.
webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

在这种情况下,T.run 扩展函数更好,因为我们可以在使用之前应用可空性检查。

2. this vs. it 参数

如果我们看T.run和T.let函数,除了一个方面,它们非常相似,即它们接受参数的方式。下面展示了两个函数的相同逻辑。


stringVariable?.run {
      println("The length of this String is $length")
}

// Similarly.

stringVariable?.let {
      println("The length of this String is ${it.length}")
}

如果您检查 T.run 函数的签名,您会注意到 T.run 只是作为扩展函数调用 block:T.()。因此,在作用域内的所有位置,T 可以被称为 this。在编程中,this 大部分时间可以省略。因此,在上面的示例中,我们可以在 println 语句中使用 length,而不是length,而不是 {this.length}。我将此称为将 this 作为参数传入。

然而,对于 T.let 函数的签名,您会注意到 T.let 将自身作为参数发送到函数中,即 block:(T)。因此,这就像是将 lambda 参数发送进去。它可以在作用域函数内部作为 it 来引用。因此,我称之为将 it 作为参数传入。

从上面的例子中,似乎 T.run 比 T.let 更优越,因为它更加隐式,但 T.let 函数也有一些微妙的优势,如下所示:

  • T.let 能够更清楚地区分给定变量的函数/成员和外部类的函数/成员。

  • 当 this 不能省略时,例如当它作为函数的参数发送时,使用 T.let 比 this 更简洁和清晰。

  • T.let 允许更好地命名转换后使用的变量,即您可以将其转换为其他名称。


stringVariable?.let {
      nonNullString ->
      println("The non null string is $nonNullString")
}

3. 返回 this vs. 其他类型

现在,让我们看一下 T.let 和 T.also,它们在内部函数作用域中是相同的。


stringVariable?.let {
      println("The length of this String is ${it.length}")
}

// Exactly the same as below

stringVariable?.also {
      println("The length of this String is ${it.length}")
}

然而,它们的微小差异在于它们的返回值。T.let 返回不同类型的值,而 T.also 返回T本身,即 this。

这两个函数都对于链式函数非常有用,其中 T.let 让您进行操作的演进,而 T.also 则让您在同一个变量上执行操作,即 this。

简单示例如下:


val original = "abc"

// Evolve the value and send to the next chain
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // evolve it as parameter to send to next let
}.let {
    println("The reverse String is $it") // "cba"
    it.length  // can be evolve to other type
}.let {
    println("The length of the String is $it") // 3
}

// Wrong
// Same value is sent in the chain (printed answer is wrong)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // even if we evolve it, it is useless
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // even if we evolve it, it is useless
}.also {
    println("The length of the String is ${it}") // "abc"
}

// Corrected for also (i.e. manipulate as original string
// Same value is sent in the chain 
original.also {
    println("The original String is $it") // "abc"
}.also {
    println("The reverse String is ${it.reversed()}") // "cba"
}.also {
    println("The length of the String is ${it.length}") // 3
}

T.also 可能在上面看起来没有意义,因为我们可以将它们合并为一个函数块。仔细思考后,它具有一些优点:

  • 它可以在同一对象上提供非常清晰的分离过程,即创建更小的功能部分。
  • 它在使用之前可以进行自我操作,使其成为一个强大的链式构建操作。

当这两者结合在一起形成链式调用时,即一个演进自身,一个保持自身,就会变得非常强大,例如下面的示例:


// Normal approach
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}

// Improved approach
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

看所有的属性

通过观察这3个属性,我们可以相当了解函数的行为。让我来说明一下 T.apply 函数,因为之前没有提到它。T.apply 函数的3个属性如下:

  • 它是一个扩展函数。
  • 它将 this 作为参数传递。
  • 它返回 this(即它本身)。

因此,可以使用它来进行以下操作:


// Normal approach
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}

// Improved approach
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }
              

或者我们还可以使无需链式调用的对象创建可链式:

// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}

// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

函数选择

因此,很明显,通过这3个属性,我们现在可以相应地对这些函数进行分类。基于此,我们可以形成以下决策树,以帮助根据需求决定使用哪个函数。

希望上述决策树能更清晰地解释这些函数,并简化您的决策过程,使您能够适当地掌握这些函数的用法。

如果您能提供一些您如何在实际中使用这些函数的好例子,我会很乐意听取。这可能会对其他人有所帮助。


欢迎关注 Kotlin 中文社区!

中文官网:www.kotlincn.net/

中文官方博客:www.kotliner.cn/

公众号:Kotlin

知乎专栏:Kotlin

CSDN:Kotlin中文社区

掘金:Kotlin中文社区

简书:Kotlin中文社区