點(diǎn)贊關(guān)注,不再迷路轿亮,你的支持對我意義重大疮薇!
?? Hi,我是丑丑我注。本文 「Android 路線」| 導(dǎo)讀 —— 從零到無窮大 已收錄按咒,這里有 Android 進(jìn)階成長路線筆記 & 博客,歡迎跟著彭丑丑一起成長但骨。(聯(lián)系方式在 GitHub)
歷史上的今天
2000 年 2 月 29 日励七,R 1.0.0 正式發(fā)布。
R 語言最初是由新西蘭奧克蘭大學(xué)的羅斯·伊哈卡和羅伯特·杰特曼開發(fā)的奔缠,由來由 “R 開發(fā)核心團(tuán)隊(duì)” 負(fù)責(zé)掠抬。R 是基于 S 語言的一個 GNU 計(jì)劃項(xiàng)目,語法來自 Schema校哎,主要用于統(tǒng)計(jì)分析两波、繪圖和數(shù)據(jù)挖掘。RStudio 是針對 R 語言設(shè)計(jì)的廣泛使用的集成開發(fā)環(huán)境闷哆。
—— 《了不起的程序員》
前言
擴(kuò)展是 Kotlin 的一種語言特性腰奋,即:在不修改類 / 不繼承類的情況下,向一個類添加新函數(shù)或者新屬性抱怔。擴(kuò)展使我們可以合理地遵循開閉原則劣坊,在大多數(shù)情況下是比繼承更好的選擇。
目錄
前置知識
這篇文章的內(nèi)容會涉及以下前置 / 相關(guān)知識屈留,貼心的我都幫你準(zhǔn)備好了局冰,請享用~
1. 為什么要使用擴(kuò)展测蘑?
在 Java 中,我們習(xí)慣于把通用代碼封裝到工具類中康二,諸如 StringUtils碳胳、ViewUtils 等,例如:
StringUtils.java
public static void firstChar(String str) {
...
}
在使用時赠摇,我們就需要調(diào)用StringUtils.firstChar(str)
固逗。然而,這種傳統(tǒng)的調(diào)用方式不夠簡單直接藕帜,表意性也不夠強(qiáng)烫罩,會讓調(diào)用方忽略 String 和 firstChar() 間的強(qiáng)聯(lián)系。另外洽故,調(diào)用方也希望省略 StringUtils 類名贝攒,讓 firstChar() 看起來更像是 String 內(nèi)部的一個屬性和方法,像這樣:"str".firstChar()
时甚。
要實(shí)現(xiàn)這種方式隘弊,在 Java 中就需要修改或繼承 String 類,然而 String 是 JDK 中的 final 類荒适,不能修改或繼承梨熙。
這個時候可以使用 Kotlin 擴(kuò)展來解決這個問題,我們可以把 firstChar 定義為 String 的擴(kuò)展函數(shù):
StringUtils.kt
定義 String 的擴(kuò)展函數(shù)
fun String.firstChar() {
...
}
此時刀诬,在使用時可以采用"str".firstChar()
的方式咽扇。在這里我們擴(kuò)展了 String 類,卻沒有修改或繼承 String陕壹。
總結(jié):擴(kuò)展是 Kotlin 中的一種特性质欲,可以 在不修改類 / 不繼承類的情況下,向一個類添加新函數(shù)或者新屬性糠馆,更符合開閉原則嘶伟。
開閉原則(OCP,Open Closed Principle)
開閉原則是面向?qū)ο筌浖O(shè)計(jì)的原則之一又碌,即:對擴(kuò)展開放九昧,而對修改是封閉的。
2. 擴(kuò)展函數(shù) & 擴(kuò)展屬性
2.1 聲明擴(kuò)展
聲明擴(kuò)展非常簡單毕匀,只需要在聲明時增加「類或者接口名」铸鹰。這個類的名稱稱為 接收者類型(receiver type),調(diào)用這個擴(kuò)展的對象稱為 接收者對象期揪。大多數(shù)情況下,擴(kuò)展會聲明為「頂級成員」规个,例如:
Utils.kt
聲明擴(kuò)展函數(shù):
fun <T : Any?> MutableList<T>.exchange(fromIndex: Int, toIndex: Int) {
val temp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = temp
}
聲明擴(kuò)展屬性:
val MutableList<Int>.sumIsEven
get() = this.sum() % 2 == 0
在使用時凤薛,就可以直接像使用普通成員函數(shù) / 屬性一樣:
xxx.kt
val list = mutableListOf(1,2,3)
使用擴(kuò)展函數(shù):
list.exchange(1,2)
使用擴(kuò)展屬性:
val isEven = list.sumIsEven
提示: MutableList 是接收者類型姓建,list 是接收者對象。
在擴(kuò)展函數(shù)內(nèi)部缤苫,你可以像 「成員函數(shù)」 那樣使用this
來引用接受者對象速兔,當(dāng)然有時也可以省略,例如:
聲明擴(kuò)展屬性:
val MutableList<Int>.sumIsEven
get() = this.sum() % 2 == 0 // 省略了 this.sum() 中的 this
2.2 可空接收者
在 第 2.1 節(jié) 中使用了「非空的接收者類型」來定義擴(kuò)展(MutableList 沒有關(guān)鍵詞?
)活玲,當(dāng)使用「可空變量」調(diào)用擴(kuò)展時涣狗,會報編譯時錯誤。例如:
val list:MutableList<Int>? = null
list.sumIsEven // Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type MutableList<Int>?
根據(jù)提示舒憾,我們知道可以 使用「可空的接收者類型」來定義擴(kuò)展镀钓,同時還要在內(nèi)部使用null == this
來對接收者對象進(jìn)行判空。例如:
可空接收者類型的擴(kuò)展函數(shù)
fun <T : Any?> MutableList<T>?.exchange(fromIndex: Int, toIndex: Int) {
if (null == this) return
val temp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = temp
}
可空接收者類型的擴(kuò)展屬性
val MutableList<Int>?.sumIsEven: Boolean
get() = if (null == this)
false
else
this.sum() % 2 == 0
2.3 在 Java 中調(diào)用
擴(kuò)展的本質(zhì):擴(kuò)展函數(shù)是定義在類外部的靜態(tài)函數(shù)镀迂,函數(shù)的第一個參數(shù)是接收者類型的對象丁溅。這意味著調(diào)用擴(kuò)展時不會創(chuàng)建適配對象或者任何運(yùn)行時的額外消耗。
在 Java 中探遵,我們只需要像調(diào)用普通靜態(tài)方法那樣調(diào)用擴(kuò)展即可窟赏。例如:
xxx.java
ArrayList<Integer> list = new ArrayList<>(3);
使用擴(kuò)展函數(shù):
UtilsKt.exchange(list, 1, 2);
使用擴(kuò)展屬性:
boolean isEven = UtilsKt.getSumIsEven(list);
2.4 擴(kuò)展的作用域
當(dāng)你定義了一個擴(kuò)展之后,它不會自動在整個項(xiàng)目內(nèi)生效箱季。在其它包路徑下涯穷,需要使用improt
導(dǎo)入。例如:
import Utils.exchange
或
import Utils.*
當(dāng)你在不同包中定義了 「重名擴(kuò)展」藏雏,并且需要在同一個文件中去使用它們拷况,那么你需要使用as
關(guān)鍵字重新命名。例如:
import Utils.exchange as swap
使用時:
list.swap(0,1)
2.5 注意事項(xiàng)
- 1诉稍、擴(kuò)展函數(shù)不能訪問 private 或 protected 成員
擴(kuò)展函數(shù)或擴(kuò)展屬性本質(zhì)上是定義在類外部的靜態(tài)方法蝠嘉,因此擴(kuò)展不可能打破類的封裝性而去調(diào)用 private 或 protected 成員;
- 2杯巨、不能重寫擴(kuò)展函數(shù)
擴(kuò)展函數(shù)在 Java 中會被編譯為靜態(tài)函數(shù)蚤告,并不是類的一部分,不具備多態(tài)性服爷。盡管你可以給父類和子類都定義一個同名的擴(kuò)展函數(shù)杜恰,看起來像是方法重寫,但實(shí)際上兩個函數(shù)沒有任何關(guān)系仍源。當(dāng)這個函數(shù)被調(diào)用時心褐,具體調(diào)用的函數(shù)版本取決于變量的 「靜態(tài)類型」,而不是 「動態(tài)類型」笼踩。
靜態(tài)方法調(diào)用
關(guān)于 Java 方法調(diào)用的本質(zhì)逗爹,在我之前寫過的一篇文章里系統(tǒng)分析過:Java | 方法調(diào)用的本質(zhì)(含重載與重寫區(qū)別)。靜態(tài)方法調(diào)用在編譯后生成
invokestatic
字節(jié)碼指令嚎于,它的處理邏輯如下:
- 1掘而、編譯階段:確定方法的符號引用挟冠,并固化到字節(jié)碼中方法調(diào)用指令的參數(shù)中;
- 2袍睡、類加載解析階段:根據(jù)符號引用中類名知染,在對應(yīng)的類中找到簡單名稱與描述符相符合的方法,如果找到則將符號引用轉(zhuǎn)換為直接引用斑胜;否則控淡,按照繼承關(guān)系從下往上依次在各個父類中搜索;
- 3止潘、調(diào)用階段:符號引用已經(jīng)轉(zhuǎn)換為直接引用掺炭;調(diào)用
invokestatic
不需要將對象加載到操作數(shù)棧,只需要將所需要的參數(shù)入棧就可以執(zhí)行invokestatic
指令覆山。
3竹伸、如果類的成員函數(shù)和擴(kuò)展函數(shù)擁有相同的簽名,成員函數(shù)優(yōu)先
4簇宽、擴(kuò)展屬性沒有支持字段勋篓,不會保存任何狀態(tài)
擴(kuò)展屬性是沒有狀態(tài)的,必須定義 getter 訪問器魏割。因?yàn)椴豢赡芙o現(xiàn)有的 Java 類添加額外的字段譬嚣,所以也就沒有地方可以存儲支持字段。舉個例子钞它,以下代碼是編譯錯誤的:
val MutableList<Int>?.sumIsEven: Boolean = true // (X) Initializer is not allowed here because this property has no backing field
get() = if (null == this)
false
else
this.sum() % 2 == 0
3. 標(biāo)準(zhǔn)庫中的函數(shù)
在 Kotlin 標(biāo)準(zhǔn)庫中拜银,定義了一系列通用的內(nèi)聯(lián)函數(shù):T.apply、T.also遭垛、T.let尼桶、T.run、with
锯仪。你是否清楚理解它們的用法 & 本質(zhì)泵督,它們都是擴(kuò)展函數(shù)嗎?
val str1: String = "".run {
println(this.length)
this
}
val str2: String = with("") {
println(this.length)
this
}
val str3: String = "".apply {
println(this.length)
}
val str4: String = "".also {
println(it.length)
}
val str5: String = "".let {
println(it.length)
it
}
在上面的示例中庶喜,我們看到有的函數(shù)作用域內(nèi)使用了this
小腊,而其它又使用了it
。這兩個關(guān)鍵字到底引用的是什么久窟,為什么會有差別呢秩冈?
我們先找到這些函數(shù)的聲明:
standard.kt
public inline fun <R> run(block: () -> R): R {
return block()
}
public inline fun <T, R> T.run(block: T.() -> R): R {
return block()
}
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}
public inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}
public inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}
public inline fun <T, R> T.let(block: (T) -> R): R {
return block(this)
}
一臉懵逼,別急斥扛,我們梳理一下:
函數(shù) | 參數(shù)1 | 參數(shù)2 | 返回值 |
---|---|---|---|
run |
/ | ()->R |
R |
T.run |
/ | T.()->R |
R |
with |
T |
T.()->R |
R |
T.apply |
/ | T.()->Unit |
T |
T.also |
/ | (T)->Unit |
T |
T.let |
/ | (T)-R |
R |
還是一臉懵逼入问,那我提幾個問題:
run
vsT.run
,差了一個T
,區(qū)別是什么芬失?
區(qū)別在于:run
是普通函數(shù)卷仑,T.run
是擴(kuò)展函數(shù)。run
中的this
是聲明的類對象(頂級函數(shù)除外)麸折,T.run
中的this
是接收者對象;T.()->Unit
vs(T)->Unit
粘昨,或者T.()->R
vs(T)->R
垢啼,T 的位置不同,區(qū)別是什么张肾?
區(qū)別在于:T.()->Unit
中的 T 是接收者類型芭析,(T)->Unit
中的 T 是函數(shù)參數(shù);-
為什么
with
用this
吞瞪,let
用it
馁启?- run、with芍秆、apply 函數(shù)中的參數(shù) block 是 「T 的擴(kuò)展函數(shù)」惯疙,所以采用 this 是擴(kuò)展函數(shù)的接收者對象(receiver)。另外因?yàn)?block 沒有參數(shù)妖啥,所以不存在 it 的定義霉颠。
- also 和 let 參數(shù) block 是 「參數(shù)為 T 的函數(shù)」,所以采用 it 是唯一參數(shù)(argument)荆虱。另外因?yàn)?block 不是擴(kuò)展函數(shù)蒿偎,所以不存在 this 的定義。
lambda 表達(dá)式
lambda 表達(dá)式本質(zhì)上是 「可以作為值傳遞的代碼塊」怀读。在老版本 Java 中诉位,傳遞代碼塊需要使用匿名內(nèi)部類實(shí)現(xiàn),而使用 lambda 表達(dá)式甚至連函數(shù)聲明都不需要菜枷,可以直接傳遞代碼塊作為函數(shù)值苍糠。
當(dāng) lambda 表達(dá)式只有一個參數(shù),可以用
it
關(guān)鍵字來引用唯一的實(shí)參犁跪。
4. 擴(kuò)展的應(yīng)用場景
在這一節(jié)里椿息,我們來介紹一些在 Android 開發(fā)中使用擴(kuò)展的應(yīng)用場景。
4.1 封裝工具 Utils
在 Java 中坷衍,我們習(xí)慣于把通用代碼封裝到工具類中寝优。傳統(tǒng) Java 的工具方法的調(diào)用方式不夠簡單直接,表意性也不夠強(qiáng)枫耳,會讓調(diào)用方忽略 String 和 firstChar() 間的強(qiáng)聯(lián)系乏矾。另外,調(diào)用方也希望省略 StringUtils 類名,讓 firstChar() 看起來更像是 String 內(nèi)部的一個屬性和方法钻心。這些需求對 Kotlin 擴(kuò)展來說都不是問題凄硼。
4.2 解決煩人的 findViewById
在 Android 中,經(jīng)常會調(diào)用 findViewById() 來找到視圖樹中的某一個 View 實(shí)例捷沸,例如:
舊版 SDK:loginButton = (Button) findViewById(R.id.btn_login);
新版 SDK:loginButton = findViewById(R.id.btn_login);
提示: 新版的 SDK 中摊沉,findViewById() 是一個泛型方法,所以你就不再需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換痒给。
public <T extends View> T findViewById(@IdRes int id) { return getWindow().findViewById(id); }
通常说墨,我們會定義一個實(shí)例變量或者局部變量來承載 findViewById() 的返回值,很多時候苍柏,這些變量都只是 “臨時變量” 尼斧,在進(jìn)行事件綁定 / 賦值之后就沒有很大的用處了。如果你面對一些比較復(fù)雜的界面试吁,你甚至需要定義幾十行臨時變量棺棵!
能不能省略這些臨時變量,直接操作R.id.*
呢熄捍?答案是可以的烛恤,我們可以利用 Kotlin 「高階函數(shù) + 擴(kuò)展函數(shù)」。例如:
fun Int.onClick(click: () -> Unit) {
findViewById<View>(this).apply {
setOnClickListener {
click()
}
}
}
此時余耽,我們可以直接使用R.id.*
來綁定點(diǎn)擊事件(R.id* 本質(zhì)就是一個整數(shù)類型):
R.id.btn_login.onClick {
// do something
}
這樣就簡潔多了棒动,我們就不再需要定義一堆臨時變量了。不過宾添,你每次都需要寫R.id
前綴船惨,這似乎也很多余,能不能再省略呢缕陕?確實(shí)可以粱锐,我們需要使用 Kotlin 為 Android 量身定制的 Gradle 插件:kotlin-android-extensions
。
apply plugin : 'kotlin-android-extension'
此時扛邑,我們可以直接用組件的 id 來操作 View 實(shí)例怜浅,例如:
MainActivity .java
btn_login.setOnClickListener{
// do something
}
我們試試反編譯這段代碼,可以看到kotlin-android-extensions
插件自動在 Activity 類中插入了以下代碼:
MainActivity.class
public class MainActivity extends AppCompatActivity {
private HashMap _$_findViewCache;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((Button) this._$_findViewCache(id.btn_login)).setOnClickListener((View.OnClickListener) null.INSTANCE);
}
public View _$_findCachedViewById(int var1) {
if (this._$_findViewCache == null) {
this._$_findViewCache = new HashMap();
}
View var2 = (View) this._$_findViewCache.get(Integer.valueOf(var1));
if (var2 == null) {
var2 = findViewById(var1);
this._$_findViewCache.put(Integer.valueOf(var1), var2);
}
return var2;
}
public void _$_clearFindViewByIdCache() {
if (this._$_findViewCache != null) {
this._$_findViewCache.clear();
}
}
}
可以看到蔬崩,在訪問R.id.*
控件時恶座,先在緩存集合_$_findViewCache
中查找,有就直接返回沥阳,沒有就通過 findViewById() 進(jìn)行查找跨琳,并添加到緩存集合中。
另外還提供了一個_$_clearFindViewByIdCache()方法桐罕,用于在徹底替換界面視圖時清除徹底緩存脉让。在 Fragment#onDestroyView() 中桂敛,會調(diào)用該方法清除緩存,而 Activity 中沒有溅潜。
4.3 簡潔的 LeetCode 題解
在解算法題時术唬,使用擴(kuò)展函數(shù)可以讓代碼更簡潔,表意性更強(qiáng)滚澜。舉個例子粗仓,我們需要交換數(shù)組中的兩個位置上的元素。相對于傳統(tǒng)的寫法设捐,可以看到擴(kuò)展函數(shù)的寫法意思更清楚潦牛。
fun swap(arr: IntArray, from: Int, to: Int) {
...
}
swap(arr,0,1)
fun IntArray.swap(from: Int, toInt) {
...
}
arr.swap(0,1)
5. 總結(jié)
擴(kuò)展可以在不修改類 / 不繼承類的情況下,向一個類添加新函數(shù)或者新屬性挡育,更符合開閉原則。相對于傳統(tǒng) Java 的工具方法的調(diào)用方式更簡單直接朴爬,表意性更強(qiáng)即寒;
擴(kuò)展函數(shù)是定義在類外部的靜態(tài)函數(shù),函數(shù)的第一個參數(shù)是接收者類型召噩,調(diào)用擴(kuò)展時不會創(chuàng)建適配對象或者任何運(yùn)行時的額外消耗母赵。在 Java 中,我們只需要像調(diào)用普通靜態(tài)方法那樣調(diào)用擴(kuò)展即可具滴;
標(biāo)準(zhǔn)庫提供的函數(shù)中凹嘲,run、with构韵、apply 函數(shù)中的參數(shù) block 是「T 的擴(kuò)展函數(shù)」周蹭,所以采用 this 是擴(kuò)展函數(shù)的接收者對象(receiver);also 和 let 參數(shù) block 是「參數(shù)為 T 的函數(shù)」疲恢,所以采用 it 是唯一參數(shù)(argument)凶朗。
參考資料
- 《Kotlin 核心編程》 (第 7 章)—— 水滴技術(shù)團(tuán)隊(duì) 著
- 《Extensions 特性》 —— Kotlin 官方文檔
- 《this 關(guān)鍵字》 —— Kotlin 官方文檔
- 《Android KTX》 —— Android Developers 官方文檔
- 《Kotlin 實(shí)戰(zhàn)》(第 3.3 節(jié))—— [俄] Dmityr Jemerov, Svetlana lsakova 著
創(chuàng)作不易,你的「三連」是丑丑最大的動力显拳,我們下次見棚愤!