天天看点

《Kotlin技巧:作用域函数let、run、with、apply和also》

作者:不秃头程序员
《Kotlin技巧:作用域函数let、run、with、apply和also》

关于Kotlin的作用域函数:let、run、with、apply和also。它们有一个共同点:它们都在对象上调用,在这些函数的作用域内,可以访问对象本身,而无需其名称。

使用let作用域函数转换对象并返回结果

首个提示关于let作用域函数,当你想对一个对象执行转换并分配结果时,它特别有用:

fun main() {
    val name = "   Kotlin  "
    val cleanName = name.let { 
      val trimmed = it.trim()
      val reversed = trimmed.reversed() 
      reversed
    }
    println(cleanName) // This will print "niltoK"
}           

let块内的操作被限定在该块内,并不影响调用它的对象。在上面的例子中,你可以看到对name的操作并未影响变量本身。相反,我们将转换的结果存储在cleanName变量中。

let函数也可用来确保只有在调用它的变量不为空时才执行一段代码:

fun main() {
    val name: String? = "Kotlin"
    name?.let {
        println("The name has ${it.length} characters.")
    }
}           

然而,尽管这是一个常见的用例,但有人误传这就是let的实际目的。实际上,let这个词来源于数学逻辑和集合理论,在那里它被用来引入一个新的变量或常量。

举例来说,在数学中,你可能会说"设a满足x²=a",这引入了a作为一个新的变量并为其定义了条件。

fun main() {
    val x = 3

    // Let a be such that x²=a
    val a = x.let { x ->
        x * x
    }

    println(a)
}           

实质上,Kotlin中的let函数就是在说"让这个变量等于这个值"。同时,它允许你在某个范围内定义一个变量,而不会污染外部范围,使代码更安全、更可预测。

使用with函数初始化或配置对象

考虑一种情况,你正在为一个新对象设置多个属性。以传统的方式,可能会发现自己在配置它的字段时反复引用对象:

fun main() {
    val stringBuilder = StringBuilder()
    stringBuilder.append("Hello, ")
    stringBuilder.append("world!")
    stringBuilder.append(" How ")
    stringBuilder.append("are ")
    stringBuilder.append("you?")

    println(stringBuilder.toString())
}           

通过使用with,将可能是冗长和重复的代码转变成了清晰简洁的块:

fun main() {
    val stringBuilder = StringBuilder()

    with(stringBuilder) {
        append("Hello, ")
        append("world!")
        append(" How ")
        append("are ")
        append("you?")
    }

    println(stringBuilder.toString())
}           

with函数接收两个参数:你正在操作的对象和一个包含在该对象上执行的操作的lambda。在这个lambda内部,可以直接访问对象的成员,无需用对象的名字或引用作为前缀。通过减少冗长的写法,不仅减少了冗长的代码,而且还降低了在引用对象时出现错误的可能性。

使用run函数组合初始化和处理

run函数是一个特别有用的工具,当你正在寻找在一个对象的上下文内执行一块操作的时候。

考虑一种情况,你正在初始化一个变量,也希望立即执行一组操作。run在这里表现出色,允许你将两种需求封装成一个优雅的表达:

class DatabaseConfiguration() {
    var host: String? = null
    var port: String? = null
    
    fun isValid() = host == null || port == null 
    
    fun connect() = DatabaseConnection(host, port)
}

class DatabaseConnection(val host: String?, val port: String?) {
    fun runQuery(query: String) = println("Running query: $query")
}

fun main() {
    val databaseConfiguration = DatabaseConfiguration()
    val connection = databaseConfiguration.run {
        // Initialization or configuration
        host = "localhost"
        port = "5739"
        
        // Execution of a block of code
        if (isValid()) error("Invalid Database Configuration")
        connect()
    }
    
    connection.runQuery("SELECT * FROM table")
}           

run充当一个对象(接收者)的作用,包含一个lambda表达式。在这个lambda中,可以直接访问对象的成员,无需明确的命名。然而,run的魅力不仅仅在于此;

它返回 lambda 的结果,将上下文感知操作的优点与函数输出结合起来。

run在需要组合初始化/配置和产生结果的执行块的场景中表现出色。它的使用不仅促进了代码的简洁性,而且增强了Kotlin所倡导的清晰性和表现力。

使用 APPLY 函数配置复杂对象

apply 函数充当配置对象的机制。然而,与 run 函数不同的是,它返回上下文对象本身。

当处理需要多次初始化或配置的对象时,此功能特别有利。 Apply 允许直接访问对象的成员,而无需显式引用对象,从而使代码更加简洁和流畅。

class DatabaseConnection {
    var host: String = ""
    var port: Int = 0
    var dbName: String = ""
    var username: String = ""
    var password: String = ""

    fun connect() {
        println("Connecting to database $dbName at $host:$port with username $username")
    }
}

fun main() {
    val dbConnection = DatabaseConnection().apply {
        host = "localhost"
        port = 5432
        dbName = "myDatabase"
        username = "admin"
        password = "password"
    }

    dbConnection.connect()
}           

它的工作原理如下:在对象上调用 apply 并采用 lambda 表达式。在此 lambda 中,对对象属性或函数的每次调用或访问都是在上下文对象上隐式进行的。完成后,apply 返回上下文对象,现已完全配置。

这不仅可以减少代码的整体冗长性,还可以提高清晰度。每个属性都在一个块中设置,该块清楚地表明了其用途:配置 DatabaseConnection 对象。

使用 also 函数执行操作

also 对于执行不应干扰主对象状态的操作或添加日志记录和其他副加功能特别有用。

class File {
    var name: String = ""
    var extension: String = ""
    var content: String = ""

    fun save() {
        println("Saving file $name.$extension with content: $content")
    }
}

fun main() {
    val report = File().apply {
        name = "Report"
        extension = "txt"
        content = "This is the content of the report."
    }.also {
        println("Preparing to save file: ${it.name}.${it.extension}")
        it.save()
    }
}           

在此示例中,也用于记录 File 对象的详细信息,然后在使用 apply 初始化后立即保存它。在also块中使用它引用File对象,允许直接访问其属性。其优点还在于它的简单性和保持代码可读性的能力,同时提供添加有意义的辅助操作的灵活性。

此外,also 返回原始对象的能力使其成为链接多个操作的完美工具,其中每个操作都是独立的,并保持核心业务逻辑干净和独立。

无论执行什么辅助操作,also 函数都体现了 Kotlin 对简洁、可读和富有表现力的代码的承诺。