Kotlin擴展

擴展函數(shù)

Kotlin中要擴展一個類的功能,除了使用繼承(直接繼承或繼承一個接口使用委托)外,更便捷的方式是為該類定義擴展函數(shù)或擴展屬性。此時稱該類為接收者(Receiver),通常我們會把擴展(Extension)定義為頂層的,以便于在各處使用。

fun Fragment?.isAlive(): Boolean {
    return this?.activity != null
            && this.activity?.isDestroyed == false
            && this.activity?.isFinishing == false
            && this.isAdded
            && !this.isDetached
}

擴展函數(shù)兩種類型聲明與轉(zhuǎn)換

當使用一個變量引用該擴展函數(shù),通過IDE的自動補全,可以發(fā)現(xiàn)變量的類型為(Fragment) -> Boolean,可實際上,將類型聲明為擴展更符合原義,即同一個函數(shù)可以聲明為兩種類型。

val a: (Fragment) -> Boolean = Fragment::isAlive
val b: Fragment.() -> Boolean = Fragment::isAlive

不過這也說明,這兩種類型其實在Kotlin認為是一致的,可以通過反編譯得知兩個變量的類型都為Function1。編譯器所理解的擴展函數(shù),實際上是將接收者作為其第一個參數(shù)的普通函數(shù)罷了。

public interface Function1<in P1, out R> : Function<R> {
    public operator fun invoke(p1: P1): R
}

因此,通過將擴展函數(shù)引用(Receiver::method)賦予一個函數(shù)變量并指定類型,并用該變量調(diào)用函數(shù),將擴展函數(shù)轉(zhuǎn)化為普通函數(shù)的形式,反之亦然。

而擴展函數(shù)引用,實際與類方法引用(Class::method)在語法上是完全一致的,類方法也支持通過這種方式進行調(diào)用形式的轉(zhuǎn)換。

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        Handler().postDelayed({
            print(isAlive()) //擴展函數(shù)
            print(a(this)) //通過擴展函數(shù)引用調(diào)用
            print(b()) //通過擴展函數(shù)引用調(diào)用
            
            val c: (Fragment) -> Boolean = Fragment::isAdded //類方法
            val d: Fragment.() -> Boolean = Fragment::isAdded
            print(c(this)) //通過類方法引用調(diào)用
            print(d()) //通過類方法引用調(diào)用
        }, 5000)
        ...
    }

方法與函數(shù)

從前,在過程式和函數(shù)式編程語言(C,JavaScript)中,我們似乎更愿意使用名稱“函數(shù)”,面向?qū)ο蟮恼Z言(Java)中我們更愿意使用名稱“方法”,來區(qū)分將函數(shù)操縱的對象放在括號內(nèi)還是括號前的不同書寫方式,雖然現(xiàn)在通常并不區(qū)分兩種名稱的使用了。Java中,我們經(jīng)常在各種Util類中定義靜態(tài)方法,以“函數(shù)”的形式來擴充類的能力,而Kotlin的擴展函數(shù),則提供了一種更面向?qū)ο蟮谋磉_方式。

擴展屬性

Kotlin中通過varval聲明的屬性(Property),要求必須進行初始化,這個初始化實際上是為其字段(backing field)賦值。屬性除了字段外,還包括val的getter方法,var的getter和setter方法。

而擴展屬性并沒有實際位于類中,它沒有backing field,也不支持初始化。擴展屬性雖然稱為屬性,原理上不過是定義了getter或setter擴展方法,然后通過屬性名調(diào)用擴展方法而已。

var File.content: String
    get() {
        return readText()
    }
    set(value) {
        writeText(value)
    }
val Float.dp: Float
    get() {
        return TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP, this, appContext.resources.displayMetrics)
    }

適用于所有類型的擴展函數(shù)

Kotlin標準庫中定義4個適用于所有類型的擴展函數(shù),聲明如下:

public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T, R> T.let(block: (T) -> R): R
public inline fun <T> T.apply(block: T.() -> Unit): T
public inline fun <T> T.also(block: (T) -> Unit): T

這4個擴展函數(shù)內(nèi)部都是直接調(diào)用block,而未進行任何特殊操作,區(qū)別僅在于接收的函數(shù)類型參數(shù)與返回值不同。可是要如何記憶這4個函數(shù)聲明而不用每次都查閱其定義呢?或許寫的多了自然能夠熟練區(qū)分,但這里提供一種方法輔助記憶。

首先思考一下,我們在什么時候使用這4個函數(shù),兩種常見的場景是:

  • 判空
  • 鏈式與嵌套調(diào)用

現(xiàn)有一個可空類型的屬性s和接收不可空類型的字符串處理方法op,當我們需要用op處理s時。使用progress1()會報錯,Smart cast to 'String' is impossible, because 's' is a mutable property that could have been changed by this time;使用progress2()雖然消除了報錯,但實際上依然不是安全的,且隨意使用!!并不是一個好的編碼習慣;這時我們需要用progress3()來正確判空。

var s: String? = ""
fun op(s: String) = ""
fun progress1() {
    if (s != null) { op(s) }
}
fun progress2() {
    if (s != null) { op(s!!) }
}
fun progress3() {
    s?.let { op(it) }
    s?.run { op(this) }
    s?.also { op(it) }
    s?.apply { op(this) }
}

而這4個擴展函數(shù),其實都可以正確判空,雖然在Kotlin編碼習慣中,普遍更多的使用let函數(shù)。

再來看鏈式與嵌套調(diào)用,通常這種使用方式主要是為了減少臨時變量的引入。從鏈式與嵌套調(diào)用的整個流程來看,只有返回值的不同才能對這種鏈式操作造成影響,而這4個擴展函數(shù)只有2種返回類型。

從鏈式與嵌套調(diào)用的單個操作的使用來看,我們可以使用函數(shù)引用和lambda表達式來向擴展函數(shù)傳遞參數(shù),而這4個擴展函數(shù)只有兩種函數(shù)參數(shù)。

下面進行嘗試,對于使用函數(shù)引用作為擴展函數(shù)參數(shù)的情況,我們前面說過,Kotlin會把相同功能是“方法”與“函數(shù)”看做同一種類型,因此4個擴展函數(shù)使用上并無差異。

fun test(fragment: Fragment?) {
    fragment?.run(Fragment::isAlive)
    fragment?.let(Fragment::isAlive)
    fragment?.apply(Fragment::isAlive)
    fragment?.also(Fragment::isAlive)
}

對于使用lambda表達式作為擴展函數(shù)參數(shù)的情況,不同的lambda表達式類型的內(nèi)部完全能實現(xiàn)相同的功能而僅有寫法上的差異而已。

fun test(fragment: Fragment?) {
    fragment?.run {
        activity != null
                && activity?.isDestroyed == false
                && activity?.isFinishing == false
                && isAdded
                && isDetached
    }
    fragment?.let {
        it.activity != null
                && it.activity?.isDestroyed == false
                && it.activity?.isFinishing == false
                && it.isAdded
                && it.isDetached
    }
}

以上我們證明了,對于所有情景,這4個擴展函數(shù)我們只需保留2個返回值類型不同的函數(shù)即可實現(xiàn)所有功能。2個3字母的擴展函數(shù)返回值為同一類型,2個非3字母的擴展函數(shù)返回值為同一類型。

可以在3字母的函數(shù)中選擇1個,在非3字母的函數(shù)中選擇1個,作為開發(fā)中使用”主力“。例如,使用runapply函數(shù),用于判空和鏈式與嵌套調(diào)用時使用,它們在lambda表達式內(nèi)部用this表示接收者,可以省略this更方便的使用類方法。當然也可以使用letalso,lambda表達式內(nèi)部用it表示參數(shù),或者起一個更有意義的名字讓程序更可讀 。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容