admin 管理员组

文章数量: 887021

秒懂Kotlin之轻松掌握Scope Functions (apply, also,let,run,with)

[版权申明] 非商业目的注明出处可自由转载
博文地址:
出自:shusheng007

文章首发于个人博客

文章目录

  • 概述
  • 定义
  • 使用方法
    • 合适才是最好的
  • 分类
    • 相同之处
    • 不同之处
      • 作用域中的上下文对象不同
      • 返回值不同
  • 使用场景
  • 总结

概述

Kotlin和Java相比提供了很多语法糖,其目的当然是为了提高程序员的编码效率,但是其中一些过于灵活导致增加了其正确使用的难度,其中 Scope Functions 就属于这一类。曾几何时,面对 applyalsoletrun还有with是不是傻傻分不清?不要羞愧,因为你不是一个人!(千万不要问:那我是什么…? 如果你非要这么问,我想大概是一只程序吧)

定义

Scope Functions 是指下表中列出的5个函数,那他们为什么叫scope functions呢?

名称定义
applypublic inline fun T.apply(block: T.() -> Unit): T
alsopublic inline fun T.also(block: (T) -> Unit): T
letpublic inline fun <T, R> T.let(block: (T) -> R): R
runpublic inline fun <T, R> T.run(block: T.() -> R): R
withpublic inline fun <T, R> with(receiver: T, block: T.() -> R): R

以上5个函数均提供了一种能力,是什么呢?就是他们都会构建一个临时的作用域(scope),在此作用域里我们可以不通过对象的名称而访问此对象,所以他们都叫scope函数。

让我们实际来体会一下。需求:构建一个Student对象,然后设置其姓名和年龄

Java实现:

Student student = new Student();
student.setName("ShuSehng007");
student.setAge(18);

Kotlin实现:

 val student: Student = Student().apply {name = "ShuSheng007"age = 18}

在Java中要访问student对象,需要使用其名称student。但是当使用Kotlin的apply函数后,其构建了一个临时的作用域,在此作用域内访问student对象就不需要其名称了,就像上面{}内的代码展示的一样。不可否认其在代码在逻辑上变得确实更紧凑了。

使用方法

关于他们的使用方法,网上有小伙伴总结了一副图,很是全面,在此致敬一下。

虽然此图对Scope Functions的使用场景总结的很到位,但是我仍然坚信理解其背后的原理才是优秀程序员碾压菜鸟程序员的不二法宝。

Scope Functions 之所以让人迷惑,就是因为他们之间太像了,不仅长得像,功能上也像,同一个需求使用他们任何一个都可以完成。这就和你问你媳妇今晚想吃什么的时候,她漫不经心的回答到:都行,随便!是一样一样的。

这不王二狗刚刚问牛翠花:翠花,你看这个需求使用哪个scope函数实现啊? 翠花悠悠地说道:都行,随便!

你是二狗你咋办,关键是人家翠花说的对,不信请看下面的代码,打印出"ss007"这个字符串的长度:

fun runScopeFunctions(){val str="ss007"val size1: Int = str.apply {println(this)}.lengthval size2: Int = str.also {println(it)}.lengthval size3: Int= str.let {println(it)it.length}val size4: Int= str.run {println(this)this.length}val size5: Int= with(str){println(this)this.length}val size6: Int = run {println(str)str}.length
}

人家翠花不仅用5个scope函数证明了自己,顺便还送了你一个run的非scope函数用法,这小娘子是要上天啊,必须的治一治了。

合适才是最好的

那是不是真的就意味着这些scope functions可以不加区分的互用了呢?当然不是,不然要5个干啥,一个就足够了!以后你媳妇再说:都行,随便。你就说:好的,晚上给你做《冷水拌米糠》。

你看不管是山珍海味,还是冷水拌米糠都能吃饱,但是你想吃冷水拌米糠吗?就是这个道理,我们都是有追求的人,所以我们要使用最合适的方法,写出最优雅的code。。。

有的小朋友可能不耐烦了:行了,别TM嘚嘚啦,快点上干货吧!小朋友稍安勿躁,俺小时候的愿望是当小说家,长大后不幸做了程序员,不幸中的万幸是程序员可以写博客… 呀,再哔哔下去确实有点跑题,那就让我们开始接着上干货吧。

分类

对于相似的事物,掌握它们的最好办法就是按照他们之间的异同进行分类,求同存异,此处也不例外。

相同之处

  1. 均为内联(Inline)函数
    意味着:无性能损失
  2. 除了with以外,其余4个均只有一个函数类型的入参
    意味着:可以使用如下语法 T.xxx{ }
    "ss007".also{println(it)
    }	
    
  3. 除了with 其他4个均为泛型扩展函数

不同之处

从scope functions的定义上可以看出,这5个函数的区别主要表现在两个维度上。

  • 作用域中的上下文对象不同
  • 函数返回值不同

其中with的定义比较特殊,它是唯一一个不是扩展函数的Scope Function,但是区别仅仅是将receiver当参数传入。如下代码,runreceiver写在外边,而withreceiver写在里面,除此之外完全相同。

    val size4: Int= str.run {println(this)this.length}val size5: Int= with(str){println(this)this.length}

作用域中的上下文对象不同

  • 需要使用it访问上下文对象
public inline fun <T, R> T.let(block: (T) -> R): R
public inline fun <T> T.also(block: (T) -> Unit): T 

从上面的定义可以发现:letalso 入参类型为(T) -> R(T) -> Unit,即可以传入一个带一个类型为T的参数的 Lambda表达式,而单参数在Lambda表达式中隐含使用it表示,如下代码所示

str.also {println(it)}
str.let {println(it)
}
  • 需要使用this访问上下文对象
public inline fun <T> T.apply(block: T.() -> Unit): T 
public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T, R> with(receiver: T, block: T.() -> R): R

从上面的定义可以看出,applyrunwith最后一个 入参类型为T.() -> UnitT.() -> R

看到T.()是不是又懵逼了?这玩意叫 “Kotlin Function Literals with Receiver” ,是不是还有点懵逼?让我们将概念化繁为简,其直接翻译为:带接收器函数字面量。由于Lambda是一种函数字面量,所以其可以进一步具体化为:带接收器的Lambda表达式。

例如有如下Lambda

val greet1:()->Unit= { println("hello world")}

我们可以为上面的lambda加上一个String类型的receiver,使其变成下面这样

var greet2: String.() -> Unit = { println("Hello $this") }

我们可以在{}中以this访问这个receiver。值得注意的是,greet2有两种等价的执行方法

greet2("world")
"world".greet2()

返回值不同

  • 返回receiver
public inline fun <T> T.apply(block: T.() -> Unit): T 
public inline fun <T> T.also(block: (T) -> Unit): T 

可见返回类型为T,与receiver的类型相同

  • 返回Lambda表达式的值
public inline fun <T, R> T.let(block: (T) -> R): R
public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T, R> with(receiver: T, block: T.() -> R): R

可见返回类型为R,与传入的lambda的类型相同

使用场景

以上就是scope函数的异同,所以说要使用哪个完全要根据你的需求和偏好决定,所以才是最难的。Java为什么如此适合长期的大型项目,就是因为它死板,或者说严谨,谁来了都得这么写,这就达到了后来人可以轻松看得懂的效果,维护扩展起来才相对容易,才更有可能达到巨大的规模。

下面是官方列出的一些使用场景:

  • let

在一个非null对象上执行lambda表达式

obj?.let{ }

声明一个局部表达式变量

val v= obj?.let{ }
  • apply

设置对象

val student: Student = Student().apply {name = "ShuSheng007"age = 18}
  • run

设置对象并计算其结果

val studentInfo: String = Student().run {name = "ShuSheng007"age = 18        "My name is $name and I amm $age "}

构建一个可执行的表达式块

val runBlock= run {"ss007"
}
for (c in runBlock.chars()){println(c)
}

其和Lambda函数字面量还是有一点区别的,其可以直接执行,不需要使用()调用语法。下面是Lambda版本,注意看起执行的话需要lambdaBlock()语法,后面有()

val lambdaBlock :()->String = {"ss007"
}for (c in lambdaBlock().chars()){println(c)
}
  • also

追加行为,例如打印日志

val student = Student().also {println("Student对象成功创建了:${it.toString()}")}
  • with

将在同一个对象上的方法调用group起来

 with(mutableListOf("shu", "sheng")) {add("007")println(fold("我们都爱") { result, element -> result + element })}

输出结果

我们都爱shusheng007

根据返回类型及上下文对象的访问方式谨慎选择最合适的scope函数,一切以提高可读性为准,切记不可滥用!

总结

Kotlin 就像个刚进门的多才多艺的小妾,使用各种奇技淫巧把老爷伺候的欲仙欲死,还一直挑衅正房Java。

老爷: Java姐姐会的妾身都会,Java姐姐不会的,妾身也会,老爷你看这招:协程,爽不爽啊?你要是觉得一时不习惯没有java姐姐的日子,妾身可以同时和Java姐姐一起伺候你,我们是100%互通的,可以和谐共处…

不知道你们怎么想,我反正是想试试kotlin,建议你也试一试。。。又到了点赞的时候了,抬起陪伴你多年的右手,对准那个大拇指猛戳一下即可

自古逢秋悲寂寥,我言秋日胜春朝。——刘禹锡《秋词》

本文标签: 秒懂Kotlin之轻松掌握Scope Functions (apply also let Run with)