Kotlin 中的 let, with, run, apply, also 等函数的使用

前言

和严格古老的 Java 相比,Kotlin 中额外提供了不少高级语法特性。
这些高级特性中,定义于 Kotlin 的 Standard.kt
为我们提供了一些内置拓展函数以方便我们写出更优雅的代码。

相比大多数人都用过 let 函数来做过 Null Check,和 let 函数一样,with, run, apply, also 都可以提供非常强大的功能用以优化代码。

let

当需要定义一个变量在一个特定的作用域时,可以考虑使用 let 函数。当然,更多的是用于避免 Null 判断。

在 let 函数内部,用 it 指代调用 let 函数的对象,并且最后返回最后的计算值

一般结构

any.let {
    // 用 it 指代 any 对象
    // todo() 是 any 对象的共有属性或方法
    // it.todo() 的返回值作为 let 函数的返回值返回
    it.todo() 
}

// 另一种用法
any?.let {
    it.todo() // any 不为 null 时才会调用 let 函数
}

具体使用

fun main() {
  val result = "Test".let {
    println(it) // Test
    3 * 4 // result = 12
  }
  println(result) // 12
}

对应到实际使用场景一般是 需要对一个可能为 null 的对象多次做空判断:

textView?.text = "TextSetInTextView"
textView?.setTextColor(ContextCompat.getColor(this, R.color.colorAccent))
textView?.textSize = 18f

使用 let 函数优化后:

textView?.let { 
    it.text = "TextSetInTextView"
    it.setTextColor(ContextCompat.getColor(this, R.color.colorAccent))
    it.textSize = 18f
}

with

和 let 类似,又和 let 不同,with 最后也包含一段函数块,也是将最后的计算的结果返回。

但是 with 不是以拓展的形式存在的。其将某个对象作为函数的参数,并且以 this 指代。

首先来看 with 的一般结构:

一般结构

whith(any) {
  // todo() 是 any 对象的共有属性或方法
  // todo() 的返回值作为 with 函数的返回值返回
  todo() 
}

其实 with 函数的原始写法应该是:

with(any, {
  todo()
})

有用过 Groove DSL 的同学一定都知道在 Groovy 中,函数调用的最后一个参数是函数的话,函数的大括号可以提到圆括号() 的外面。

巧了,Kotlin DSL 也支持,所以最终就变成了一般结构中的那种写法了。

没错,Kotlin 也是支持 DSL 的,Android 使用 Gradle 进行编译,build.gradle 使用 Groovy 进行编写。

如果你对 Groovy 不太熟悉的话,也可以使用 Kotlin DSL 来写 build.gradle.kts

具体使用

class Person(val name: String, val age: Int)

fun main() {
    val chengww = Person("chengww", 18)
    val result = with(chengww) {
        println("Greetings. My name is $name, I am $age years old.")
        3 * 4 // result = 12
    }
    println(result)
}

在 let 函数的实际使用中,我们对 textView 进行空判断,但是每次函数调用的时候还是要使用 it 对象去调用。

如果我们使用 with 函数的话,由于代码块中传入的是 this,而不是 it,那么我们就可以直接写出函数名(属性)来进行相应的设置:

if (textView == null) return
with(textView) {
    text = "TextSetInTextView"
    setTextColor(ContextCompat.getColor(this@TestActivity, R.color.colorAccent))
    textSize = 18f
}

这段代码唯一的缺点就是要事先判空了,有没有既能像 let 那样能优雅的判空,又能写出这样的便利的代码呢?

别着急,咱们接着往下看。

run

刚刚说到,我们想能有 let 函数那样又优雅的判空,又能有 with 函数省去同一个对象多次设置属性的便捷写法。

没错,就是这就非我们 run 函数莫属了。run 函数基本是 let 和 with 的结合体,对象调用 run 函数,接收一个 lambda 函数为参数,传入 this 并以闭包形式返回,返回值是最后的计算结果。

一般结构

any.run {
  // todo() 是 any 对象的共有属性或方法
  // todo() 的返回值作为 run 函数的返回值返回
  todo() 
}

那么上面 TextView 设置各种属性的优化写法就是这样的:

textView?.run {
    text = "TextSetInTextView"
    setTextColor(ContextCompat.getColor(this@TestActivity, R.color.colorAccent))
    textSize = 18f
}

像上面这个例子,在需要多次设置属性,但设置属性后返回值不是改对象(或无返回值:Unit)不能链式调用的时候,就非常适合使用 run 函数。

apply

apply 函数和 run 函数很像,但是 apply 最后返回的是调用对象自身。

一般结构

val result = any.apply {
  // todo() 是 any 对象的共有属性或方法
  todo() 
  3 * 4 // 最后返回的是 any 对象,而不是 12
}

println(result) // 打印的是 any 对象

由于 apply 函数返回的是调用对象自身,我们可以借助 apply 函数的特性进行多级判空。

具体使用

在 Java 中多级判空一直是老大难的问题:

下面是一个 School 类中包含内部类 Class,在 Class 又包含内部类 Student,我们想获取该 Student 的 name 属性的示例。

这其中包含对 className 的修改操作。

public class Main {
    public static void main(String[] args) {
        School school = init();
        // To change the className of the a student and get his(her) name in this school what we should do in Java
        if (school != null && school.mClass != null) {
            school.mClass.className = "Class 1";
            System.out.println("Class name has been changed as Class 1.");
            if (school.mClass.student != null) {
                System.out.println("The student's name is " + school.mClass.student.name);
            }
        }
    }

    static School init() {
        School school = new School();
        school.mClass = new School.Class();
        school.mClass.student = new School.Class.Student();
        school.mClass.student.name = "chengww";
        return school;
    }

    static class School {
        Class mClass;
        private static class Class {
            String className;
            Student student;
            private static class Student {
                String name;
            }
        }
    }
}

实际情况中可能会有更多的判空层级,如果我们用 Kotlin 的 apply 函数来操作又会是怎么样呢?

fun main() {
    val school = init()
    school?.mClass?.apply {
        className = "Class 1"
        println("Class name has been changed as Class 1.")
    }?.student?.name?.also { println("The student's name is $it.") }
}

fun init(): School = School(School.Class(School.Class.Student("chengww")))


class School(var mClass: Class? = null) {
    class Class(var student: Student? = null, var className: String? = null) {
        class Student(var name: String? = null)
    }
}

also

有没有注意到上面的示例中,我们最后打印该学生的名字的时候,调用了 also 函数。

没错,和 let 函数类似,唯一的区别就是 also 函数的返回值是调用对象本身,在上例中 also 函数将返回 school.mClass.student.name

一般结构

val result = any.also {
    // 用 it 指代 any 对象
    // todo() 是 any 对象的共有属性或方法
    it.todo() 
    3 * 4 // 将返回 any 对象,而不是 12
}

总结

函数定义见下表:

函数名 实现
let public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
with public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
run public inline fun <T, R> T.run(block: T.() -> R): R = block()
apply public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
also public inline fun T.also(block: (T) -> Unit): T { block(this); return this }

具体的调用情况见下图:


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