[译]掌握Kotlin中的标准库函数: run、with、let、also和apply

(转载)原文链接:https://juejin.im/post/5b0048ed518825428a2619ed


翻译说明:

原标题: 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的操作实现。

这个作用域函数本身似乎看起来不是很有用。但是这还有一个比作用域有趣一点是,它返回一些东西,是这个作用域内部的最后一个对象。

因此,以下的内容会变得更加整洁,我们可以将show()方法应用到两个View中,而不需要去调用两次show()方法。

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

作用域函数的三个属性特征

为了让作用域函数更有趣,让我把他们的行为分类成三个属性特征。我将会使用这些属性特征来区分他们每一个函数。

1、普通函数 VS 扩展函数 (Normal vs. extension function)

如果我们对比 withT.run 这两个函数的话,他们实际上是十分相似的。下面使用他们实现相同的功能的例子.

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 参数(This vs. it argument)

如果我们对比 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.lenght}. 所以我把这个称之为传递 this参数

然而对于 T.let 函数的声明,你将会注意到 T.let 是传递它自己本身到函数中block: (T)。因此这个类似于传递一个lambda表达式作为参数。它可以在函数作用域内部使用it来指代. 所以我把这个称之为传递 it参数

从上面看,似乎T.runT.let更加优越,因为它更隐含,但是T.let函数具有一些微妙的优势,如下所示:

  • 1、T.let函数提供了一种更清晰的区分方式去使用给定的变量函数/成员与外部类函数/成员。
  • 2、例如当it作为函数的参数传递时,this不能被省略,并且it写起来比this更简洁,更清晰。
  • 3、T.let允许更好地命名已转换的已使用变量,即可以将it转换为其他有含义名称,而 T.run则不能,内部只能用this指代或者省略。
stringVariable?.let {
      nonNullString ->
      println("The non null string is $nonNullString")
}
复制代码

3、返回this VS 其他类型 (Return this vs. other type)

现在,让我们看看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类型本身,即这个。

这两个函数对于函数的链式调用都很有用,其中T.let让您演变操作,而T.also则让您对相同的变量执行操作。

简单的例子如下:

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似乎看上去没有意义,因为我们可以很容易地将它们组合成一个功能块。仔细思考,它有一些很好的优点。

  • 1、它可以对相同的对象提供非常清晰的分离过程,即创建更小的函数部分。
  • 2、在使用之前,它可以非常强大的进行自我操作,从而实现整个链式代码的构建操作。

当两者结合在一起使用时,即一个自身演变,一个自我保留,它能使一些操作变得更加强大。

// 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的三个属性如下

  • 1、它是一个扩展函数
  • 2、它是传递this作为参数
  • 3、它是返回 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个属性特征,我们现在可以对功能进行相应的分类。基于此,我们可以在下面构建一个决策树,以帮助确定我们想要使用哪个函数,来选择我们需要的。

[图片上传中...(image-2c25dd-1574061276700-1)]

<figcaption></figcaption>

[图片上传中...(image-f92133-1574061276700-0)]

<figcaption></figcaption>

希望上面的决策树能够更清晰地说明功能,并简化你的决策,使你能够适当掌握这些功能的使用.

译者有话说

  • 1、为什么我要翻译这篇博客?

我们都知道在Kotlin中Standard.Kt文件中短短不到100来行库函数源码,但是它们作用是非常强大,可以说它们是贯穿于整个Kotlin开发编码过程中。使用它们能让你的代码会更具有可读性、更优雅、更简洁。善于合理使用标准库函数,也是衡量你对Kotlin掌握程度标准之一,因为你去看一些开源Kotlin源码,随处可见的都是使用各种标准库函数。

但是这些库函数有难点在于它们的用法都非常相似,有的人甚至认为有的库函数都是多余的,其实不然,每个库函数都是有它的实际应用场景。虽然有时候你能用一种库函数也能实现相同的功能,但是也许那并不是最好的实现方式。相信很多初学者对于这些标准库函数也是傻傻分不清楚(曾经的我也是),但是这篇博客非常一点在于它提取出了这些库函数三个主要特征:是否是扩展函数、是否传递this或it做为参数(在函数内部表现就是this和it的指代)、是否需要返回调用者对象本身,基于特征就可以进行分类,分类后相应的应用场景也就一目了然。这种善于提取特征思路还是值得学习的。

  • 2、关于使用标准库函数需要补充的几点。

第一点: 建议尽量不要使用多个标准库函数进行嵌套,不要为了简化而去做简化,否则整个代码可读性会大大降低,一会是it指代,一会又是this指代,估计隔一段时间后连你自己都不知道指代什么了。

第二点: 针对上面译文的let函数和run函数需要补充下,他们之所以能够返回其他类型的值,其原理在于内部block lambda表达式返回的R类型,也就是这两者函数的返回值类型取决于传入block lambda表达式返回类型,然而决定block lambda表达式返回值类型,取决于外部传入lambda表达式体内最后一行返回值

第三点: 关于T.also和T.apply函数为什么都能返回自己本身,是因为在各自Lambda表达式内部最后一行都调用return this,返回它们自己本身,这个this能被指代调用者,是因为它们都是扩展函数特性

  • 3、总结

关于标准库函数本篇译文在于告知应用场景以及理清它们的区别以及在使用库函数简化代码实现时要掌握好度,不要滥用否则你的代码可读性会很差,后续会深入每个标准库函数内部原理做分析,欢迎关注。

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