Kotlin 标准函数:with、run 和 apply
Kotlin 中的标准函数是指定义在 Standard.kt
文件中的函数,它们会默认导入,让我们在任何地方中都可以随意调用所有的标准函数。
我们先来看看最常用且有助于简化代码的标准函数:with
、run
和 apply
。
with 函数
with
函数接收两个参数,第一个参数 receiver
可以接收一个任意类型的对象,第二个参数 block
则是一个 Lambda 表达式。
kotlin
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
...
}
with
函数会给 Lambda 表达式(block
)提供 receiver
对象的上下文(this
),在 Lambda 表达式中,你可以直接调用 receiver
对象的公有成员和方法,无需显式使用对象名。with
函数会将 Lambda 表达式中最后一个表达式的结果作为返回值返回。
比如:
kotlin
val result = with(obj) {
// 这里是obj的上下文,可以直接调用obj的方法和属性
this.someMethod() // 'this'可以省略
anotherMethod()
"value" // "value" 将作为 with 函数的返回值
}
那它有什么用呢?它主要用于在连续调用同一对象的多个方法或属性时,让代码更加精简,无需重复写对象名。
我们来看一个具体的例子。比如有一个名字列表,我们要让它以一定地格式拼接成一个字符串,并打印出来。之前我们可能会这样写:
kotlin
fun main() {
val list = listOf("Sally", "Jack", "Martin", "Tommy", "Asher", "Alan")
val builder = StringBuilder()
builder.append("All People:\r\n")
for (people in list){
builder.append(people).append("\r\n")
}
builder.append("There are ${list.size} people in total.")
val result = builder.toString()
println(result)
}
上述代码中,我们多次调用了 builder
对象的 append()
方法来完成字符串的多次拼接。
运行结果:
但你会发现,我们连续调用了多次 builder
对象的 append()
方法,并且还调用了该对象的 toString()
方法以获取最终结果。这时,其实我们就可以考虑使用 with
函数来简化代码,像这样:
kotlin
fun main() {
val list: List<String> = listOf("Sally", "Jack", "Martin", "Tommy", "Asher", "Alan")
val builder = StringBuilder()
val result = with(builder) {
append("All People:\r\n")
for (people in list) {
append(people)
append("\r\n")
}
append("There are ${list.size} people in total.")
toString() // 拼接好的字符串作为 with 函数的返回值
}
println(result)
}
我们在 with
函数的尾 Lambda 表达式中,我们拥有通过 receiver
参数传入的 builder
对象的上下文(this
)。所以我们可以直接调用该对象中的 append()
和 toString()
方法,使代码更简洁。这两段代码的执行结果是完全一样的,只是第二段代码更加简洁。
run 函数 (作为扩展函数)
run
函数其实和 with
函数非常相似,只是语法上稍微有些不同,准确来说是调用方式不同,并且它本身是一个扩展函数。
首先 run
函数作为扩展函数,不能直接调用,而是要在某个对象上调用。然后 run
函数只接收一个 Lambda 表达式作为参数,该 Lambda 表达式中也拥有调用对象的上下文(this
),并且 run
函数会把 Lambda 表达式中最后一个表达式的结果返回。
kotlin
val result = obj.run {
// 这里是 obj 的上下文 (this)
this.someMethod() // 'this' 可以省略
anotherMethod()
"value" // "value" 将作为 run 函数的返回值
}
我们现在来使用 run
函数修改一下之前的 StringBuilder
示例:
kotlin
fun main() {
val list: List<String> = listOf("Sally", "Jack", "Martin", "Tommy", "Asher", "Alan")
val builder = StringBuilder()
val result = builder.run { // 在 builder 对象上调用 run
append("All People:\n")
for (people in list) {
append(people)
append("\n")
}
append("There are ${list.size} people in total.")
toString() // Lambda的最后一个表达式,作为 run 函数的返回值
}
println(result)
}
修改后的效果和之前也是完全一样的。可以看到,with
和 run
函数差别不大,看你喜欢。
但 run
可用于可空对象的链式调用中,比如:
kotlin
val nullableObject:String? = null
nullableObject?.run {
}
注意:还有一个非扩展函数的 run
函数,它只会执行一个代码块,并返回最后一个表达式的结果,主要用于将多条语句包装成一条表达式。比如:
kotlin
fun main() {
// 随机获取一个用户角色
val userRoles = listOf("admin", "user", "guest")
val userRole = userRoles[userRoles.indices.random()]
// 使用非扩展函数 run
val welcomeMessage = run {
val prefix = "Welcome, "
val detailedRoleDescription = when (userRole) {
"admin" -> "Administrator!"
"user" -> "Valued User."
else -> "Guest."
}
// Lambda 表达式的最后一行是返回值
"$prefix$detailedRoleDescription"
}
println(welcomeMessage)
}
在上述代码中,我们只需 welcomeMessage
的值,但计算这个值需要好几步操作,而 run
就可以把这些操作包装成一个表达式来使用。并且这些临时操作的变量还不会污染到当前的作用域。
apply 函数
apply
函数也和 run
函数类似:也是需要在某个对象上才能调用,也只接收一个 Lambda 参数,并且在 Lambda 表达式中也拥有调用对象的上下文(this
)。只不过 apply
函数无法指定返回值,不管 Lambda 表达式中最后一行代码是什么,都只会自动返回调用对象(receiver
)本身。
kotlin
val originalObj = obj.apply {
// 这里是 obj 的上下文 (this)
this.property = "new value"
someMethod()
// 即使这里有其他表达式,apply 依然返回 obj
}
// originalObj == obj 结果为 true
我们再来使用 apply
函数修改之前的代码:
kotlin
fun main() {
val list: List<String> = listOf("Sally", "Jack", "Martin", "Tommy", "Asher", "Alan")
// apply 通常用于对象初始化和配置
val builderInstance = StringBuilder().apply {
append("All People:\n")
for (people in list) {
append(people)
append("\n")
}
append("There are ${list.size} people in total.")
}
val result = builderInstance.toString() // 获取字符串结果
println(result)
}
注意:这里因为 apply
函数只能返回 StringBuilder
对象本身,所以我们在 apply
执行完毕后,再调用它的 toString()
方法获取最终的字符串结果。apply
非常适合对象的初始化场景。
在 Kotlin 中定义静态方法
在 Java 中定义一个静态方法非常简单,只需在方法声明前加上 static
关键字即可:
java
public class Util {
public static void doAction() {
System.out.println("do action");
}
}
上述代码中,定义了一个 doAction()
静态方法,调用该静态方法只需 Util.doAction()
,无需创建类的实例。所以静态方法特别适合作为工具类的方法,因为工具类是全局通用的,希望无需创建实例也能使用。
在 Kotlin 中定义静态方法似乎成了一件难事,因为 Kotlin 中并没有 static
关键字。但请别担心,这不代表我们就无法实现类似的功能了,Kotlin 有更好用的语法特性来实现这类需求。
单例类 (object)
如果一个类中的所有方法都希望像静态方法那样被调用(如工具类的方法),这就非常适合使用单例类 (object declaration) 来实现。比如上述的示例可以改为:
kotlin
object Util {
fun doAction() {
println("do action")
}
}
虽然 doAction
并不是名义上的静态方法,它只是 Util
单例对象的一个实例方法。但我们仍然可以通过 Util.doAction()
来调用它,效果上和 Java 的静态方法调用类似。在 Java 中要调用该方法,需要通过 Util.INSTANCE.doAction()
完成。
伴生对象 (companion object)
不过使用单例类的话,该类中所有的方法都会变为类似于静态方法的方式调用,并且该类不能实例化了。
如果只想让类中的部分方法能以类似静态方法的方式调用,并且可以创建这个类的对象,可以使用伴生对象 (companion object
) :
kotlin
class Util {
fun instanceAction() { // 实例方法
println("do action")
}
companion object{ // 伴生对象
fun doAction(){
println("do action from companion object")
}
}
}
在上述代码中,Util
是一个普通的类,类中定义了一个普通的 instanceAction
实例方法,它需要创建 Util
实例才能调用。又在 companion object
中定义了一个 doAction
方法,你可以像调用静态方法一样调用它。
但 doAction
也不是真正意义上的静态方法,companion object
关键字实际上会在 Util
类内部创建一个名称为 Companion
私有静态内部类实例,而 doAction
方法是定义在这个实例中的。Kotlin 会保证 Util
类中始终只会有一个伴生对象实例,所以调用 Util.doAction()
时,就是在调用 Util
类中那个唯一的伴生对象的 doAction()
方法。
如果要在 Java 中调用 doAction
方法,需要这样:Util.Companion.doAction()
。
实现真正的静态方法
可以看出,Kotlin 确实是没有直接定义静态方法的关键字,只是提供了一些语法特性来支持类似静态方法调用的写法。单例类的方法和伴生对象的方法虽然都不是真正的JVM静态方法,不能在 Java 代码中以静态方法的形式去调用它们(提示方法不存在),但基本满足日常需求。
然而你确实想要创建一个真正的静态方法,你还是能够完成的。
Kotlin仍然提供了两种实现方式:
1.注解
2.顶层方法
@JvmStatic 注解
先来看注解,只需给单例类或是伴生对象中的方法(或属性)加上 @JvmStatic
注解,那么 Kotlin 编译器就会将这些成员编译成真正的JVM静态方法(和静态字段)。
比如:
kotlin
object UtilWithObject {
@JvmStatic
fun doAction() {
println("do action from object with @JvmStatic")
}
}
class UtilWithCompanionObject {
fun instanceAction() {
println("instance action")
}
companion object {
@JvmStatic
fun doAction() {
println("do action from companion object with @JvmStatic")
}
}
}
注意,该注解不能加到普通类的实例方法(或属性)上,会报错:Only members in named objects and companion objects can be annotated with '@JvmStatic'
由于 doAction()
方法已经成为了真正的静态方法了,那么无论是在 Kotlin 还是 Java 中,都可以通过类名直接调用。
顶层方法
再来看看顶层方法,什么是顶层方法?
就是没有定义在任何类中的方法,比如 main
方法,又或者是之前的标准函数。Kotlin 编译器会将所有的顶层方法编译成 JVM 上的静态方法。
要想定义一个顶层方法,我们先来创建一个 Kotlin 文件(后缀是.kt),比如创建一个 Helper.kt 文件。创建好后,我们在这个文件中书写的任何方法都是顶层方法,比如定义一个 doSomething()
方法:
在 Kotlin 中,你可以直接在任意地方调用,像这样:
kotlin
doSomething()
而在 Java 中,没有顶层方法这个概念,所有的方法必须定义在类中,那么这个 doSomething
方法被放到了哪?
我们刚刚创建的文件是 Helper.kt,Kotlin 编译器会自动创建一个 Java 类,类名默认是文件名加上 Kt
后缀。在这里是 HelperKt
,doSomething
方法就被放在了这个类中,并且是以静态方法的形式存在的。
调用时,只需通过类名调用,像这样:
java
HelperKt.doSomething();
如果你想修改这个生成的 Java 类名,只需在 Helper.kt
文件的开头,在 package 声明之前使用 @file:JvmName("Xxx")
注解就行了。
kotlin
@file:JvmName("CustomHelperUtils")
package com.example
// 顶层方法
...
这样,在 Java 中就可以通过 CustomHelperUtils.doSomething()
来调用了。