前序
委託,對於很多Java開發者來說都會一面矇蔽,我也不例外。委託,維基百科的解釋是:有兩個物件參與處理同一個請求,接受請求的物件將請求委託給另一個物件來處理。這好像有一點代理的味道(*゜ー゜*)。Kotlin中委託分為類委託和委託屬性。
類委託
在解釋類委託之前,需要先了解一波裝飾設計模式。裝飾設計模式的核心思想是:
在不使用繼承的情況下,擴充套件一個物件的功能,使該物件變得更加強大。
通常套路是:建立一個新類,新類實現與原始類一樣的介面,並將原來的類的例項作為一個欄位儲存,與原始類擁有同樣的行為(方法)。一部分行為(方法)與原始類保持一致(即直接呼叫原始類的行為(方法)),還有一部分行為(方法)在原始類的行為(方法)基礎上進行擴充套件。
裝飾設計模式的缺點是需要較多的樣板程式碼,顯得比較囉嗦。例如:最原始的裝飾類需要實現介面的全部方法,並在這些方法中呼叫原始類物件對應的方法。
class CustomList<T>(
val innerList:MutableList<T> = ArrayList<T>()):MutableCollection<T> {
override val size: Int = innerList.size
override fun contains(element: T): Boolean = innerList.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
override fun isEmpty(): Boolean = innerList.isEmpty()
override fun add(element: T): Boolean = innerList.add(element)
override fun addAll(elements: Collection<T>): Boolean = innerList.addAll(elements)
override fun clear() = innerList.clear()
override fun iterator(): MutableIterator<T> = innerList.iterator()
override fun remove(element: T): Boolean = innerList.remove(element)
override fun removeAll(elements: Collection<T>): Boolean = innerList.removeAll(elements)
override fun retainAll(elements: Collection<T>): Boolean = innerList.retainAll(elements)
}
複製程式碼
但Kotlin將委託作為一個語言級別的功能進行頭等支援。可以利用by
關鍵字,將新類的介面實現委託給原始類,編譯器會為新類自動生成介面方法,並預設返回原始類對應的具體實現。然後我們過載需要擴充套件的方法。
class CustomList<T>(
val innerList:MutableList<T> = ArrayList<T>()):MutableCollection<T> by innerList{
override fun add(element: T): Boolean {
println("CustomList add element")
innerList.add(element)
}
}
複製程式碼
委託屬性
委託屬性就是將屬性的訪問器(
get
和set
)委託給一個符合屬性委託約定規則的物件。
委託屬性和類委託不同,委託屬性更像是給屬性找代理。 委託屬性同樣是利用by
關鍵字,將屬性委託給代理物件。屬性的代理物件不必實現任何的介面,但是需要提供一個 getValue()
函式與 setValue()
函式(僅限 var
屬性)。例如:
class Person{
var name:String by Delegate()
}
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "kotlin"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String){
}
}
複製程式碼
屬性 name
將自己的set
/get
方法委託給了Delegate物件的getValue()
和 setValue()
。在getValue()
和 setValue()
中都有operator
修飾,意味著委託屬性也是依賴於約定的功能。像其他約定的函式一樣,getValue()
和 setValue()
可以是成員函式,也可以是擴充套件函式。
Kotlin官方庫中提供 ReadOnlyProperty
或 ReadWriteProperty
介面,方便開發者實現這些介面來提供正確的 getValue()
方法 和 setValue()
方法。
public interface ReadOnlyProperty<in R, out T> {
public operator fun getValue(thisRef: R, property: KProperty<*>): T
}
public interface ReadWriteProperty<in R, T> {
public operator fun getValue(thisRef: R, property: KProperty<*>): T
public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
複製程式碼
使用委託屬性
惰性初始化
當需要進行屬性延遲初始化時,往往會想到使用lateinit var
進行延遲初始化。但那是對於var
變數,即可變變數,但對於val
變數呢?可以使用支援屬性來實現惰性初始化:
class Person{
//真正儲存郵箱列表的物件
private var _emails:List<Email>? = null
//對外暴露的郵箱列表物件
val emails:List<Email>
get() {
if ( _emails == null){
_emails = ArrayList<Email>()
}
return _emails!!
}
}
複製程式碼
提供一個"隱藏"屬性_emails
用來儲存真正的值,而另一個屬性emails
用來提供屬性的讀取訪問。_emails
是可變可空,emails
不可變不可空,當你訪問emails
時,才初始化_emails
變數,並返回_emails
物件,達到對val
物件延遲初始化的目的。
但這種方案在需要多個惰性屬性時,就顯得很囉嗦了,而且他並不是執行緒安全的。Kotlin提供了更加便捷的解決方案:委託屬性,並使用標準庫函式lazy
返回代理物件。
class Person{
val email:List<Email> by lazy {
ArrayList<Email>()
}
}
複製程式碼
lazy
函式接收初始化該值操作的lambda,並返回一個具有getValue()
方法的代理物件,並配合by
關鍵字將屬性委託給lazy
函式返回的代理物件。lazy
函式是執行緒安全的,不用擔心非同步的問題。
屬性改變的通知
當一個物件的屬性需要更改時得到通知,最原始的辦法就是,重寫set方法,在set方法中設定處理屬性改變的邏輯。手工實現屬性修改的通知:
class Person(name:String,age:Int){
var age :Int = age
set(newValue) {
val oldValue = field
field = newValue
//監聽值改變(或使用Listener物件)
valueChangeListener("age",oldValue,newValue)
}
var name :String = name
set(newValue) {
val oldValue = field
field = newValue
//監聽值改變
valueChangeListener("name",oldValue,newValue)
}
fun <T> valueChangeListener(fieldName:String,oldValue:T,newValue:T){
println("$fieldName oldValue = $oldValue newValue = $newValue")
}
}
複製程式碼
但這種方案在跟惰性初始化最開始的例子類似,當需要監聽多個屬性時,程式碼冗長且囉嗦。我們可以像惰性初始化一樣,使用委託屬性實現:
class Person(name:String,age:Int){
var age:Int by PropertyObservable(age){ property, oldValue, newValue ->
}
var name:String by PropertyObservable(name){ property, oldValue, newValue ->
}
}
//委託類
class PropertyObservable<T>(var initValue:T,
val observer:(property: KProperty<*>, oldValue: T, newValue: T) -> Unit): ReadWriteProperty<Person, T> {
override fun getValue(thisRef: Person, property: KProperty<*>): T {
return initValue;
}
override fun setValue(thisRef: Person, property: KProperty<*>, newValue: T) {
val oldeValue = initValue
initValue = newValue
//監聽值改變(或使用Listener物件)
observer(property,oldeValue,newValue)
}
}
複製程式碼
定義委託類,通過委託屬性"接管"該屬性的get
/set
,並提供初始值,以及屬性被修改時的處理邏輯。大大簡化屬性設定監聽的程式碼。
但Kotlin在標準庫中已經為我們提供了Delegates.observable()
方法,大大方便我們使用委託屬性對屬性的修改進行監聽,像我們自定義的委託類一樣,該方法接受屬性的初始化值,以及屬性變化時的處理邏輯:
class Person(name:String,age:Int){
var age:Int by Delegates.observable(age){ property, oldValue, newValue ->
println("${property.name} oldValue = $oldValue newValue = $newValue")
}
var name:String by Delegates.observable(name){ property, oldValue, newValue ->
println("${property.name} oldValue = $oldValue newValue = $newValue")
}
}
複製程式碼
Kotlin在標準庫中提供了一個類似Delegates.observable()
的方法:Delegates.vetoable()
。但會在屬性被賦新值生效之前會傳遞給 Delegates.vetoable()
進行處理,依據Delegates.vetoable()
的返回的布林值判斷要不要賦新值。
第三種延遲初始化
之前已經知道,var
屬性需要延遲初始化時,可以使用lateinit
關鍵字,val
屬性需要延遲初始化時,可以使用委託屬性 + lazy()
函式的方法。但lateinit
關鍵字的延遲處理僅對引用型別有用,對基本資料型別無效,當需要對基本資料型別進行延遲初始化怎麼辦呢?Kotlin通過委託屬性提供另一種延遲初始化的方式:Delegates.notNull()
var num:Int by Delegates.notNull()
複製程式碼
雖然Kotlin提供了延遲初始化的方式,使開發者不用強制在建構函式中初始化(例如Activity
中在onCreate
中初始化),但對於延遲初始化的值,必須確保其被初始化,否則將會像Java空指標一樣,丟擲異常。
方式 | 適用型別 |
---|---|
lateinit | 引用型別 |
Delegates.notNull() | 基本資料型別、引用型別 |
轉換規則
每個委託屬性的實現的背後,Kotlin 編譯器都會生成輔助屬性並委託給它。例如:對於屬性 name
,編譯器會生成隱藏屬性 name$delegate,而屬性 name
訪問器的程式碼委託給隱藏屬性的getValue()/setValue()。
class Person{
var name:String by MyDelegate()
}
複製程式碼
編譯器生成以下程式碼:
class Person{
private val name$delegate = MyDelegate()
var name:String
get() = name$delegate.getValue(this,<property>)
set(value:String) = name$delegate.setValue(this,<property>,value)
}
複製程式碼
- thisRef表示持有該委託屬性的物件
- property KProperty<*> 型別或是它的父類,屬性的描述。(可獲取屬性的名稱等)
- value 屬性的新值
原始碼閱讀
掌握了Kotllin的委託屬性如何使用後,還需要深入瞭解下委託屬性的原始碼:
NotNullVar:Delegates.notNull()
延遲初始化的委託類
Delegates:Delegates
作為一個物件宣告存在,裡面擁有3個非常熟悉的方法:notNull()
、observable
和vetoable
。
ObservableProperty:ObservableProperty
系統定義的委託類,observable
和vetoable
返回該委託類的匿名物件。
Delegates.notNull()
Delegates.notNull()
直接返回NotNullVar
物件作為委託屬性的代理物件。
#Delegates.kt
public fun <T : Any> notNull(): ReadWriteProperty<Any?, T> = NotNullVar()
複製程式碼
#Delegates.kt
private class NotNullVar<T : Any>() : ReadWriteProperty<Any?, T> {
private var value: T? = null
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()
和setValue()
,使Delegates.notNull()
可以代理var
屬性。
Delegates.observable()
Delegates.observable()
和Delegates.vetoable()
一樣,都是直接返回ObservableProperty的匿名物件。但Delegates.observable()
過載afterChange
函式,並在afterChange
函式中執行Delegates.observable()
接收的lambda。ObservableProperty#setValue()
在對屬性賦新值後,將舊值和新值作為引數執行afterChange
函式。
#Delegates.kt
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)
}
複製程式碼
#ObservableProperty.kt
protected open fun afterChange(property: KProperty<*>, oldValue: T, newValue: T): Unit {}
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)
}
複製程式碼
Delegates.vetoable()
Delegates.vetoable()
與Delegates.observable()
非常相似,只是過載的函式不一致,Delegates.vetoable()
過載beforeChange
函式。ObservableProperty
的getValue()
會先獲取beforeChange
函式的返回值(預設是true),判斷是否繼續執行賦值操作。所以這就是Delegates.vetoable()
的不同的地方。
#Delegates.kt
public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean):
ReadWriteProperty<Any?, T> =
object : ObservableProperty<T>(initialValue) {
override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue)
}
複製程式碼
#ObservableProperty.kt
protected open fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = true
public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
val oldValue = this.value
//如果beforeChange返回false,則直接返回函式,不賦值
if (!beforeChange(property, oldValue, value)) {
return
}
this.value = value
afterChange(property, oldValue, value)
}
複製程式碼
委託屬性的原理
想要更深層次的瞭解Kotlin的委託,最好的辦法就是將其轉換成Java程式碼進行研究。
#daqiKotlin.kt
class Person{
var name:String by Delegates.observable("daqi"){ property, oldValue, newValue ->
println("${property.name} oldValue = $oldValue newValue = $newValue")
}
}
複製程式碼
反編譯後的Java程式碼:
public final class Person$$special$$inlined$observable$1 extends ObservableProperty {
// $FF: synthetic field
final Object $initialValue;
public Person$$special$$inlined$observable$1(Object $captured_local_variable$1, Object $super_call_param$2) {
super($super_call_param$2);
this.$initialValue = $captured_local_variable$1;
}
protected void afterChange(@NotNull KProperty property, Object oldValue, Object newValue) {
Intrinsics.checkParameterIsNotNull(property, "property");
String newValue = (String)newValue;
String oldValue = (String)oldValue;
int var7 = false;
String var8 = property.getName() + " oldValue = " + oldValue + " newValue = " + newValue;
System.out.println(var8);
}
}
public final class Person {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "name", "getName()Ljava/lang/String;"))};
@NotNull
private final ReadWriteProperty name$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 Person() {
Delegates var1 = Delegates.INSTANCE;
Object initialValue$iv = "daqi";
ReadWriteProperty var5 = (ReadWriteProperty)(new Person$$special$$inlined$observable$1(initialValue$iv, initialValue$iv));
this.name$delegate = var5;
}
}
複製程式碼
- 1、建立一個繼承自
ObservableProperty
的Person$$special$$inlined$observable$1
類,因為Delegates.observable()
是返回一個匿名的ObservableProperty
物件。 - 2、
Person
類中定義了一個name$delegate
屬性,該屬性指向name
屬性的代理物件,即Person$$special$$inlined$observable$1
類的物件。 - 3、
Person
類中name
屬性會轉換為getName()
和setName()
。 - 4、
name
屬性的get
和set
方法的內部呼叫name$delegate
相應的setValue()
和getValue()
。 - 5、KProperty陣列中會儲存通過Kotlin反射得到的Personr類中的name屬性的資訊。在呼叫
name$delegate
的setValue()
和getValue()
時,將這些資訊作為引數傳遞進去。
幕後欄位與幕後屬性
你看完反編譯的Java原始碼後,或許會發現一個問題:為什麼Kotlin中Person
的name
屬性並沒有在Java的Person
中被定義,只實現了該屬性的get
和set
方法。
這其中涉及到Kotlin的幕後欄位的問題, Kotlin 什麼是幕後欄位? 中講得很清楚:
只有擁有幕後欄位的屬性轉換成Java程式碼時,才有對應的Java變數。
Kotlin屬性擁有幕後欄位需要滿足以下條件之一:
- 使用預設
getter
/setter
的屬性,一定有幕後欄位。對於var
屬性來說,只要getter
/setter
中有一個使用預設實現,就會生成幕後欄位; - 在自定義
getter
/setter
中使用了field
的屬性
所以也就能理解,為什麼擴充套件屬性不能使用 field
,因為擴充套件屬性並不能真的在該類中新增新的屬性,不能具有幕後欄位。而且委託屬性中,該屬性的get
和set
方法內部都是呼叫代理物件的getValue()
或setValue()
,並沒有使用 field
,且都不是使用預設的get
和set
方法。
總結
- 類委託可以很方便的實現裝飾設計模式,開發者只用關心需要擴充套件的方法。
- 委託屬性就是將該屬性的
set
和get
交由 代理物件 的setValue
和getValue
來處理。 - 委託屬性也是一種 約定 。
setValue
和getValue
都需帶有operator
關鍵字修飾。 - Kotlin標準庫提供
ReadOnlyProperty
或ReadWriteProperty
介面,方便開發者實現這些介面來提供正確的getValue()
方法 和setValue()
方法。 val
屬性可以藉助 委託屬性 進行延遲初始化,使用lazy()
設定初始化流程,並自動返回代理物件。Delegates.observable()
能在 被委託的屬性 改變時接收到通知,有點類似ACC的LiveDataDelegates.vetoable()
能在 被委託的屬性 改變前接收通知,並能決定該屬性賦不賦予新值。Delegates.notNull()
可以用作任何型別的var
變數進行 延遲初始化- 只有擁有幕後欄位的屬性轉換成Java程式碼時,才有對應的Java變數。
參考資料:
- 《Kotlin實戰》
- Kotlin官網