[译]精通Kotlin标准函数:run、with、let、also和apply

原文地址:https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84java

一些 Kotlin 的标准函数很是类似,以致于咱们都没法肯定要使用哪个。这里我会介绍一种简单的方式来区分他们的不一样点以及如何选择使用。git

做用域函数

接下来聚焦的函数有:runwithT.runT.letT.also 以及 T.apply。我称他们为做用域函数(scoping functions),由于它们为调用方函数提供了一个内部做用域。github

最可以体现做用域的是 run 函数:web

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,而且它彻底被包裹(enclosed)在 run 的区域内。编程

这个做用域函数自己看起来并不会很是有用。可是除了拥有单独的区域以外,它还有另外一个优点:它有返回值,即区域内的最后一个对象。app

所以,下面的代码会变得整洁,咱们把 show() 函数应用到两个 view 之上,可是并不须要调用两次。less

run {
    if (firstTimeView) introView else normalView
}.show()
复制代码

这里演示所用,其实还能够简化为 (if (firstTimeView) introView else normalView).show()函数

做用域函数三大特性

为了让做用域函数更有意思,可将其行为分类为三大特性。我会使用这些特性来区分彼此。this

1、正常 vs. 扩展函数

若是咱们看一下 withT.run,会发现它们的确很是类似。下面的代码作了一样的事情。spa

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

// similarly

webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}
复制代码

可是,它们的不一样点在于,一个是正常函数(即 with),另外一个是扩展函数(即 T.run)。

假设 webview.settings 可能为空,那么代码就会变成下面的样子:

// 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.runT.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 而不是 ${this.length}。我把它称之为:this 做为参数进行传递。

可是,对于 T.let 函数,你会发现 T.let 把它本身传入了函数 block: (T)。所以它被当作一个 lambda 参数来传递。在做用域函数内它能够被引用为 it。因此我称之为:it 做为参数进行传递。

从上面能够看出,T.run 好像比 T.let 高级,由于它更隐式一些,可是 T.let 函数会有些一些微妙的优点:

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

  • this 不能被省略的状况下,例如用做一个函数参数,itthis 更短更清晰。

  • T.let 容许用更好的命名来表示转换过的所用变量(the converted used variable),也就是说,你能够把 it 转换为其余名字:

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

3、返回 this vs. 其余类型

如今,咱们看一下 T.letT.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

简单的示例以下:

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 貌似没什么意义,由于咱们能够轻松把它们组合进一个单一的函数块内。仔细想一下,它们会有以下优点:

  • 它能够为相同的对象提供清晰的处理流程,可使用粒度更小的函数式部分。
  • 它能够在被使用以前作灵活的自处理(self manipulation),能够建立一个链式构造器操做。

若是二者结合链式来使用,一个进化本身,一个持有本身,就会变得很是强大,例如:

// 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() }
复制代码

回顾一下全部的特性

经过这三个特性,咱们能够清楚地知道每一个函数的行为。让咱们举例说明一下上面没有提到的 T.apply 函数,它的 3 个特性以下所述:

  • 它是一个扩展函数
  • 它把 this 做为参数
  • 它返回了 this(它本身)
// 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) }
复制代码

或者咱们也能够把一个非链式的对象建立过程变得可链式(chain-able):

// 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) }
复制代码

函数选择

如今思路变清晰了,根据这三大特性,咱们能够对函数进行分类。基于此能够构建一个决策树来帮助咱们根据须要来选择使用哪个函数。

但愿上面的决策树可以更清晰地阐述这些函数,同时也能简化你的决策,使你可以得当地使用这些函数。

相关文章
相关标签/搜索