Kotlin修炼指南

Kotlin修炼指南

作用域函数

作用域函数是Kotlin中的一个非常有用的函数,它主要分为两种,一种是拓展函数式,另一种是顶层函数式。作用域函数的主要功能是为调用函数提供一个内部范围,同时结合kotlin的语法糖提供一些便捷操作。

作用域函数主要有下面这几种,它们的主要区别就是函数体内使用对象和返回值的区别。

  • run

函数体内使用this代替本对象。返回值为函数最后一行或者return指定的表达式

  • let

函数内使用it代替本对象。返回值为函数最后一行或者return指定的表达式。

  • apply

函数内使用this代替本对象。返回值为本对象。

  • also

函数内使用it代替本对象。返回值为本对象。

  • takeIf

条件为真返回对象本身否则返回null。

  • takeUnless

条件为真返回null否则返回对象本身。

  • with

with比较特殊,不是以扩展方法的形式存在的,而是一个顶级函数。
传入参数为对象,函数内使用this代替对象。
返回值为函数最后一行或者return指定的表达式。

  • repeat

将函数体执行多次。

通过表格进行下总结,如下所示。

操作符 this/it 返回值
let it 最后一行或者return指定的表达式
with it 最后一行或者return指定的表达式
run this 最后一行或者return指定的表达式
also this 上下文对象
apply this 上下文对象

下面通过一个简单的例子来演示下这些作用域函数的基本使用方式。

class TestBean {
    var name: String = "xuyisheng"
    var age: Int = 18
}
fun main(args: Array<String>) {
    val test = TestBean()
    val resultRun = test.run {
        name = "xys"
        age = 3
        println("Run内部 $this")
        age
    }
    println("run返回值 $resultRun")
    val resultLet = test.let {
        it.name = "xys"
        it.age = 3
        println("let内部 $it")
        it.age
    }
    println("let返回值 $resultLet")
    val resultApply = test.apply {
        name = "xys"
        age = 3
        println("apply内部 $this")
        age
    }
    println("apply返回值 $resultApply")
    val resultAlso = test.also {
        it.name = "xys"
        it.age = 3
        println("also内部 $it")
        it.age
    }
    println("also返回值 $resultAlso")
    val resultWith = with(test) {
        name = "xys"
        age = 3
        println("with内部 $this")
        age
    }
    println("with返回值 $resultWith")
    test.age = 33
    val resultTakeIf = test.takeIf {
        it.age > 3
    }
    println("takeIf $resultTakeIf")
    val resultTakeUnless = test.takeUnless {
        it.age > 3
    }
    println("takeUnless $resultTakeUnless")
}

执行结果如下所示。

Run内部 TestBean@27c170f0
run返回值 3
let内部 TestBean@27c170f0
let返回值 3
apply内部 TestBean@27c170f0
apply返回值 TestBean@27c170f0
also内部 TestBean@27c170f0
also返回值 TestBean@27c170f0
with内部 TestBean@27c170f0
with返回值 3
takeIf TestBean@27c170f0
takeUnless null

官网提供了一张图来帮助开发者选择合适的作用域函数,如下所示。

file

顶级函数使用场景

run、with、repeat,是比较常用的3个顶级函数,它们是区别于其它几种拓展函数类型的,它们的使用也比较简单,示例代码如下所示。

  • run
fun testRun() {
    var str = "I am xys"
    run {
        val str = "I am zj"
        println(str) // I am xys
    }
    println(str)  // I am zj
}

可以发现,run顶级函数提供了一个独立的作用域,可以在该作用域内完整的使用全新的变量和属性。

  • repeat
repeat(5){
    print("repeat")
}

repeat比较简单,直接将函数体按指定次数执行。

  • with

前面的代码已经演示过with如何使用。

with(ArrayList<String>()) {
    add("a")
    add("b")
    add("c")
    println("this = " + this)
    this
}

要注意的是其返回值是根据return的类型或者最后一行代码来进行判断的。

拓展函数使用场景

?.结合拓展函数

Kotlin的?操作符和作用域函数的拓展函数可以非常方便的进行对象的判空及后续处理,例如下面的例子。

// 对result进行了判空并bindData
result?.let {
    if (it.isNotEmpty()) {
        bindData(it)
    }
}

简化对象的创建

类似apply这样的作用域函数,可以返回this的作用域函数,可以将对象的创建和属性的赋值写在一起,简化代码,类似builder模式,例如下面的这个例子。

// 使用普通的方法创建一个Fragment
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}
// 通过apply来创建一个Fragment
fun createInstance(args: Bundle)
    = MyFragment().apply { arguments = args }

再例如下面的实现。

// 使用普通的方法创建Intent
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data = Uri.parse(intentData)
    return intent
}
 
// 通过apply函数的链式调用创建Intent
fun createIntent(intentData: String, intentAction: String) =
    Intent().apply { action = intentAction }
    .apply { data = Uri.parse(intentData) }

以及下面的实现。

// 正常方法
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}
// 改进方法
fun makeDir(path: String) 
    = path.let{ File(it) }.also{ it.mkdirs() }

同一对象的多次操作

在开发中,有些对象有很多参数或者方法需要设置,但该对象又没用提供builder方式进行构建,例如下面的例子。

val linearLayout = LinearLayout(itemView.context).apply {
    orientation = LinearLayout.VERTICAL
    layoutParams = LinearLayout.LayoutParams(
            LinearLayout.LayoutParams.MATCH_PARENT,
            LinearLayout.LayoutParams.WRAP_CONTENT)
}

progressBar.apply {
    progress = newProgress
    visibility = if (newProgress in 1..99) View.VISIBLE else View.GONE
}

不论是let、run、apply还是其它拓展函数,都可以实现这样的需求,借助it或this,可以很方便的对该对象的多个属性进行操作。

不过这些拓展函数还是有一些细微的差别的,例如T.run和T.let(即使用it和this的区别)

  • 使用it的作用域函数,可以使用特定的变量名来重命名it,从而表达更清楚的语义。
  • this在大部分情况下是可以省略的,比使用it简单

例如下面的例子。

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

通过对it的重命名,语义表达更加清楚。

条件操作

借助kotlin的?操作符,可以简化很多条件操作,例如下面的几个例子。

url = intent.getStringExtra(EXTRA_URL)?.takeIf { it.isNotEmpty() } ?: run {
    toast("url空")
    activity.finish()
}

上面的代码演示了【从intent中取出url并在url为空时的操作】。

test.takeIf { it.name.isNotEmpty() }?.also { print("name is $it.name") } ?: print("name empty")

上面代码演示了【从test中取出name,不为空的时候和为空的时候的操作】。

链式调用

作用域函数的一个非常方便的作用就是通过其返回值的改变来组装链式调用。一个简单示例如下所示。

test.also {
    // todo something
}.apply {
    // todo something
}.name = "xys"

通过let来改变返回值,从而将不同的处理通过链式调用串联起来。

val original = "abc"
// 改变值并且传递到下一链条
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // 改变参数并且传递到下一链条
}.let {
    println("The reverse String is $it") // "cba"
    it.length   // 改变参数类型并且传递到下一链条
}.let {
    println("The length of the String is $it") // 3
}

上面的代码借助let,可以将函数的返回值不断进行修改,从而直接将下一个操作进行链式连接。
而使用also(即返回this的作用域函数)可以将多个对同一对象的操作进行链式调用,如下所示。

original.also {
    println("The original String is $it") // "abc"
    it.reversed() // 即使我们改变它,也是没用的
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // 即使我们改变它,也是没用的
}.also {
    println("The length of the String is ${it}") // "abc"
}

这里只是为了演示,所以将可以写在同一个作用域函数中的进行了拆分。

also和let的链式调用,实际上各有不同的使用技巧,通过let,可以改变返回值,而通过also,可以将多个不同的原子操作通过链式进行组合,让逻辑更加明朗。

国际惯例

also & apply

虽然also和apply都是返回this,但国际惯例,它们在使用的时候,还是有一些细微的差别的,also强调的是【与调用者无关的操作】,而apply强调的是【调用者的相关操作】,例如下面的这个例子。

test?.also {
    println("some log")
}?.apply {
    name = "xys"
}

let & run

let和run的返回值相同,它们的区别主要在于作用域内使用it和this的区别。一般来说,如果调用者的属性和类中的属性同名,则一般会使用let,避免出现同名的赋值引起混乱。

国际惯例,run通常使用在链式调用中,进行数据处理、类型转换,例如?.run{}的使用。

欢迎大家关注我的微信公众号——Android群英传

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

推荐阅读更多精彩内容

  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,732评论 2 9
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,738评论 0 38
  • 第一章 错误处理: 错误: 程序运行过程中,导致程序无法正常执行的现象(即bug) 现象: 程序一旦出错,默认会报...
    fastwe阅读 1,093评论 0 1
  • 前言 最近使用kotlin语言开发了新的项目,kotlin的一些特性和大量的语法糖相当好用,相比于java,开发效...
    SirWwh阅读 2,344评论 1 2
  • 什么是作用域函数(Scope Functions)? Kotlin 标准库包含了几个特殊的函数,其目的是在调用对象...
    SkyRiN阅读 912评论 1 4