Hi,大家好,我是承香墨影!
擴充套件
擴充套件並不是 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 最終依然會被編譯成 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 的子類進行呼叫,它們並不衝突。
可空接收者
擴充套件的類的型別,也可以是一個可空的接收者型別。也就是我們可以在一個可空的類上定義擴充套件,大大的增加了擴充套件的適用範圍。
fun Any?.toString(): String {
if (this == null) return "null"
// 空檢測之後,“this”會自動轉換為非空型別,所以下面的 toString()
// 解析為 Any 類的成員函式
return toString()
}
複製程式碼
在這個例子中,我們在任意物件上,通過擴充套件實現了 toString()
方法,注意這裡擴充套件的類是 Any?
,它是允許在一個為 null 的物件上直接呼叫的。
擴充套件屬性
與函式類似,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 裡。
在 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 的意義非凡,也確實能讓我們編寫的程式碼更清晰以及呼叫起來更方便。
另外,關於 Kotlin 的擴充套件,你還有什麼不瞭解的,可以在留言區留言討論!
公眾號後臺回覆成長『成長』,將會得到我準備的學習資料,也能回覆『加群』,一起學習進步;你還能回覆『提問』,向我發起提問。
推薦閱讀: