簡述: 今天繼續Kotlin原創系列的第十一講,一塊兒來揭開Kotlin屬性代理的漂亮外衣。屬性代理能夠說是Kotlin獨有的強大的功能之一,特別是對於框架開發的小夥伴來講很是有用,由於會常常涉及到更改存儲和修改屬性的方式操做,例如Kotlin中的SQL框架Exposed源碼就大量使用了屬性代理。相信你已經在代碼也使用了諸如Delegates.observable()、Delegates.notNull()、Delegates.vetoable()或者自定義的屬性代理。也許你還停留用的階段或者對它還有點陌生,不用擔憂這篇文章將會基本上解決你全部的疑惑。廢話很少說,直接來看一波章節導圖:java
屬性代理是藉助於代理設計模式,把這個模式應用於一個屬性時,它能夠將訪問器的邏輯代理給一個輔助對象。git
能夠簡單理解爲屬性的setter、getter訪問器內部實現是交給一個代理對象來實現,至關於使用一個代理對象來替換了原來簡單屬性字段讀寫過程,而暴露外部屬性操做仍是不變的,照樣是屬性賦值和讀取,只是setter、getter內部具體實現變了。github
class Student{
var name: String by Delegate()
}
class Delegate{
operator fun <T> getValue(thisRef: Any?, property: KProperty<*>): T{
...
}
operator fun <T> setValue(thisRef: Any?, property: KProperty<*>, value: T){
...
}
}
複製代碼
屬性name將它訪問器的邏輯委託給了Delegate對象,經過by關鍵字對錶達式Delegate()求值獲取這個對象。任何符合屬性代理規則均可以使用by關鍵字。屬性代理類必需要遵循getValue(),setValue()方法約定,getValue、setValue方法能夠是普通方法也能夠是擴展方法,而且是方法是支持運算符重載。若是是val修飾的屬性只須要具有getValue()方法便可。設計模式
屬性代理基本流程就是代理類中的getValue()方法包含屬性getter訪問器的邏輯實現,setValue()方法包含了屬性setter訪問器的邏輯實現。當屬性name執行賦值操做時,會觸發屬性setter訪問器,而後在setter訪問器內部調用delegate對象的setValue()方法;執行讀取屬性name操做時,會在getter訪問器中調用delegate對象的getValue方法.數組
by關鍵字實際上就是一個屬性代理運算符重載的符號,任何一個具有屬性代理規則的類,均可以使用by關鍵字對屬性進行代理。安全
屬性代理是Kotlin獨有的特性,咱們本身去自定義屬性代理,固然Kotlin還提供了幾種常見的屬性代理實現。例如:Delegates.notNull(), Delegates.observable(), Delegates.vetoable()bash
Delegate.notNull()代理主要用於能夠不在構造器初始化時候初始化而是能夠延遲到以後再初始化這個var修飾的屬性,它和lateinit功能相似,可是也有一些不一樣,不過它們都須要注意的一點是屬性的生命週期,開發者要作到可控,也就是必定要確保屬性初始化是在屬性使用以前,不然會拋出一個IllegalStateException.app
package com.mikyou.kotlin.delegate
import kotlin.properties.Delegates
class Teacher {
var name: String by Delegates.notNull()
}
fun main(args: Array<String>) {
val teacher = Teacher().apply { name = "Mikyou" }
println(teacher.name)
}
複製代碼
可能有的人並無看到notNull()有什麼大的用處,先說下大背景吧就會明白它的用處在哪了?框架
大背景: 在Kotlin開發中與Java不一樣的是在定義和聲明屬性時必需要作好初始化工做,不然編譯器會提示報錯的,不像Java只要定義就OK了,管你是否初始化呢。我解釋下這也是Kotlin優於Java地方之一,沒錯就是空類型安全,就是Kotlin在寫代碼時就讓你明確一個屬性是否初始化,不至於把這樣的不明肯定義拋到後面運行時。若是在Java你忘記了初始化,那麼恭喜你在運行時你就會拿到空指針異常。ide
問題來了: 大背景說完了那麼問題也就來了,相比Java,Kotlin屬性定義時多出了額外的屬性初始化的工做。可是可能某個屬性的值在開始定義的時候你並不知道,而是須要執行到後面的邏輯才能拿到。這時候解決方式大概有這麼幾種:
方式A: 開始初始化的時給屬性賦值個默認值
方式B: 使用Delegates.notNull()屬性代理
方式C: 使用lateinit修飾屬性
以上三種方式有侷限性,方式A就是很暴力直接賦默認值,對於基本類型還能夠,可是對於引用類型的屬性,賦值一個默認引用類型對象就感受不太合適了。方式B適用於基本數據類型和引用類型,可是存在屬性初始化必須在屬性使用以前爲前提條件。方式C僅僅適用於引用類型,可是也存在屬性初始化必須在屬性使用以前爲前提條件。
優缺點分析:
屬性使用方式 | 優勢 | 缺點 |
---|---|---|
方式A(初始化賦默認值) | 使用簡單,不存在屬性初始化必須在屬性使用以前的問題 | 僅僅適用於基本數據類型 |
方式B(Delegates.notNull()屬性代理) | 適用於基本數據類型和引用類型 | 一、存在屬性初始化必須在屬性使用以前的問題; 二、不支持外部注入工具將它直接注入到Java字段中 |
方式C(lateinit修飾屬性) | 僅適用於引用類型 | 一、存在屬性初始化必須在屬性使用以前的問題; 二、不支持基本數據類型 |
使用建議: 若是能對屬性生命週期作很好把控的話,且不存在注入到外部字段需求,建議使用方式B;此外還有一個不錯建議就是方式A+方式C組合,或者方式A+方式B組合。具體看實際場景需求。
Delegates.observable()主要用於監控屬性值發生變動,相似於一個觀察者。當屬性值被修改後會往外部拋出一個變動的回調。它須要傳入兩個參數,一個是initValue初始化的值,另外一個就是回調lamba, 回調出property, oldValue, newValue三個參數。
package com.mikyou.kotlin.delegate
import kotlin.properties.Delegates
class Person{
var address: String by Delegates.observable(initialValue = "NanJing", onChange = {property, oldValue, newValue ->
println("property: ${property.name} oldValue: $oldValue newValue: $newValue")
})
}
fun main(args: Array<String>) {
val person = Person().apply { address = "ShangHai" }
person.address = "BeiJing"
person.address = "ShenZhen"
person.address = "GuangZhou"
}
複製代碼
運行結果:
property: address oldValue: NanJing newValue: ShangHai
property: address oldValue: ShangHai newValue: BeiJing
property: address oldValue: BeiJing newValue: ShenZhen
property: address oldValue: ShenZhen newValue: GuangZhou
Process finished with exit code 0
複製代碼
Delegates.vetoable()代理主要用於監控屬性值發生變動,相似於一個觀察者,當屬性值被修改後會往外部拋出一個變動的回調。它須要傳入兩個參數,一個是initValue初始化的值,另外一個就是回調lamba, 回調出property, oldValue, newValue三個參數。與observable不一樣的是這個回調會返回一個Boolean值,來決定這次屬性值是否執行修改。
package com.mikyou.kotlin.delegate
import kotlin.properties.Delegates
class Person{
var address: String by Delegates.vetoable(initialValue = "NanJing", onChange = {property, oldValue, newValue ->
println("property: ${property.name} oldValue: $oldValue newValue: $newValue")
return@vetoable newValue == "BeiJing"
})
}
fun main(args: Array<String>) {
val person = Person().apply { address = "NanJing" }
person.address = "BeiJing"
person.address = "ShangHai"
person.address = "GuangZhou"
println("address is ${person.address}")
}
複製代碼
以上咱們介紹了常見的屬性代理基本使用,若是僅僅停留在使用的階段,確實有點low了, 那麼讓咱們一塊兒先來揭開它們的第一層外衣。先來看波Kotlin標準庫源碼中常見的屬性代理包結構。
一、源碼包結構
二、關係類圖
Delegates: 是一個代理單例對象,裏面有notNull、observable、vetoable靜態方法,每一個方法返回不一樣的類型代理對象
NotNullVar: notNull方法返回代理對象的類
ObserableProperty: observable、vetoable方法返回代理對象的類
ReadOnlyProperty: 只讀屬性代理對象的通用接口
ReadWriteProperty: 讀寫屬性代理對象的通用接口
notNull()首先是一個方法,返回的是一個NotNullVar屬性代理實例;那麼它處理核心邏輯就是NotNullVar內部的setValue和getValue方法,一塊兒來瞅一眼。
public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
}
public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
複製代碼
經過源碼能夠看到一旦getValue中的value是爲null,那麼就會拋出一個IllegalStateException,也就是在使用該屬性以前沒有作初始化。實際上能夠理解在訪問器getter加了一層判空的代理實現。
observable()是一個方法,返回的是一個ObservableProperty屬性代理實例;那它是怎麼作到在屬性值發生變化通知到外部的呢,其實很簡單,首先在內部保留一個oldValue用於存儲上一次的值,而後就在ObservableProperty類setValue方法執行真正賦值以後再向外部拋出了一個afterChange的回調,而且把oldValue,newValue,property回調到外部,最終利用onChange方法回調到最外層。
public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
val oldValue = this.value
if (!beforeChange(property, oldValue, value)) {
return
}
this.value = value
afterChange(property, oldValue, value)
}
public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
}
複製代碼
若是說第三節是揭開屬性代理第一層外衣,那麼第四節將是揭開最後一層外衣了,你會看到屬性代理真正背後的原理,看完你會發現其實挺簡單的。很少說先上一個簡單例子
class Teacher {
var name: String by Delegates.notNull()
var age: Int by Delegates.notNull()
}
複製代碼
實際上,以上那行代碼是經歷了兩個步驟:
class Teacher {
private val delegateString: ReadWriteProperty<Teacher, String> = Delegates.notNull()
private val delegateInt: ReadWriteProperty<Teacher, Int> = Delegates.notNull()
var name: String by delegateString
var age: Int by delegateInt
}
複製代碼
Kotlin反編譯後Java源碼
public final class Teacher {
// $FF: synthetic field
//關鍵點一
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Teacher.class), "name", "getName()Ljava/lang/String;")), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Teacher.class), "age", "getAge()I"))};
//關鍵點二
@NotNull
private final ReadWriteProperty name$delegate;
@NotNull
private final ReadWriteProperty age$delegate;
//關鍵點三
@NotNull
public final String getName() {
return (String)this.name$delegate.getValue(this, $$delegatedProperties[0]);
}
public final void setName(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.name$delegate.setValue(this, $$delegatedProperties[0], var1);
}
public final int getAge() {
return ((Number)this.age$delegate.getValue(this, $$delegatedProperties[1])).intValue();
}
public final void setAge(int var1) {
this.age$delegate.setValue(this, $$delegatedProperties[1], var1);
}
public Teacher() {
this.name$delegate = Delegates.INSTANCE.notNull();
this.age$delegate = Delegates.INSTANCE.notNull();
}
}
複製代碼
分析過程:
有以上的介紹,本身寫個自定義的屬性代理應該很簡單了吧。實現一個簡單的屬性代理最基本架子就是setValue,getValue方法且無需實現任何的接口。
在Android中SharedPreferences實際上就是個很好場景,由於它涉及到了屬性存儲和讀取。自定義屬性代理實現Android中SharedPreferences能夠直接實現自帶的ReadWriteProperty接口,固然也能夠本身去寫一個類而後去定義相應的setValue方法和getValue方法。
class PreferenceDelegate<T>(private val context: Context, private val name: String, private val default: T, private val prefName: String = "default")
: ReadWriteProperty<Any?, T> {
private val prefs: SharedPreferences by lazy {
context.getSharedPreferences(prefName, Context.MODE_PRIVATE)
}
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("setValue from delegate")
return getPreference(key = name)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
println("setValue from delegate")
putPreference(key = name, value = value)
}
private fun getPreference(key: String): T {
return when (default) {
is String -> prefs.getString(key, default)
is Long -> prefs.getLong(key, default)
is Boolean -> prefs.getBoolean(key, default)
is Float -> prefs.getFloat(key, default)
is Int -> prefs.getInt(key, default)
else -> throw IllegalArgumentException("Unknown Type.")
} as T
}
private fun putPreference(key: String, value: T) = with(prefs.edit()) {
when (value) {
is String -> putString(key, value)
is Long -> putLong(key, value)
is Boolean -> putBoolean(key, value)
is Float -> putFloat(key, value)
is Int -> putInt(key, value)
else -> throw IllegalArgumentException("Unknown Type.")
}
}.apply()
}
複製代碼
到這裏屬性代理的內容就結束了,有沒有以爲Kotlin語言糖設計仍是很巧妙的。雖然不少人抵觸語法糖,但不能否認的是它給咱們開發在效率上帶來了很大的提高。有時候咱們更多地是須要透過語法糖外衣,看到其背後的原理,弄清整個語法糖設計思路和技巧,以一個全局眼光去看待它,就會以爲它也就那麼回事。最後,感謝一波bennyHuo大佬,我是先看到他sharedPreferences的屬性擴展例子,感受很不錯,而後決定去深刻探究一下屬性擴展,這下應該對Kotlin屬性擴展有了比較深的認識了。
歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~