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_standard_function_selections.png

参考:

  1. https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容