Kotlin作用域函數的使用經驗

前言

Kotlin給我們提供了很多Java沒有的便利,作用域函數(Scope Function)就是Kotlin標準庫裏面的提供的一些讓我們減少重複代碼和提高可讀性的一系列函數。

下面結合我的使用經驗來介紹一下Kotlin的作用域函數:

  • 是什麼
  • 作用是什麼
  • 怎麼使用
  • 怎麼選擇
  • 對我們開發的啓發

介紹

官網介紹

如官網介紹所說,作用域函數(Scope Function)是能讓我們創建一個臨時的作用域,在這個作用域裏可以有一個上下文對象給我們用,最後它還有返回值的一些函數,可以用包括:let、run、apply、with、also等關鍵字來使用。

簡單總結,它的作用就是:可以減少冗餘代碼,從而讓代碼更簡潔;可以讓代碼形成鏈式調用,從而使代碼邏輯更清晰。

共有特點

  • 都是在一個代碼塊裏面執行一些代碼,在這個代碼塊裏,有返回值
  • 上下文對象的引用方式,都是隻讀val的,不要想着在代碼塊改變它的引用

本質

根據我個人的理解,我認爲Kotlin作用域函數的本質其實是:通過編譯器把一些常用的編碼範式,封裝成更簡潔,更容易讓開發者使用的上層接口。

使用格式

擴展函數

[返回值] = [對象].[作用域函數關鍵字]{ [上下文對象] ->

		// 代碼塊
}

非擴展函數

[返回值] = [作用域函數關鍵字]([對象]).{[上下文對象] ->

		// 代碼塊
}

使用例子

下面總結下我在開發時候遇到的一些使用例子:

簡單判空

        intent?.let { 
            Log.i(TAG, "onCreate: ${it.data}")
        }

Intent設置

        Intent().apply {
            putExtra("name", "totond")
            putExtra("age", 18)
            putExtra("time", 111)
            startActivity(this)
        }

Paint設置

    var paint = Paint().apply {
        textSize = 14.0f
        color = Color.WHITE
        isAntiAlias = false
    }

分類

是否擴展函數

區別

  • 最大的區別就是,如果需要用?.判空,則肯定需要擴展函數
  • 用擴展函數的必定存在一個上下文對象

上下文對象——是this還是it

區別

  • this適合在代碼塊裏面,對象作爲函數提供方的代碼比較多的時候使用,因爲可以省略12
  • it適合在代碼塊裏面,對象作爲函數入參的代碼比較多的時候使用,相比this可以寫少兩個字母

返回值——是上下文對象還是Lambda的結果

區別

  • 返回上下文對象時,一般是用於對這個對象設置的操作,所以很多時候配合this做上下文對象
  • 返回Lambda結果時,一般是要返回一個代碼塊裏面計算、處理後的結果

怎麼選

從上面我們瞭解到這些作用域函數的用法,但是有好幾種作用域函數,我們想要用它們的時候,要怎麼選呢?我個人是根據下面的方式來選的,大家可以參考下:

函數 上下文對象的引用方式 返回值 是否擴展函數
let it Lambda 表達式的結果值
run this Lambda 表達式的結果值
run - Lambda 表達式的結果值 不是: 不使用上下文對象來調用
with this Lambda 表達式的結果值 不是: 上下文對象作爲參數傳遞.
apply this 上下文對象本身
also it 上下文對象本身

對着表看:

  1. 上下文對象的引用方式、返回值、是否擴展函數,這3個要素,有沒有可以隨便的。例如很多情況下,返回值就是可以隨便的

  2. 根據步驟1中不可隨便的要素,來進行排除,排除優先級:返回值 > 是否擴展函數 > 上下文對象的引用方式

    • 返回值:最優先排除,因爲這個確定性最高,一般要不要返回值,在寫代碼塊之前就想好了
    • 是否擴展函數:用不用擴展函數,使用差異性很大,而且要判空就要有擴展函數的
    • 上下文對象的引用方式:最容易模糊的選擇的要素,因爲選哪個都能實現想要的效果,用錯了代碼會不夠簡潔:
      • this適合在代碼塊裏面,對象作爲函數提供方的代碼比較多的時候使用,因爲可以省略
      • it適合在代碼塊裏面,對象作爲函數入參的代碼比較多的時候使用,相比this可以寫少兩個字母

其實this也就比it多兩個字母

優缺點分析

使用場景分析

按照我的理解,作用域函數其實是一個錦上添花的功能,不用它用普通的ifelse也可以實現,例如:

        // 作用域函數寫法
        val result = input?.let {
            "strLen = ${it.length + 100}"
        }
        Log.i(TAG, "testLet: $result")

        // 普通寫法
        val result1 = if (input != null) {
            "strLen = ${input.length + 100}"
        } else {
            null
        }
        Log.i(TAG, "testLet: $result1")

但是,從上面的例子可以看出,作用域函數寫法看起來是更加簡單明瞭,雖然不熟悉的人看了可能會有點理解成本,但是熟悉了之後就很容易理解了。然後,我們一般處理一些流程的時候,通常會這樣寫代碼:

        // 一般寫法 input -> A -> B -> C
        val A = toA(input)
        if (A != null) {
            val B = AToB(A)
            if (B != null) {
                val C = BToC(B)
                Log.i(TAG, "C is $C")
            }
        }
        

這樣看起來太過層嵌套if語句了,我們可以用作用域函數把它優化爲:

        // 作用域寫法 input -> A -> B -> C
        toA(input)?.let {
            AToB(it)
        }?.let {
            BToC(it)
        }?.let {
            Log.i(TAG, "C is $it")
        }

這樣寫,實際就是利用作用域函數把判空的邏輯隱藏,利用鏈式調用的方式把邏輯以平鋪的方式展示出來,看上去是不是比一般寫法更清晰一點?但是,如果我們要利用if的多個分支或者需要利用流程的中間值的時候,用這種鏈式調用就可能會有點不方便了,例如:

        // if多了爲空的分支,c要引用中間值a
        val a = toA(input)
        if (a != null) {
            val B = AToB(A)
            if (B != null) {
                val C = BToC(B) + a
                Log.i(TAG, "C is $C")
            }
        } else {
            Log.i(TAG, "testCompare: a is null")
        }

當然這些也可以通過封裝值到data類等方法解決,但是這樣子就有種爲了寫鏈式調用代碼來寫代碼的意味了,所以選擇作用域函數的時候,我會特別留意這些點,後續這個代碼塊會不會擴展爲,多參數輸入、多分支走向、中間值引用等

優點:

  • 省去冗餘代碼,讓代碼更簡潔。如使用了this,基本可以用來取代Builder模式
  • 讓代碼能夠保持鏈式調用,邏輯更清晰(看熟悉了之後)

缺點:

  • 有上手難度,一開始記不住這麼多類型,可能每次用的時候要查表
  • 多個作用域函數組成的鏈式調用,可擴展性沒有普通使用那麼好,如果

啓發

瞭解了kotlin的這些作用域函數之後,我發現了這些也算是官方給我們寫的例子,教我們怎麼使用Lambda和擴展函數。

我們在實際開發中,完全可以模仿官方這種設計思想,封裝一些實用的函數,如:

類型的轉換

當我們某些數據類型要進行轉換成另外一個類型的時候,用java寫一般都是會封裝成啥xxUtil,現在我們可以直接用擴展函數,在給這個對象“加”方法:

    fun String.LetterToNum(): Int{
        return when (this) {
            "A" -> 1
            "B" -> 2
            "C" -> 3
            else -> 0
        }
    }
    
    Log.i(TAG, "A to num = ${"A".LetterToNum()}")
    //輸出:A to num = 1

簡化一些方法的調用

如果有些方法,要調用的話需要另外一個語句,不能保持鏈式調用的話,可以通過轉換爲擴展函數的方式去使用,如:

項目裏用的把Disposable加到CompositeDisposable:

Observable.just(data)
            .observeOn(Schedulers.io())
            .subscribeOn(AndroidSchedulers.mainThread())
            .subscribe()
            .addComposite(compositeDisposable)
            

fun Disposable.addComposite(compositeDisposable: CompositeDisposable) {
    compositeDisposable.add(this)
}

打印分割線:

定義:
   // 打印分割線
    private fun Unit.divider() {
        Log.i(TAG, "--------------------------------------------------------------")
    }
        
使用:
        testRun().divider()
        testLet().divider()
        testApply().divider()
        testAlso().divider()
        testWith().divider()

輸出:
I: testRun: strLen = 103
I: --------------------------------------------------------------
I: testLet: strLen = 103
I: --------------------------------------------------------------
I: testApply: yan
I: --------------------------------------------------------------
I: testAlso: yan
I: --------------------------------------------------------------
I: testWith: strLen = 103
I: --------------------------------------------------------------

後話

以上是我開發中使用kotlin作用域函數所積累到的一些經驗,如有錯漏,敬請指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章