??Kotlin 能夠擴展一個類的新功能而無需繼承該類或者使用像裝飾者這樣的設計模式。 這通過叫做 擴展 的特殊聲明完成。 例如,你可以為一個你不能修改的、來自第三方庫中的類編寫一個新的函數。 這個新增的函數就像那個原始類本來就有的函數一樣,可以用普通的方法調用。 這種機制稱為 擴展函數 。此外,也有 擴展屬性 , 允許你為一個已經存在的類添加新的屬性。
前言
??作為安卓開發,我們常常碰到這樣的場景,需要把以dp為單位的值轉化為以px為單位。這時候我們常會寫一個Utils類,比如說
public class Utils {
public static float dp2px(int dpValue) {
return (0.5f + dpValue * Resources.getSystem().getDisplayMetrics().density);
}
}
??在代碼中直接調用 Utils.dp2px(100) 來使用,
val dp2px = Utils.dp2px(100)
??如果用kotlin擴展函數的方式來實現,會是怎么調用呢?
val dp2px = 100.dp2px()
??是不是很驚訝,100作為一個Int,竟然直接調用了一個dp2px方法,如果你去源碼里找找,其實是沒有個方法的。我們沒有動源碼,而是使用拓展函數的方式為Int增加了一個方法。
fun Int.dp2px(): Float {
return (0.5f + this * Resources.getSystem().displayMetrics.density)
}
擴展函數
??我們再來舉個??,有一個Person類如下
class Person(val name: String) {
fun eat() {
Log.i(name, "I'm going to eat")
}
fun sleep() {
Log.i(name, "I'm going to sleep")
}
}
??它有兩個方法,一個是 eat 、一個是 sleep,調用的話就分別打印相應的Log。我們現在不想動Person類,但是又想給他增加一個新的方法,怎么做呢。我們可以新建一個文件 PersonExtensions.kt,再通過一下代碼實現,就可以為 Person類新增一個 drink 方法啦。
fun Person.drink() {
Log.i("Person", "${this.name}: I'm going to drink")
}
??聲明一個擴展函數,我們需要用一個 接收者類型 也就是被擴展的類型來作為他的前綴。上面我們就是以 Person 作為一個擴展函數的接收類型,為其拓展來 drink 方法。我們在其方法中調用了 this ,這個 this 指的就是調用這個拓展方法的當前 Person 對象。
??擴展函數調用的話也和普通的方法相同。但是你會發現IDE顯示的方法顏色有點不一樣。
??由此也可以看出普通的方法和我們的拓展函數是不同的。下面我們來看看擴展函數的實際實現。
??在 Android Studio 中,我們可以查看 kotlin 文件的字節碼,然后再 Decompile 為 Java 代碼。上面我們為 Person 擴展函數轉為Java代碼后如下。
@Metadata(
mv = {1, 1, 15},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\f\n\u0000\n\u0002\u0010\u0002\n\u0002\u0018\u0002\n\u0000\u001a\n\u0010\u0000\u001a\u00020\u0001*\u00020\u0002¨\u0006\u0003"},
d2 = {"cook", "", "Lcom/chaochaowu/kotlinextension/Person;", "app_debug"}
)
public final class PersonExtensionsKt {
public static final void cook(@NotNull Person $this$cook) {
Intrinsics.checkParameterIsNotNull($this$cook, "$this$cook");
Log.i("Person", $this$cook.getName() + ": I'm going to cook");
}
}
??妹想到啊,它原來是一個 static final 聲明的靜態方法,它的入參是一個 Person 類型,也就是我們之前的接收類型。那在Java代碼中能不呢調用呢?
PersonExtensionsKt.cook(new Person("Bob"));
??竟然也沒有報錯!由此可見,所謂擴展函數并不是真正的在類中增加了一個方法,而是通過外部文件的靜態方法來實現,其實就是和Utils類一個道理。
??因為將一個 Person 作為入參傳入了方法中,所以我們也就可以在方法內對這個 Person 對象進行操作,這也就是在擴展方法中我們可以使用 this 來訪問 Person 屬性的原因。
??再來看一個特殊的例子。
val s: String? = null
s.isNullOrEmpty()
??上面的代碼中,s的值為null,我們用null去調用了一個方法,這會不會報錯呢?按照以前的經驗,一個null去調用一個方法,必然會報空指針的異常,但是上面的代碼卻是不會崩的。為什么哩?
??其實 isNullOrEmpty 是 CharSequence? 的一個擴展方法,我們可以看一下它的源碼。
@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}
return this == null || this.length == 0
}
??contract這個契約方法這邊我們不需要注意,不影響。主要是看 return this == null || this.length == 0 這句話。它先是判斷了 this 是否為空,然后再判斷this 的長度。根據我們上面講的擴展函數的本質,我們可以很好的理解,為什么null可以調用這個方法的原因。因為上面的代碼轉為 Java 代碼后是這樣子的。
public static final boolean isNullOrEmpty(@Nullable CharSequence $this$isNullOrEmpty) {
int $i$f$isNullOrEmpty = 0;
return $this$isNullOrEmpty == null || $this$isNullOrEmpty.length() == 0;
}
??我們在用null調用這個擴展方法時,其實是將null作為一個參數傳入這個方法中,先判斷參數是否為null,再進行下一步判斷,這當然不會崩潰。
??擴展不能真正的修改他們所擴展的類。通過定義一個擴展,你并沒有在一個類中插入新成員, 僅僅是可以通過該類型的變量用點表達式去調用這個新函數,并將自身作為參數傳入。
擴展屬性
??擴展屬性和擴展函數類似,再舉上面Person 的例子,我們對 Person 類稍作修改,為其增加 birthdayYear 字段,表示其出生的年份。
class Person(val name: String, val birthdayYear: Int) {
fun eat() {
Log.i(name, "I'm going to eat")
}
fun sleep() {
Log.i(name, "I'm going to sleep")
}
}
??我們現在要為 Person 增加年齡 age 的屬性,但是不改 Person 類,怎么實現呢。和擴展函數一樣,在其他文件中聲明如下。
const val currentYear = 2019
val Person.age: Int
get() = currentYear - this.birthdayYear
??我們通過當前年份減去生日年份計算出 Person 的年齡。可以看到,age 是一個屬性,而不是方法。這樣我們就為 Person 增加了一個擴展屬性。可以看看它轉化為 Java 代碼后的樣子,和擴展函數沒啥區別。
@Metadata(
mv = {1, 1, 15},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\u0010\n\u0000\n\u0002\u0010\b\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\"\u000e\u0010\u0000\u001a\u00020\u0001X\u0086T¢\u0006\u0002\n\u0000\"\u0015\u0010\u0002\u001a\u00020\u0001*\u00020\u00038F¢\u0006\u0006\u001a\u0004\b\u0004\u0010\u0005¨\u0006\u0006"},
d2 = {"currentYear", "", "age", "Lcom/chaochaowu/kotlinextension/Person;", "getAge", "(Lcom/chaochaowu/kotlinextension/Person;)I", "app_debug"}
)
public final class PersonExtensionsKt {
public static final int currentYear = 2019;
public static final int getAge(@NotNull Person $this$age) {
Intrinsics.checkParameterIsNotNull($this$age, "$this$age");
return 2019 - $this$age.getBirthdayYear();
}
}
??上面我們聲明的是一個 val,當然也可以聲明一個 var,不過 var 的話需要同時定義其 get 和 set 方法。
??由于擴展沒有實際的將成員插入類中,因此對擴展屬性來說幕后字段是無效的。這就是為什么擴展屬性不能有初始化器。他們的行為只能由顯式提供的 getters/setters 定義。
總結
??在 Java 中,我們要擴展一個類時,常常是繼承該類或者用裝飾者模式類似的設計模式來實現,Kotlin 擴展函數和擴展屬性為這種需求提供了一種新思路,并且也可以作為 Utils 類的另外一種選擇,值得一試。