Kotlin标准库函数: run,let,also,apply,with

Kotlin标准库函数: run,let,also,apply,with

一些 Kotlin 的标准函数非常相似,以至于我们都无法确定要使用哪一个。这里我会介绍一种简单的方式来区分他们的不同点以及如何选择使用

作用域函数

run, with(T), T.run, T.let, T.also, T.apply都支持闭包作为参数; 但是他们为调用者,在闭包内部提供了一个内部作用域, 我称他们为作用域函数(scoping functions);

最明显的是 run 函数

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

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

run函数的区域内, mood变量被重新定义, 并不和外部的定义冲突, 并且定义之后的run函数的范围内, mood变量会覆盖外部定义;

这么看上去,除了方法内拥有单独的作用域外, 并非特别有用; 但是这些方法还有一个特点, 它们拥有返回值;
eg. 'run'的返回值是区域内的最后一个对象;

使用这个特性,可以使我们的代码更整洁:
eg.我们想选择对某个view调用show()方法, 并不需要对每个view都进行调用,

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

作用域函数的特点

1. 正常 vs. 扩展函数

我们看一下withT.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的扩展函数要好一些, 在使用变量之前,就做了非空检查; 而with内部需要每个都做检查;

2. this vs. it变量

上面说过, 这些函数内部, 新定义的变量都有单独的作用域, 不和外部冲突; 而这个作用域中, 最特殊的就是this 和 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 添加了一个方法; 因此, 在它的作用域中, T 可以被引用为this; 而在实际变成中, this大部分情况下可以省略; 在上面代码中, println中的$length, 实际上就是${this.length}. 这种我称之为this参数传递;

T.let的函数定义中, 你会发现T.let把它自己作为一个参数. 传递给了函数 block: (T); 而在lambda表达式中, 一个参数可以省略, 使用it代替; 因此,在作用域中, T被引用为了it; 这种我称之为**it参数传递;

从上面看, T.run好像比T.let更高级一点, 在T.run中可以隐式的使用this代替自身; 但是在部分情况下, T.let更合适一点;

  • T.let更容易区分当前作用域的函数/变量和外部类的函数/变量

  • this不能被省略的地方, it相比this更加清晰简短

  • T.let中可以使用更好的变量命名 (it是lambda省略参数的指代, 因此可以把it转换为其他名字)

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

2. 返回 this vs. 其他类型(block()函数的返回值)

看一下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的返回值,是对应调用的lambda表达式的返回值; 但是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似乎没什么作用, 我们可以把几个also的代码合并到一个函数块中, 但是细想一下, 会有下面几个优势:

  • 它可以为同一个对象, 提供更加清晰的处理流程, 提供更细力度的函数控制
  • 它可以构建链式调用

如果两者结合使用, 使用T.let升级自身, 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函数, 它的性质如下:

  • 它是一个扩展函数
  • 它把this作为参数传递, 在函数体内, 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) }

选择使用哪个函数?

根据函数的特点, 我们可以对函数进行分类, 构建一个决策树帮助我们选择使用哪个函数:

Kotlin标准库函数: run,let,also,apply,with_第1张图片
kotlin_standard_function_selections.png

参考:

  1. https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84

你可能感兴趣的:(Kotlin标准库函数: run,let,also,apply,with)