簡述:
今天繼續(xù)Kotlin原創(chuàng)系列的第十一講,一起來揭開Kotlin屬性代理的漂亮外衣。屬性代理可以說是Kotlin獨有的強大的功能之一,特別是對于框架開發(fā)的小伙伴來說非常有用屈梁,因為會經(jīng)常涉及到更改存儲和修改屬性的方式操作,例如Kotlin中的SQL框架Exposed源碼就大量使用了屬性代理继控。相信你已經(jīng)在代碼也使用了諸如Delegates.observable()旬迹、Delegates.notNull()、Delegates.vetoable()或者自定義的屬性代理液斜。也許你還停留用的階段或者對它還有點陌生累贤,不用擔(dān)心這篇文章將會基本上解決你所有的疑惑。廢話不多說少漆,直接來看一波章節(jié)導(dǎo)圖:
一臼膏、屬性代理的基本定義
- 1、基本定義
屬性代理是借助于代理設(shè)計模式示损,把這個模式應(yīng)用于一個屬性時渗磅,它可以將訪問器的邏輯代理給一個輔助對象。
可以簡單理解為屬性的setter检访、getter訪問器內(nèi)部實現(xiàn)是交給一個代理對象來實現(xiàn)始鱼,相當(dāng)于使用一個代理對象來替換了原來簡單屬性字段讀寫過程,而暴露外部屬性操作還是不變的脆贵,照樣是屬性賦值和讀取医清,只是setter、getter內(nèi)部具體實現(xiàn)變了卖氨。
- 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關(guān)鍵字對表達(dá)式Delegate()求值獲取這個對象筒捺。任何符合屬性代理規(guī)則都可以使用by關(guān)鍵字柏腻。屬性代理類必須要遵循getValue(),setValue()方法約定,getValue系吭、setValue方法可以是普通方法也可以是擴展方法五嫂,并且是方法是支持運算符重載贴妻。如果是val修飾的屬性只需要具備getValue()方法即可帐姻。
屬性代理基本流程就是代理類中的getValue()方法包含屬性getter訪問器的邏輯實現(xiàn),setValue()方法包含了屬性setter訪問器的邏輯實現(xiàn)鞭达。當(dāng)屬性name執(zhí)行賦值操作時蟆盹,會觸發(fā)屬性setter訪問器孩灯,然后在setter訪問器內(nèi)部調(diào)用delegate對象的setValue()方法;執(zhí)行讀取屬性name操作時逾滥,會在getter訪問器中調(diào)用delegate對象的getValue方法.
- 3峰档、by關(guān)鍵字
by關(guān)鍵字實際上就是一個屬性代理運算符重載的符號败匹,任何一個具備屬性代理規(guī)則的類,都可以使用by關(guān)鍵字對屬性進(jìn)行代理讥巡。
二掀亩、常見屬性代理基本使用
屬性代理是Kotlin獨有的特性,我們自己去自定義屬性代理欢顷,當(dāng)然Kotlin還提供了幾種常見的屬性代理實現(xiàn)槽棍。例如:Delegates.notNull(), Delegates.observable(), Delegates.vetoable()
- 1、Delegates.notNull()的基本使用
Delegate.notNull()代理主要用于可以不在構(gòu)造器初始化時候初始化而是可以延遲到之后再初始化這個var修飾的屬性,它和lateinit功能類似抬驴,但是也有一些不同炼七,不過它們都需要注意的一點是屬性的生命周期,開發(fā)者要做到可控布持,也就是一定要確保屬性初始化是在屬性使用之前豌拙,否則會拋出一個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開發(fā)中與Java不同的是在定義和聲明屬性時必須要做好初始化工作按傅,否則編譯器會提示報錯的,不像Java只要定義就OK了胧卤,管你是否初始化呢唯绍。我解釋下這也是Kotlin優(yōu)于Java地方之一,沒錯就是空類型安全枝誊,就是Kotlin在寫代碼時就讓你明確一個屬性是否初始化况芒,不至于把這樣的不明確定義拋到后面運行時。如果在Java你忘記了初始化叶撒,那么恭喜你在運行時你就會拿到空指針異常牛柒。
問題來了: 大背景說完了那么問題也就來了,相比Java痊乾,Kotlin屬性定義時多出了額外的屬性初始化的工作。但是可能某個屬性的值在開始定義的時候你并不知道椭更,而是需要執(zhí)行到后面的邏輯才能拿到哪审。這時候解決方式大概有這么幾種:
方式A: 開始初始化的時給屬性賦值個默認(rèn)值
方式B: 使用Delegates.notNull()屬性代理
方式C: 使用lateinit修飾屬性
以上三種方式有局限性,方式A就是很暴力直接賦默認(rèn)值虑瀑,對于基本類型還可以湿滓,但是對于引用類型的屬性,賦值一個默認(rèn)引用類型對象就感覺不太合適了舌狗。方式B適用于基本數(shù)據(jù)類型和引用類型叽奥,但是存在屬性初始化必須在屬性使用之前為前提條件。方式C僅僅適用于引用類型痛侍,但是也存在屬性初始化必須在屬性使用之前為前提條件朝氓。
優(yōu)缺點分析:
屬性使用方式 | 優(yōu)點 | 缺點 |
---|---|---|
方式A(初始化賦默認(rèn)值) | 使用簡單,不存在屬性初始化必須在屬性使用之前的問題 | 僅僅適用于基本數(shù)據(jù)類型 |
方式B(Delegates.notNull()屬性代理) | 適用于基本數(shù)據(jù)類型和引用類型 | 1、存在屬性初始化必須在屬性使用之前的問題赵哲; 2待德、不支持外部注入工具將它直接注入到Java字段中 |
方式C(lateinit修飾屬性) | 僅適用于引用類型 | 1、存在屬性初始化必須在屬性使用之前的問題枫夺; 2将宪、不支持基本數(shù)據(jù)類型 |
使用建議: 如果能對屬性生命周期做很好把控的話,且不存在注入到外部字段需求橡庞,建議使用方式B较坛;此外還有一個不錯建議就是方式A+方式C組合,或者方式A+方式B組合扒最。具體看實際場景需求丑勤。
- 2、Delegates.observable()的基本使用
Delegates.observable()主要用于監(jiān)控屬性值發(fā)生變更扼倘,類似于一個觀察者确封。當(dāng)屬性值被修改后會往外部拋出一個變更的回調(diào)。它需要傳入兩個參數(shù)再菊,一個是initValue初始化的值爪喘,另一個就是回調(diào)lamba, 回調(diào)出property, oldValue, newValue三個參數(shù)。
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"
}
運行結(jié)果:
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()代理主要用于監(jiān)控屬性值發(fā)生變更秉剑,類似于一個觀察者,當(dāng)屬性值被修改后會往外部拋出一個變更的回調(diào)稠诲。它需要傳入兩個參數(shù)侦鹏,一個是initValue初始化的值,另一個就是回調(diào)lamba, 回調(diào)出property, oldValue, newValue三個參數(shù)臀叙。與observable不同的是這個回調(diào)會返回一個Boolean值略水,來決定此次屬性值是否執(zhí)行修改。
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標(biāo)準(zhǔn)庫源碼中常見的屬性代理包結(jié)構(gòu)跨释。
-
1、源碼包結(jié)構(gòu)
2厌处、關(guān)系類圖
Delegates: 是一個代理單例對象鳖谈,里面有notNull、observable阔涉、vetoable靜態(tài)方法缆娃,每個方法返回不同的類型代理對象
NotNullVar: notNull方法返回代理對象的類
ObserableProperty: observable捷绒、vetoable方法返回代理對象的類
ReadOnlyProperty: 只讀屬性代理對象的通用接口
ReadWriteProperty: 讀寫屬性代理對象的通用接口
- 3、Delegates.notNull()源碼分析
notNull()首先是一個方法龄恋,返回的是一個NotNullVar屬性代理實例疙驾;那么它處理核心邏輯就是NotNullVar內(nèi)部的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加了一層判空的代理實現(xiàn)显押。
- 4扳肛、Delegates.observable()源碼分析
observable()是一個方法,返回的是一個ObservableProperty屬性代理實例乘碑;那它是怎么做到在屬性值發(fā)生變化通知到外部的呢挖息,其實很簡單,首先在內(nèi)部保留一個oldValue用于存儲上一次的值兽肤,然后就在ObservableProperty類setValue方法執(zhí)行真正賦值之后再向外部拋出了一個afterChange的回調(diào)套腹,并且把oldValue,newValue,property回調(diào)到外部,最終利用onChange方法回調(diào)到最外層资铡。
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屬性代理實例笤休;通過上面源碼就可以發(fā)現(xiàn)尖飞,在setValue方法中執(zhí)行真正賦值之前,會有一個判斷邏輯店雅,根據(jù)beforeChange回調(diào)方法返回的Boolean決定是否繼續(xù)執(zhí)行下面的真正賦值操作政基。如果beforChange()返回false就終止此次賦值,那么observable也不能得到回調(diào)闹啦,如果返回true則會繼續(xù)此次賦值操作沮明,并執(zhí)行observable的回調(diào)。
四窍奋、屬性代理背后的原理和源碼反編譯分析
如果說第三節(jié)是揭開屬性代理第一層外衣珊擂,那么第四節(jié)將是揭開最后一層外衣了,你會看到屬性代理真正背后的原理费变,看完你會發(fā)現(xiàn)其實挺簡單的。不多說先上一個簡單例子
class Teacher {
var name: String by Delegates.notNull()
var age: Int by Delegates.notNull()
}
實際上圣贸,以上那行代碼是經(jīng)歷了兩個步驟:
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
//關(guān)鍵點一
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"))};
//關(guān)鍵點二
@NotNull
private final ReadWriteProperty name$delegate;
@NotNull
private final ReadWriteProperty age$delegate;
//關(guān)鍵點三
@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屬性會自動生成對應(yīng)的setter,getter方法,并且會自動生成對應(yīng)的name$delegate吁峻、age$delegate委托對象滑负,如代碼中標(biāo)識的關(guān)鍵點二在张。
- 2、然后矮慕,$$delegatedProperties的KProperty數(shù)組中會保存通過Kotlin反射出當(dāng)前Teacher類中的中name帮匾,age屬性,反射出來每個屬性單獨對應(yīng)保存在KProperty數(shù)組中痴鳄。
- 2瘟斜、然后,在對應(yīng)屬性setter,getter方法中是把具體的實現(xiàn)委托給對應(yīng)的name$delegate痪寻、age$delegate對象的setValue螺句、getValue方法來實現(xiàn)的,如代碼中標(biāo)識的關(guān)鍵點三橡类。
- 3蛇尚、最后在delegate對象中的setValue和getValue方法中的傳入對應(yīng)反射出來的屬性以及相應(yīng)的值。
五顾画、自己動手實現(xiàn)屬性代理
有以上的介紹取劫,自己寫個自定義的屬性代理應(yīng)該很簡單了吧。實現(xiàn)一個簡單的屬性代理最基本架子就是setValue,getValue方法且無需實現(xiàn)任何的接口研侣。
在Android中SharedPreferences實際上就是個很好場景谱邪,因為它涉及到了屬性存儲和讀取。自定義屬性代理實現(xiàn)Android中SharedPreferences可以直接實現(xiàn)自帶的ReadWriteProperty接口义辕,當(dāng)然也可以自己去寫一個類然后去定義相應(yīng)的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()
}
六、結(jié)語
到這里屬性代理的內(nèi)容就結(jié)束了灌砖,有沒有覺得Kotlin語言糖設(shè)計還是很巧妙的璧函。雖然很多人抵觸語法糖,但不可否認(rèn)的是它給我們開發(fā)在效率上帶來了很大的提升基显。有時候我們更多地是需要透過語法糖外衣蘸吓,看到其背后的原理,弄清整個語法糖設(shè)計思路和技巧撩幽,以一個全局眼光去看待它库继,就會覺得它也就那么回事。最后窜醉,感謝一波bennyHuo大佬宪萄,我是先看到他sharedPreferences的屬性擴展例子,感覺很不錯榨惰,然后決定去深入探究一下屬性擴展拜英,這下應(yīng)該對Kotlin屬性擴展有了比較深的認(rèn)識了。
<div align="center"><img src="https://user-gold-cdn.xitu.io/2018/5/14/1635c3fb0ba21ec1?w=430&h=430&f=jpeg&s=39536" width="200" height="200"></div>
歡迎關(guān)注Kotlin開發(fā)者聯(lián)盟琅催,這里有最新Kotlin技術(shù)文章居凶,每周會不定期翻譯一篇Kotlin國外技術(shù)文章虫给。如果你也喜歡Kotlin,歡迎加入我們~~~