簡述: 今天繼續Kotlin原創系列的第十一講,一起來揭開Kotlin屬性代理的漂亮外衣。屬性代理可以說是Kotlin獨有的強大的功能之一,特別是對於框架開發的小夥伴來說非常有用,因為會經常涉及到更改儲存和修改屬性的方式操作,例如Kotlin中的SQL框架Exposed原始碼就大量使用了屬性代理。相信你已經在程式碼也使用了諸如Delegates.observable()、Delegates.notNull()、Delegates.vetoable()或者自定義的屬性代理。也許你還停留用的階段或者對它還有點陌生,不用擔心這篇文章將會基本上解決你所有的疑惑。廢話不多說,直接來看一波章節導圖:
一、屬性代理的基本定義
- 1、基本定義
屬性代理是藉助於代理設計模式,把這個模式應用於一個屬性時,它可以將訪問器的邏輯代理給一個輔助物件。
可以簡單理解為屬性的setter、getter訪問器內部實現是交給一個代理物件來實現,相當於使用一個代理物件來替換了原來簡單屬性欄位讀寫過程,而暴露外部屬性操作還是不變的,照樣是屬性賦值和讀取,只是setter、getter內部具體實現變了。
- 2、基本語法格式
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方法.
- 3、by關鍵字
by關鍵字實際上就是一個屬性代理運算子過載的符號,任何一個具備屬性代理規則的類,都可以使用by關鍵字對屬性進行代理。
二、常見屬性代理基本使用
屬性代理是Kotlin獨有的特性,我們自己去自定義屬性代理,當然Kotlin還提供了幾種常見的屬性代理實現。例如:Delegates.notNull(), Delegates.observable(), Delegates.vetoable()
- 1、Delegates.notNull()的基本使用
Delegate.notNull()代理主要用於可以不在構造器初始化時候初始化而是可以延遲到之後再初始化這個var修飾的屬性,它和lateinit功能類似,但是也有一些不同,不過它們都需要注意的一點是屬性的生命週期,開發者要做到可控,也就是一定要確保屬性初始化是在屬性使用之前,否則會丟擲一個IllegalStateException.
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你忘記了初始化,那麼恭喜你在執行時你就會拿到空指標異常。
問題來了: 大背景說完了那麼問題也就來了,相比Java,Kotlin屬性定義時多出了額外的屬性初始化的工作。但是可能某個屬性的值在開始定義的時候你並不知道,而是需要執行到後面的邏輯才能拿到。這時候解決方式大概有這麼幾種:
方式A: 開始初始化的時給屬性賦值個預設值
方式B: 使用Delegates.notNull()屬性代理
方式C: 使用lateinit修飾屬性
以上三種方式有侷限性,方式A就是很暴力直接賦預設值,對於基本型別還可以,但是對於引用型別的屬性,賦值一個預設引用型別物件就感覺不太合適了。方式B適用於基本資料型別和引用型別,但是存在屬性初始化必須在屬性使用之前為前提條件。方式C僅僅適用於引用型別,但是也存在屬性初始化必須在屬性使用之前為前提條件。
優缺點分析:
屬性使用方式 | 優點 | 缺點 |
---|---|---|
方式A(初始化賦預設值) | 使用簡單,不存在屬性初始化必須在屬性使用之前的問題 | 僅僅適用於基本資料型別 |
方式B(Delegates.notNull()屬性代理) | 適用於基本資料型別和引用型別 | 1、存在屬性初始化必須在屬性使用之前的問題; 2、不支援外部注入工具將它直接注入到Java欄位中 |
方式C(lateinit修飾屬性) | 僅適用於引用型別 | 1、存在屬性初始化必須在屬性使用之前的問題; 2、不支援基本資料型別 |
使用建議: 如果能對屬性生命週期做很好把控的話,且不存在注入到外部欄位需求,建議使用方式B;此外還有一個不錯建議就是方式A+方式C組合,或者方式A+方式B組合。具體看實際場景需求。
- 2、Delegates.observable()的基本使用
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
複製程式碼
- 3、Delegates.vetoable()的基本使用
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標準庫原始碼中常見的屬性代理包結構。
-
1、原始碼包結構
-
2、關係類圖
Delegates: 是一個代理單例物件,裡面有notNull、observable、vetoable靜態方法,每個方法返回不同的型別代理物件
NotNullVar: notNull方法返回代理物件的類
ObserableProperty: observable、vetoable方法返回代理物件的類
ReadOnlyProperty: 只讀屬性代理物件的通用介面
ReadWriteProperty: 讀寫屬性代理物件的通用介面
- 3、Delegates.notNull()原始碼分析
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加了一層判空的代理實現。
- 4、Delegates.observable()原始碼分析
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)
}
複製程式碼
- 5、Delegates.vetoable()原始碼分析 vetoable()是一個方法,返回的是一個ObservableProperty屬性代理例項;通過上面原始碼就可以發現,在setValue方法中執行真正賦值之前,會有一個判斷邏輯,根據beforeChange回撥方法返回的Boolean決定是否繼續執行下面的真正賦值操作。如果beforChange()返回false就終止此次賦值,那麼observable也不能得到回撥,如果返回true則會繼續此次賦值操作,並執行observable的回撥。
四、屬性代理背後的原理和原始碼反編譯分析
如果說第三節是揭開屬性代理第一層外衣,那麼第四節將是揭開最後一層外衣了,你會看到屬性代理真正背後的原理,看完你會發現其實挺簡單的。不多說先上一個簡單例子
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();
}
}
複製程式碼
分析過程:
- 1、首先, Teacher類的name和age屬性會自動生成對應的setter,getter方法,並且會自動生成對應的name$delegate、age$delegate委託物件,如程式碼中標識的關鍵點二。
- 2、然後,$$delegatedProperties的KProperty陣列中會儲存通過Kotlin反射出當前Teacher類中的中name,age屬性,反射出來每個屬性單獨對應儲存在KProperty陣列中。
- 2、然後,在對應屬性setter,getter方法中是把具體的實現委託給對應的name$delegate、age$delegate物件的setValue、getValue方法來實現的,如程式碼中標識的關鍵點三。
- 3、最後在delegate物件中的setValue和getValue方法中的傳入對應反射出來的屬性以及相應的值。
五、自己動手實現屬性代理
有以上的介紹,自己寫個自定義的屬性代理應該很簡單了吧。實現一個簡單的屬性代理最基本架子就是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,歡迎加入我們~~~