学会使用Kotlin标准函数:run / with / let 和 apply

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

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

1*9nUzj5iRxj_Hddni6ob28w.png

有一些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
}

<我注: 输出结果>

I am happy
I am sad

在上面代码的test函数中, 你可以使用run关键字定义一个单独的代码块, 在这个代码块中在打印输出之前将mood变量的值改为I am happy. 同时在run代码块中定义的mood的值只能作用于这个代码块. 因为你会发现在run代码块之外, 再去打印mood 输出的是 I am sad.

限定变量作用域的这个功能本身并没有太大用处. 但是除此之外他有另外一个有趣的功能点, 那就是他还可以有返回值: 返回在代码块范围内修改后的对象.

如此以来下面的代码看起来就比较整洁:

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

这段代码中, run代码块根据不同的条件返回了不同的对象, 然后调用不同对象的show()方法. 这样我们就不必单独维护两个变量来分别调用他们的show方法.

范围函数的3种特性

为了让范围函数更有意思, 我把他们的不同表现总结为3种特性. 我将使用这些特性来把他们区分开.

1. 普通函数 vs. 扩展函数 (normal vs. extension function)

如果我们观察 withT.run, 我们发现他们两个实际作用很相似. 比如下面这段代码:

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

上面代码中用withT.run 实现了同样的功能. 但是他们的不同之处在于: with是一个普通函数, 而T.run则是一个扩展函数.

那么问题来了, 这两种用法各自的优点是什么?

我们假设 webview.settings 这个变量的值有可能为null的话, 他们的不同点就体现出来了:

// Yack! -- 代码块中在对webview.settings对象进行操作之前都需要进行判空操作
with(webview.settings) {
      this?.javaScriptEnabled = true
      this?.databaseEnabled = true
}
// Nice.
webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

在这个例子中, 很明显 T.run 这种扩展函数的方式更好, 因为我们可以在使用对象之前, 对他进行全局的判空操作. (<我注:>而with那种方式需要在代码块中逐句判空)

2. thisit 参数

如果我们观察 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.run函数的代码块中, 可以使用this关键字来得到对主变量T的引用. 在实际编程中, 通过this关键字的调用通常可以不写this.. 所以在上面的示例代码中, 我们直接使用了println($length) 而不是println(${this.length}). 我将这种方式称为使用this作为参数的函数调用.

<我注: T.run的源码>

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

然而如果去看T.let函数的源码, 你会发现T.let是把主变量自己作为参数调用代码块: block: (T). 看起来像是使用lambda参数进行函数调用. 这种方式在代码块中是使用 it 来引用主变量. 所以我将这种方式称为: 使用it作为参数的函数调用.

<我注: T.let的源码>

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

从上面的论述中, 看起来T.run用起来比T.let更方便些, 因为使用T.run我们可以直接隐式使用this访问主变量, 而T.let需要主动指定使用it才能访问主变量. 但是使用T.let函数还有一些细微的好处:

  • T.let提供了一种更清晰的方式来区分访问的属性或方法是来自于调用函数的主变量, 还是来自其他外部的变量.
  • this需要显示传递的时候: 比如在调用另外的方法需要把this作为参数传递过去, 这种情况下, 使用it(2个字母) 比使用this(4个字母) 更短, 也更清晰.
  • T.let 允许在作用域范围内对it重命名为更加有意义的变量名称. 比如:
stringVariable?.let {
      nonNullString ->
      println("The non null string is $nonNullString")
}

3. 返回 this 或是 其他类型

现在我们来看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(代码块的主变量).

T.letT.also在链式调用方面都非常好用, T.let可以将操作之后的结果返回, T.also可以在主变量上进行操作然后再返回this主变量.

下面是对T.letT.also的简单示例代码:

val original = "abc"
// 改变主变量的值并向后传递
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // 将it的内容反转并传递到下一步
}.let {
    println("The reverse String is $it") // "cba"
    it.length  // it的值类型从string转变为int
}.let {
    println("The length of the String is $it") // 3
}
// 错误示例
// 整个链上都是同样的值 (打印结果与期望不同)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // 尽管把it的值反转了 但他并没有把反转后的结果传递到下一步
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // 尽管返回了it的长度但这个值并没有传递到下一步
}.also {
    println("The length of the String is ${it}") // "abc"
}
// 使用 `also` 来得到相同的结果 (也就是在原来字符串的基础上进行操作
// 整个链上传递的值都是一样的
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 似乎看起来毫无意义, 因为我们可以直接将他们放到一个单独的代码块中即可实现相同的功能. 其实仔细考虑一下, T.also还是有一些好处的:

  1. 他可以让整个链上的操作过程显得更加清晰: 将整个操作分开到不同的更小的代码块中来完成
  2. 在使用对象之前分步骤对self进行操作来使用链式构造, 此时T.also会显的更加方便易用.

当把这两个函数联合使用时(一个在this的基础上改进并返回, 一个保持this的引用并返回), 范围函数的功能会更加强大, 比如:

// 常规方式
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}
// 使用 `let` / `also`的改进方式
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

其他特性

通过上述对3种特性的描述, 我们了解了各自函数的特性. 现在来说说尚未提及的 T.apply 函数, 这个函数相应的特性如下:

  1. 他是一个扩展函数
  2. T.run类似, T.apply也是传递this到代码块
  3. T.also类似, T.apply也是返回this的引用

因此一个可以想象到的应用方式如下:

// 普通方式
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}
// 使用 `apply` 改进后的方式
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }

或者我们可以把不可链式调用的代码变成链式调用的代码风格:

// 普通方式 非链式调用
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}
// 改进后的链式调用风格
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

如何选择使用哪个函数

通过上述对各个函数的3种特性的描述, 我们可以对他们进行归类. 基于上述特性, 我们可以总结出下面的决策树来帮助我们根据具体需求来决定应该使用哪个函数.

1*pLNnrvgvmG6Mdi0Yw3mdPQ.png

希望上面的决策树插图能将这些标准函数的特性描述的更清晰一些, 希望能使你更方便的决定应该使用哪个函数来操作, 同时更好的掌握对这些函数的恰当使用.

很愿意听到大家提供对这些标准函数的真事使用场景, 大家一起讨论一起进步.

希望你能通过这篇文章理解到这些标准函数的使用方式, 如果你感觉有帮助可以分享给你的程友.


再次注明:
原文链接: https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84

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

推荐阅读更多精彩内容