Kotlin 标准函数 with, run, apply 与静态方法实现

Kotlin 标准函数:with、run 和 apply

Kotlin 中的标准函数是指定义在 Standard.kt 文件中的函数,它们会默认导入,让我们在任何地方中都可以随意调用所有的标准函数。

我们先来看看最常用且有助于简化代码的标准函数:withrunapply

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)
}

修改后的效果和之前也是完全一样的。可以看到,withrun 函数差别不大,看你喜欢。

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 后缀。在这里是 HelperKtdoSomething方法就被放在了这个类中,并且是以静态方法的形式存在的。

调用时,只需通过类名调用,像这样:

java 复制代码
HelperKt.doSomething();

如果你想修改这个生成的 Java 类名,只需在 Helper.kt 文件的开头,在 package 声明之前使用 @file:JvmName("Xxx") 注解就行了。

kotlin 复制代码
@file:JvmName("CustomHelperUtils")
package com.example
// 顶层方法
...

这样,在 Java 中就可以通过 CustomHelperUtils.doSomething() 来调用了。

相关推荐
移动开发者1号16 小时前
Fragment懒加载优化方案总结
android·kotlin
移动开发者1号17 小时前
Android Activity启动模式面试题
android·kotlin
alexhilton17 小时前
Jetpack Compose 中ViewModel的最佳实践
android·kotlin·android jetpack
猿小蔡-Cool20 小时前
Kotlin 中 Lambda 表达式的语法结构及简化推导
开发语言·windows·kotlin
wzj_what_why_how1 天前
Kotlin JVM 注解详解
android·kotlin
tangweiguo030519872 天前
Android全局网络监控最佳实践(Kotlin实现)
android·kotlin
移动开发者1号2 天前
Android后台服务保活方案对比分析
android·kotlin
移动开发者1号2 天前
ContentProvider URI匹配机制详解
android·kotlin
zhifanxu2 天前
android协程异步编程常用方法
android·开发语言·kotlin