漫畫:Kotlin 的擴展細節探究

漫畫:Kotlin 的擴展細節探究

擴展並不是 Kotlin 首創的,在 C# 和 Gosu 裡,其實早就有類似的實現,Kotlin 本身在設計之初,就參考了很多語言的優點!

Kotlin 可以利用擴展,在不繼承父類也不使用任何裝飾器設計模式的情況下,對指定的類進行功能的擴展。

Kotlin 的擴展包含了擴展函數和擴展屬性,需要適用特殊的聲明方式來完成。也就是說你可以對任何類,增加一些方法或者屬性,來增強它的功能。

比較常見的場景,就是原本我們需要實現的各種 SpUtils、ViewUtils 之類的各種 XxxUtils 工具類。如果需要,我們可以直接在對應的類上,進行直接擴展。

說的這麼厲害,舉個實際的例子就可以說明一切了。我一般會在項目內建立一個 SpUtils 的幫助類,來幫我們快速的操作 SharePreferences。

fun Context.getSpString(key:String):String{ val sp = getSharedPreferences("cxmy_sp",Context.MODE_PRIVATE) return sp.getString(key,"") }

在這個例子中,我們對 Context 類進行擴展,為了讓它能夠支持快速的從 SharePreferences 中獲取到持久化的數據。當然,我們還是要傳遞進去一個我們存儲數據的 Key。

這樣使用它就非常的簡單了,我們可以直接能夠持有 Context 的地方,直接調用 getSpString() 方法。

// Activity 中 getSpString("cxmy") // or mContext.getSpString("cxmy")

漫畫:Kotlin 的擴展細節探究

擴展是靜態解析的

我們知道,Kotlin 最終依然會被編譯成 Java 的字節碼在虛擬機中運行。Kotlin 也無法突破 Java 中不被允許的操作限制,所以它並不能真正的修改他們所擴展的類。

通過定義一個擴展,其實你並沒有在一個現有類中,真的插入一個新的方法或者屬性,僅僅是可以通過該類型的變量,用點表達式調用這個新方法或者屬性。

類是允許繼承的,而靜態解析這一規則,就是為了在類的繼承這一點上,不存在二義性。

當父類以及它的子類,都通過擴展的方式,增加一個 foo() 方法的時候,具體在調用的時候,是調用父類的 foo() 方法還是子類的 foo() 方法,完全取決於調用時,表達式所在的類型決定的,而不是由表達式運行時的類型決定的。

這裡強調的擴展是靜態解析的,即他們不是根據接受者類型的虛方法來判定調用那個方法。

一例勝千文,我們依然來舉個例子。

open class A() class B:A(){ } fun A.foo(){ Log.i("cxmy","A.foo") } fun B.foo(){ Log.i("cxmy","B.foo") } fun printFoo(a: A){ a.foo() } printFoo(B()) 

在這個例子中,我們傳遞進去的是 B 對象,但是實際上會調用 A.foo() 方法,所以輸出應該是 "A.foo()"。

這也印證了擴展是依據調用所在的表達式類型來決定的,而不是由表達式運行時的類型決定的。

在 Kotlin 中,使用 is 操作符,會讓代碼塊中的類型有一次隱式轉換,但是它對擴展是無效的,如果有特殊要求,可以使用 as 操作符顯式的進行強轉,方可生效。

fun foo(){ val b = B() b.foo() if(b is A){ (b as A).foo() b.foo() } }

隨手運行一下,它的結果就明朗了。

B.foo() A.foo() B.foo()

不過雖說靜態解析這一規則是為了限制繼承的歧義,但是正常使用擴展,它其實是可以在其繼承者身上調用的。例如在 Context 類上擴展了某個方法,同樣可以通過 Activity 或者 Server 這些 Context 的子類進行調用,它們並不衝突。

漫畫:Kotlin 的擴展細節探究

可空接收者

擴展的類的類型,也可以是一個可空的接收者類型。也就是我們可以在一個可空的類上定義擴展,大大的增加了擴展的適用範圍。

fun Any?.toString(): String { if (this == null) return "null" // 空檢測之後,“this”會自動轉換為非空類型,所以下面的 toString() // 解析為 Any 類的成員函數 return toString() }

在這個例子中,我們在任意對象上,通過擴展實現了 toString() 方法,注意這裡擴展的類是 Any? ,它是允許在一個為 null 的對象上直接調用的。

漫畫:Kotlin 的擴展細節探究

擴展屬性

與函數類似,Kotlin 同樣支持擴展屬性。

val Context.pgName: String get() = "com.cxmy.dev"

和擴展方法一樣,擴展屬性不過擴展屬性並不等於這個類上真實的屬性,它並沒有實際的將這個屬性插入到這個類當中。

因此,對擴展屬性來說,幕後字段 field 是不存在的,所以我們沒法寫類似這樣的代碼,並且擴展屬性不能有初始化器。

var stringRepresentation: String = "cxmyDev" get() = field.toString() set(value) { field = value // 解析字符串並賦值給其他屬性 }

雖然擴展屬性沒有幕後字段,但是它們的行為我們依然可以通過顯示提供的 getters/setters 來定義。

例如:

var Context.channel: String get() { return getSpString("channel") } set(value) { setSpString("channel",value) }

雖然沒有幕後字段 field ,但是我們可以將值存儲在其他地方,這裡舉例將其存儲在 SharePreferences 裡。

漫畫:Kotlin 的擴展細節探究

在 Java 中調用 Kotlin 的擴展代碼

首先,Kotlin 在設計之初,就已經考慮了和 Java 互相調用的問題,所以這一點我們完全不用擔心,不知道怎麼調用,只要去找對應的調用方法就好了。

例如文檔中的例子:

在 org.foo.bar 包內的 example.kt 文件中聲明的所有函數和屬性,包括擴展函數,都會編譯成一個名為 org.foo.bar.ExampleKt 的 Java 類的靜態方法。

這也印證了前面提到的,對於 Kotlin 的擴展,它並不會真的在擴展類中,插入一個方法或者屬性,而是以一個 XxxKt 的命名方式命名的類的形似存在。

而 Kotlin 的擴展,在轉換為 Java 字節碼的時候,會進行特殊處理,會自動生成另外一個方法簽名。

例如:

// SpUtils.kt fun Context.getSpString(key: String): String { val sp = getSharedPreferences("cxmy_sp", Context.MODE_PRIVATE) return sp.getString(key, "") }

會變成:

// SpUtilsKt.java public static final String getSpString(Context context,String key){ //... }

可以看到它幫我們生成的方法中,會將擴展依賴的類當成一個參數傳遞給這個靜態方法。這樣,我們在 Java 中的調用,就清晰了。

SpUtilsKt.getSpString(context,"channel")

擴展屬性也一樣,會變成一個 getXxx() 的方法,就不再贅述了。

雖然 XxxKt 這個類是自動生成的,我們無需關心細節。如果對這個命名有特殊嗜好,其實可以通過 @JvmName 註釋,修改生成的 Java 類的類名,需要注意的是 @JvmName 註釋,需要加載 kt 文件的首行,前面不能有其他代碼。

@file:JvmName("SpHelper") // ...

這樣,我們在 Java 代碼中調用的時候,就脫離了 Kt 字段,更像是一個原本就用 Java 語言編寫的方法了。

擴展到這裡就完全清晰了,有的點都涉及到了。實際上 Google I/O 上發佈的 AndroidX KTX,基本上就是依賴 Kotlin 的擴展功能實現的,還不瞭解 Android KTX 的可以戳這裡。

擴展對於 Kotlin 的意義非凡,也確實能讓我們編寫的代碼更清晰以及調用起來更方便。


分享到:


相關文章: