為什么需要擴展
一個新特性的出現(xiàn)必然是為了解決之前遺留的開發(fā)問題和提升目前開發(fā)效率。擴展函數(shù)也是如此。
首先來介紹下OOP:開放封閉原則。
軟件應該是可擴展,而不可修改的。也就是對擴展開放,對修改封閉
舉個栗子: 當某個三方庫的功能無法滿足現(xiàn)有業(yè)務時需要新增功能時。最簡單的做法就是直接對庫源碼修改,但是這樣違反了開放封閉原則:對源碼修改。
更合理的方案是依靠擴展。Kotlin的擴展函數(shù)很顯然能夠優(yōu)雅的解決這種問題。
擴展函數(shù)是什么
首先來看下他的使用:
fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) {
val tmp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = tmp
}
我們將MutableList叫做接受者(receivers),意思就是這個MutableList接受了這個函數(shù),也就是給這個類擴展了這個函數(shù)。
Java中的this叫做調用者,對于普通函數(shù)來說就是該函數(shù)所屬類的實例也就是調用者對象。由于這個函數(shù)是屬于MutableList的,所以在這個方法體中this也就是指代的MutableList。 通俗的來說擴展函數(shù)體里面的this就是receivers的類型
擴展函數(shù)怎么用
根據(jù)上面定義的擴展函數(shù)栗子,來看下這個擴展函數(shù)的用法:
val list = mutableListOf(1,3,5)
list.exchange(1,5)
這里看到擴展函數(shù)是基于對象實例來調用的,如果希望使用靜態(tài)的方式調用又該如何寫呢?稍后講解
再談擴展函數(shù)是什么
還是回到剛剛第二個話題,這次的是什么就不是簡單的介紹了。之前有篇文章講解過新技術必然離不開性能方面的考慮。因此再來講解下他是如何實現(xiàn)擴展函數(shù)的,我們通過解析他的反編譯字節(jié)碼~~
public static final void exchange(@NotNull List $receiver, int fromIndex, int toIndex) {
//檢查$receiver參數(shù)是否為空。receiver就是調用者
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
int tmp = ((Number)$receiver.get(fromIndex)).intValue();
$receiver.set(fromIndex, $receiver.get(toIndex));
$receiver.set(toIndex, Integer.valueOf(tmp));
}
可以看到該函數(shù)會變成一個靜態(tài)不可重寫的方法,并且receiver變成了第一個參數(shù)。擴展函數(shù)里的的this就是receiver參數(shù)。
public 修飾的靜態(tài)方法也就是全局方法,任何地方都可以調用到(之后詳細說)。
看來并沒有什么神奇的地方只是將擴展函數(shù)變成了一個靜態(tài)方法而已。所以性能方面是沒有影響的
擴展函數(shù)在哪里可以被使用
這里首先說明下,擴展函數(shù)定義在不同的地方效果也是不一樣的。
- 不定義在類中,也就是類外部
可以看到上面反編譯后的擴展函數(shù)就是這種類型,被static,public,final修飾的方法會有這個特征:在同一個包中是可以共享這個擴展函數(shù)的也就是可以調用到這個擴展函數(shù)。其他包里面如果也想使用這個函數(shù)就可以import這個包中的這個函數(shù)即可。
- 定義在類中,也就是類內部
這時候詭異的事情出現(xiàn)了,擴展函數(shù)無法被調用。接下來看下對應的擴展函數(shù)反編譯后的字節(jié)碼:
public final void exchange(@NotNull List $receiver, int fromIndex, int toIndex) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
int tmp = ((Number)$receiver.get(fromIndex)).intValue();
$receiver.set(fromIndex, $receiver.get(toIndex));
$receiver.set(toIndex, Integer.valueOf(tmp));
}
可以看到失去了static關鍵字并且變成了外部類中的方法(和正常的方法沒什么區(qū)別了),也就是其他地方調用不到了,只有該類或者該類的子類可以調用;如果失去了public關鍵字,那么將只有該類才能使用這個擴展函數(shù),其子類也無法使用。
總結下,如果沒有定義在類中那么該函數(shù)就是靜態(tài)的大家都可以調用。如果定義在類中那么就默認屬于該類和子類的普通函數(shù),所以只有在該類和子類中使用。上面只是說了調用的地方,實際上調用還是需要使用receiver進行調用。
擴展函數(shù)的限制
前面介紹了擴展函數(shù)實現(xiàn)的原理并且看到了擴展函數(shù)的作用域信息,接下來分析下擴展函數(shù)在哪些場景下會被限制。
靜態(tài)擴展函數(shù)
首先來回顧下普通的靜態(tài)函數(shù)/變量如何定義,在Kotlin中使用伴生對象類將函數(shù)/變量定義在其中,那么該函數(shù)/變量就是靜態(tài)函數(shù)/變量了。
class Son {
companion object {
//該變量為靜態(tài)變量
val age = 10
}
}
伴生類的實現(xiàn)可以觀察反編譯后的字節(jié)碼,其是定義了一個Companion的靜態(tài)內部類然后再該類中定義了這些靜態(tài)變量和方法
和普通函數(shù)/變量一樣,擴展函數(shù)也是一樣的定義方式,在伴生對象中定義擴展函數(shù):
fun Son.Companion.foo() {
println("age = $age")
}
這樣foo就不需要Son的實例直接可以通過Son的類名進行調用了。
這樣似乎看起來沒有什么問題,但是當我們需要擴展三方類的靜態(tài)函數(shù)時,如果其沒有用Kotlin的伴生對象指定靜態(tài)方法/變量,那么該方案將無法使用,只能用實例去調用。
函數(shù)優(yōu)先級
有沒有想過這樣一種情況:就是這個類擴展的函數(shù)名之前在這個類中就已經存在了,那么調用這個方法時,會調用擴展函數(shù)還是之前類中定義好的方法。
答案是:之前類中定義的方法、 因此:成員方法優(yōu)先級高于擴展函數(shù)
this的指向
當我們在類中使用擴展函數(shù)時,在擴展函數(shù)體內想要獲取當前類的this,而不是默認的擴展函數(shù)的receivers的類型的時候,我們可以指定this@類名來指向外部類。
擴展函數(shù)注意點
調用者類型是運行時類型,而接受者類型是編譯時類型也就是說當擴展被生命為成員函數(shù)時具體調用哪個類的擴展方法是由它的運行時類型決定,而具體調用哪個擴展方法是根據(jù)其被定義為什么類型也就是編譯時可知類型。
調用者類型也就是上面說的定義在類內部的擴展函數(shù)只有類實例才可以調用,而接受者receiver類型是擴展哪個類的類型
還是java中的規(guī)則: 重載基于編譯時類型,重寫基于運行時類型。
所以在編寫擴展函數(shù)時需要注意
- 1.如果該擴展函數(shù)定義在類內部就是頂級函數(shù)/成員函數(shù),不能被覆蓋;(因為是基于運行時類型)
- 2.我們無法訪問其接收器的非公共屬性;(本質是將其變?yōu)榉椒ǖ牡谝粋€參數(shù))
- 3.擴展接收器總是被靜態(tài)調度。(和重載一樣)
- 4.也是最重要的一點,不要濫用擴展特性,思考好合適的接受者receivers,不要什么都往context上堆;參數(shù)簡化要考慮是否有副作用
總結
Kotlin的擴展函數(shù)是非常好用的,其符合OOP原則,而且還可以擴展很多函數(shù)Google的ktx庫也是基于這個功能開發(fā)了很多好用的方法。