天天看點

《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 對簡潔、可讀和富有表現力的代碼的承諾。