天天看點

let,run,with,apply及also差異化分析

作者:付十一

連結:https://juejin.cn/post/6975384870675546126

作用域函數是Kotlin比較重要的一個特性,共分為以下5種:let、run、with、apply 以及 also,這五個函數的工作方式可以說非常相似,但是我們需要了解的是這5種函數的差異,以便在不同的場景更好的利用它。讀完這篇文章您将了解到:

  • 什麼是Kotlin的作用域函數?
  • let、run、with、apply 以及 also這5種作用域函數各自的角色定位;
  • 5種作用域函數的差異區分;
  • 何時何地使用這5種作用域?

Kotlin的作用域函數

Kotlin 标準庫包含幾個函數,它們的唯一目的是在對象的上下文中執行代碼塊。當對一個對象調用這樣的函數并提供一個 lambda 表達式時,它會形成一個臨時作用域。在此作用域中,可以通路該對象而無需其名稱。這些函數稱為作用域函數。

簡單來說,作用域函數是為了友善對一個對象進行通路和操作,你可以對它進行空檢查或者修改它的屬性或者直接傳回它的值等操作,下面提供了案例對作用域函數進行了詳細說明。

角色定位

let

public inline fun <T, R> T.let(block: (T) -> R): R 
           

複制

let函數是參數化類型 T 的擴充函數。在let塊内可以通過 it 指代該對象。傳回值為let塊的最後一行或指定return表達式。

我們以一個Book對象為例,類中包含Book的name和price,如下:

class Book() {
    var name = "《資料結構》"
    var price = 60
    fun displayInfo() = print("Book name : $name and price : $price")
}
           

複制

fun main(args: Array<String>) {
    val book = Book().let {
        it.name = "《計算機網絡》"
        "This book is ${it.name}"
    }
    print(book)
}

控制台輸出:
This book is 《計算機網絡》
           

複制

在上面案例中,我們對Book對象使用let作用域函數,在函數塊的最後一句添加了一行字元串代碼,并且對Book對象進行列印,我們可以看到最後控制台輸出的結果為字元串“This book is 《計算機網絡》”。

按照我們的程式設計思想,列印一個對象,輸出必定是對象,但是使用let函數後,輸出為最後一句字元串。這是由于let函數的特性導緻。因為在Kotlin中,如果let塊中的最後一條語句是非指派語句,則預設情況下它是傳回語句。

那如果我們将let塊中最後一條語句修改為指派語句,會發生什麼變化?

fun main(args: Array<String>) {
    val book = Book().let {
        it.name = "《計算機網絡》"
    }
    print(book)
}

控制台輸出:
kotlin.Unit
           

複制

可以看到我們将Book對象的name值進行了指派操作,同樣對Book對象進行列印,但是最後控制台的輸出結果為“kotlin.Unit”,這是因為在let函數塊的最後一句是指派語句,print則将其當做是一個函數來看待。

這是let角色設定的第一點:1️⃣

let塊中的最後一條語句如果是非指派語句,則預設情況下它是傳回語句,反之,則傳回的是一個 Unit類型

我們來看let的第二點:2️⃣

let可用于空安全檢查。

如需對非空對象執行操作,可對其使用安全調用操作符 ?. 并調用 let 在 lambda 表達式中執行操作。如下案例:

var name: String? = null
fun main(args: Array<String>) {
    val nameLength = name?.let {
        it.length
    } ?: "name為空時的值"
    print(nameLength)
}
           

複制

我們設定name為一個可空字元串,利用name?.let來進行空判斷,隻有當name不為空時,邏輯才能走進let函數塊中。在這裡,我們可能還看不出來let空判斷的優勢,但是當你有大量name的屬性需要編寫的時候,就能發現let的快速和簡潔。

let的第三點:3️⃣

let可對調用鍊的結果進行操作。

關于這一點,官方教程給出了一個案例,在這裡就直接使用:

fun main(args: Array<String>) { 
    val numbers = mutableListOf("One","Two","Three","Four","Five")
    val resultsList = numbers.map { it.length }.filter { it > 3 }
    print(resultsList)
}
           

複制

我們的目的是擷取數組清單中長度大于3的值。因為我們必須列印結果,是以我們将結果存儲在一個單獨的變量中,然後列印它。但是使用“let”操作符,我們可以将代碼修改為:

fun main(args: Array<String>) {
    val numbers = mutableListOf("One","Two","Three","Four","Five")
    numbers.map { it.length }.filter { it > 3 }.let {
        print(it)
    }
}
           

複制

使用let後可以直接對數組清單中長度大于3的值進行列印,去掉了變量指派這一步。

另外,let函數還存在一個特點。

let的第四點:4️⃣

let可以将“It”重命名為一個可讀的lambda參數。

let是通過使用“It”關鍵字來引用對象的上下文,是以,這個“It”可以被重命名為一個可讀的lambda參數,如下将it重命名為book:

fun main(args: Array<String>) {
    val book = Book().let {book ->
        book.name = "《計算機網絡》"
    }
    print(book)
}
           

複制

run

run函數以“this”作為上下文對象,且它的調用方式與let一緻。

另外,第一點:1️⃣ 當 lambda 表達式同時包含對象初始化和傳回值的計算時,run更适合。

這句話是什麼意思?我們還是用案例來說話:

fun main(args: Array<String>) {

    Book().run {
        name = "《計算機網絡》"
        price = 30
        displayInfo()
    }
}

控制台輸出:
Book name : 《計算機網絡》 and price : 30
           

複制

如果不使用run函數,相同功能下代碼會怎樣?來看一看:

fun main(args: Array<String>) {

    val book = Book()
    book.name = "《計算機網絡》"
    book.price = 30
    book.displayInfo()
}

控制台輸出:
Book name : 《計算機網絡》 and price : 30
           

複制

輸出結果還是一樣,但是run函數所帶來的代碼簡潔程度已經顯而易見。

除此之外,讓我們來看看run函數的其他優點:

通過檢視源碼,了解到run函數存在兩種聲明方式,

  1. 與let一樣,run是作為T的擴充函數;
inline fun <T, R> T.run(block: T.() -> R): R 
           

複制

  1. 第二個run的聲明方式則不同,它不是擴充函數,并且塊中也沒有輸入值,是以,它不是用于傳遞對象并更改屬性的類型,而是可以使你在需要表達式的地方就可以執行一個語句。
inline fun <R> run(block: () -> R): R
           

複制

如下利用run函數塊執行方法,而不是作為一個擴充函數:

run {
        val book = Book()
        book.name = "《計算機網絡》"
        book.price = 30
        book.displayInfo()
    }
           

複制

with

inline fun <T, R> with(receiver: T, block: T.() -> R): R 
           

複制

with屬于非擴充函數,直接輸入一個對象receiver,當輸入receiver後,便可以更改receiver的屬性,同時,它也與run做着同樣的事情。

還是提供一個案例說明:

fun main(args: Array<String>) {
    val book = Book()
   
    with(book) {
        name = "《計算機網絡》"
        price = 40
    }
    print(book)
}
           

複制

以上面為例,with(T)類型傳入了一個參數book,則可以在with的代碼塊中通路book的name和price屬性,并做更改。

with使用的是非null的對象,當函數塊中不需要傳回值時,可以使用with。

apply

inline fun <T> T.apply(block: T.() -> Unit): T
           

複制

apply是 T 的擴充函數,與run函數有些相似,它将對象的上下文引用為“this”而不是“it”,并且提供空安全檢查,不同的是,apply不接受函數塊中的傳回值,傳回的是自己的T類型對象。

fun main(args: Array<String>) {
    Book().apply {
        name = "《計算機網絡》"
        price = 40

    }
    print(book)
}

控制台輸出:
com.fuusy.kotlintest.Book@61bbe9ba
           

複制

前面看到的 let、with 和 run 函數傳回的值都是 R。但是,apply 和下面檢視的 also 傳回 T。例如,在 let 中,沒有在函數塊中傳回的值,最終會成為 Unit 類型,但在 apply 中,最後傳回對象本身 (T) 時,它成為 Book 類型。

apply函數主要用于初始化或更改對象,因為它用于在不使用對象的函數的情況下傳回自身。

also

inline fun <T> T.also(block: (T) -> Unit): T 
           

複制

also是 T 的擴充函數,傳回值與apply一緻,直接傳回T。also函數的用法類似于let函數,将對象的上下文引用為“it”而不是“this”以及提供空安全檢查方面。

因為T作為block函數的輸入,可以使用also來通路屬性。是以,在不使用或不改變對象屬性的情況下也使用also。

fun main(args: Array<String>) {
    val book  = Book().also {
        it.name = "《計算機網絡》"
        it.price = 40
    }
    print(book)
}

控制台輸出:
com.fuusy.kotlintest.Book@61bbe9ba
           

複制

差異化

let & run

  • let将上下文對象引用為it ,而run引用為this;
  • run無法将“this”重命名為一個可讀的lambda參數,而let可以将“it”重命名為一個可讀的lambda參數。在let多重嵌套時,就可以看到這個特點的優勢所在。

with & run

with和run其實做的是同一種事情,對上下文對象都稱之為“this”,但是他們又存在着不同,我們來看看案例。

先使用with函數:

fun main(args: Array<String>) {
    val book: Book? = null
    with(book){
        this?.name = "《計算機網絡》"
        this?.price = 40
    }
    print(book)

}
           

複制

我們建立了一個可空對象book,利用with函數對book對象的屬性進行了修改。代碼很直覺,那麼我們接着将with替換為run,代碼更改為:

fun main(args: Array<String>) {
    val book: Book? = null
    book?.run{
        name = "《計算機網絡》"
        price = 40
    }
    print(book)
}
           

複制

首先run函數的調用省略了this引用,在外層就進行了空安全檢查,隻有非空時才能進入函數塊内對book進行操作。

相比較with來說,run函數更加簡便,空安全檢查也沒有with那麼頻繁。

apply & let

  • apply不接受函數塊中的傳回值,傳回的是自己的T類型對象,而let能傳回。
  • apply上下文對象引用為“this”,let為“it”。

何時應該使用 apply、with、let、also 和 run ?

  • 用于初始化對象或更改對象屬性,可使用apply
  • 如果将資料指派給接收對象的屬性之前驗證對象,可使用also
  • 如果将對象進行空檢查并通路或修改其屬性,可使用let
  • 如果是非null的對象并且當函數塊中不需要傳回值時,可使用with
  • 如果想要計算某個值,或者限制多個本地變量的範圍,則使用run

總結

以上便是Kotlin作用域函數的作用以及使用場景,在Android實際開發中,5種函數使用的頻次非常高,在使用過程中發現,當代碼邏輯少的時候,作用域函數能帶給我們代碼的簡潔性可讀性,但是當邏輯複雜時,使用不同的函數,多次疊加都将降低可讀性。這就要我們去區分它們各自的特點,以便在适合且複雜的場景下去使用它。