Kotlin:作用域函数

Kotlin

前言

最近使用kotlin语言开发了新的项目,kotlin的一些特性和大量的语法糖相当好用,相比于java,开发效率高了不少。但Kotlin大量的语法糖也带来了一些问题:学习成本高,语法糖使用场景的困惑。
比如,当我第一次看到作用域函数就产生了这样的疑问:what is this?Which function to use?

于是我研究了一下什么是作用域函数,以及各个函数的区别和使用场景。

介绍

官方介绍:The Kotlin standard library contains several functions whose sole purpose is to execute a block of code within the context of an object. When you call such a function on an object with a lambda expression provided, it forms a temporary scope. In this scope, you can access the object without its name. Such functions are called scope functions. There are five of them: let, run, with, apply, and also.

翻译理解:作用域函数的目的是在对象的上下文中执行代码块,它为调用者对象提供了一个临时内部作用域,在这个作用域中可以不显式的访问该对象。这样的作用域函数有5个:let,run,with,apply,和also。

函数

run

run函数是最能体现作用域的用途的函数,如下使用示例:
在mian函数中使用run函数创建了一个单独的作用域,在该作用域中重新定义了一个word变量,两次打印使用的是各自作用域中的word变量,互不影响;并且,run函数返回了lambda结果。

使用示例

fun main(args: Array<String>) {
    var word = "我是小明"
    val returnValue = run {
        var word = "我是小红"
        println("run:$word")
        word
    }
    println("main:$word")
    println("returnValue:$returnValue")
}

运行结果:

run:我是小红
main:我是小明
returnValue:我是小红

with

with函数可以将任意对象作为上下文对象this传入,并且可以隐式的访问该对象,返回lambda结果。如下使用示例:在mian函数中使用with函数创建了一个临时作用域,在该作用域中可以重新定义person变量,两个person变量互无影响;并且可以使用this访问上下文对象,隐式修改person的age变量值。

使用示例

data class Person (
    var name: String,
    var age: Int = 0
)
fun main(args: Array<String>) {
    var person = Person("小明",25)
    val returnValue = with(person) {
        println("with:this=$this")
        var person = Person("小红",23)
        println("with:person=$person")
        age = 26
        person
    }
    println("main:person=$person")
    println("main:returnValue=$returnValue")
}

运行结果:

with:this=Person(name=小明, age=25)
with:person=Person(name=小红, age=23)
main:person=Person(name=小明, age=26)
main:returnValue=Person(name=小红, age=23)

T.run

T.run函数可以使用T作为作用域的上下文对象this,在作用域中可以隐式访问T对象,并返回lambda结果。

使用示例

data class Person (
    var name: String,
    var age: Int = 0
)
fun main(args: Array<String>) {
    var person: Person? = null
    // T?.run当T为null时不调用run函数
    person?.run {
        println("person?.run:person=$person")
    }
    person = Person("小明",25)
    val returnValue = person.run {
        println("person.run:this=$this")
        var person = Person("小红",23)
        println("person.run:person=$person")
        age = 26
        person
    }
    println("main:person=$person")
    println("main:returnValue=$returnValue")
}

运行结果:

person.run:this=Person(name=小明, age=25)
person.run:person=Person(name=小红, age=23)
main:person=Person(name=小明, age=26)
main:returnValue=Person(name=小红, age=23)

T.let

T.let函数与T.run函数唯一的区别是:T作为作用域上下文对象的名称不同,前者是it,后者是this,所以在T.let函数中必须显式使用it访问T对象。

使用示例

data class Person (
    var name: String,
    var age: Int = 0
)
fun main(args: Array<String>) {
    var person: Person? = null
    person?.let {
        println("person?.let:person=$person")
    }
    person = Person("小明",25)
    val returnValue = person.let {
        println("person.let:it=$it")
        var person = Person("小红",23)
        println("person.let:person=$person")
        it.age = 26
        person
    }
    println("main:person=$person")
    println("main:returnValue=$returnValue")
}

运行结果:

person.let:it=Person(name=小明, age=25)
person.let:person=Person(name=小红, age=23)
main:person=Person(name=小明, age=26)
main:returnValue=Person(name=小红, age=23)

T.also

如下使用示例,T.also函数和T.let函数的唯一区别是:前者返回值是this(即T),后者返回值是lambda结果。

使用示例

data class Person (
    var name: String,
    var age: Int = 0
)
fun main(args: Array<String>) {
    var person: Person? = null
    person?.also {
        println("person?.also:person=$person")
    }
    person = Person("小明",25)
    val returnValue = person.also {
        println("person.also:it=$it")
        var person = Person("小红",23)
        println("person.also:person=$person")
        it.age = 26
        person
    }
    println("main:person=$person")
    println("main:returnValue=$returnValue")
}

运行结果:

person.also:it=Person(name=小明, age=25)
person.also:person=Person(name=小红, age=23)
main:person=Person(name=小明, age=26)
main:returnValue=Person(name=小明, age=26)

T.apply

如下使用示例,T.apply函数和T.also函数的唯一的区别是:T作为作用域上下文对象的名称不同,前者是this,后者是it,所以在T.apply函数中可以隐式访问T对象。

使用示例

data class Person (
    var name: String,
    var age: Int = 0
)
fun main(args: Array<String>) {
    var person: Person? = null
    person?.apply {
        println("person?.apply:person=$person")
    }
    person = Person("小明",25)
    val returnValue = person.apply {
        println("person.apply:this=$this")
        var person = Person("小红",23)
        println("person.apply:person=$person")
        age = 26
        person
    }
    println("main:person=$person")
    println("main:returnValue=$returnValue")
}

运行结果:

person.apply:this=Person(name=小明, age=25)
person.apply:person=Person(name=小红, age=23)
main:person=Person(name=小明, age=26)
main:returnValue=Person(name=小明, age=26)

特殊的作用域函数

T.takeIf

以it作为在作用域上下文对象T的名称,若lambda结果为true,返回this;否则,返回null。

函数源码

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (predicate(this)) this else null
}

使用示例

fun main(args: Array<String>) {
    var count = 0
    while (count <= 10) {
        val returnValue = count.takeIf {
            count++ % 2 == 0
        }
        println(returnValue)
    }
}

运行结果:

0
null
2
null
4
null
6
null
8
null
10

T.takeUnless

以it作为在作用域上下文对象T的名称,若lambda结果为true,返回null;否则,返回this。与taskIf的实现相比,其实就是对lambda结果进行了取反操作。

函数源码

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (!predicate(this)) this else null
}

使用示例

fun main(args: Array<String>) {
    var count = 0
    while (count <= 10) {
        val returnValue = count.takeUnless {
            count++ % 2 == 0
        }
        println(returnValue)
    }
}

运行结果:

null
1
null
3
null
5
null
7
null
9
null

repeat

以当前执行的次数it作为在作用域上下文对象T的名称,执行给定lambda函数指定的次数。从函数源码和使用示例可以看出,执行次数角标是从0开始。

函数源码

@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
    contract { callsInPlace(action) }
    for (index in 0 until times) {
        action(index)
    }
}

使用示例

fun main(args: Array<String>) {
    repeat(5) {
        print("$it,")
    }
}

运行结果:

0,1,2,3,4,

总结

从上面的函数介绍和实际使用可以看出let,run,with,apply,和also,这些作用域函数的功能之间起着相互补充的作用,单独看某两个函数可能差别不大,但它们结合起来所实现的功能涵盖了绝大部分的使用场景。

总结一下,用于快速判断操作符使用场景,主要使用这几个因素辨别:

  1. 调用者

    • 正常函数:有run,with函数。主要作用是:开辟一个作用域,不受作用域之外上下文影响,with还可以方便地在作用域中访问上下文对象。
    • 扩展函数:可以使用T?.fun()在调用之前做空检查,如:null?.run { println("Kotlin") },作用域内容不会被执行。
  2. 上下文对象

    • this:方便在作用域中直接访问this
    • it:可以更清楚的区分作用域和非作用域中的成员
  3. 返回值

    • 上下文对象this:可以作为链式调用。
    • lambda表达式结果:返回表达式结果,可以将结果结合其他作用域函数,使用更灵活。
    // 示例:使用apply函数进行链式调用
    class Person {
        var name = ""
        var age = 0
    }
    fun main(args: Array<String>) {
        val person = Person().apply { name = "小明" }.apply { age = 25 }
        println("${person.name},${person.age}")
    }
    // 运行结果:小明,25
    

下面对作用域函数简要区分,可以更方便快速的辨别各函数的作用和使用场景。

作用域函数简要区分:

  • run:返回lambda结果
  • with:this上下文,返回lambda结果
  • T.run:支持空检查,this上下文,返回lambda结果
  • T.let:支持空检查,it上下文,返回lambda结果
  • T.also:支持空检查,it上下文,返回this(即T,it)
  • T.apply:支持空检查,this上下文,返回this(即T,this)

特殊的作用域函数区分:

  • T.takeIf:支持空检查,it上下文,函数体返回值类型Boolean,函数体返回true,函数返回this;否则返回null
  • T.takeUnless:支持空检查,it上下文,函数体返回值类型Boolean,函数体返回true,函数返回null;否则返回this
  • repeat:执行给定函数 action 指定的次数 times (角标:0-times)

参考资料

官方文档:https://www.kotlincn.net/docs/reference/scope-functions.html
medium Elye:https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84
CSDN george_zyf:https://blog.csdn.net/android_zyf/article/details/82496983

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