Kotlin知識歸納(十) —— 委託

大棋發表於2019-07-08

前序

      委託,對於很多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)
    }

}
複製程式碼

委託屬性

委託屬性就是將屬性的訪問器(getset)委託給一個符合屬性委託約定規則的物件。

      委託屬性和類委託不同,委託屬性更像是給屬性找代理。 委託屬性同樣是利用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官方庫中提供 ReadOnlyPropertyReadWriteProperty 介面,方便開發者實現這些介面來提供正確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的委託屬性如何使用後,還需要深入瞭解下委託屬性的原始碼:

Kotlin知識歸納(十) —— 委託

NotNullVar:Delegates.notNull()延遲初始化的委託類

Delegates:Delegates作為一個物件宣告存在,裡面擁有3個非常熟悉的方法:notNull()observablevetoable

ObservablePropertyObservableProperty系統定義的委託類,observablevetoable返回該委託類的匿名物件。

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函式。ObservablePropertygetValue()會先獲取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、建立一個繼承自ObservablePropertyPerson$$special$$inlined$observable$1類,因為Delegates.observable()是返回一個匿名的ObservableProperty物件。
  • 2、Person類中定義了一個name$delegate屬性,該屬性指向name屬性的代理物件,即Person$$special$$inlined$observable$1類的物件。
  • 3、Person類中name屬性會轉換為getName()setName()
  • 4、name屬性的getset方法的內部呼叫name$delegate相應的setValue()getValue()
  • 5、KProperty陣列中會儲存通過Kotlin反射得到的Personr類中的name屬性的資訊。在呼叫name$delegatesetValue()getValue()時,將這些資訊作為引數傳遞進去。

幕後欄位與幕後屬性

      你看完反編譯的Java原始碼後,或許會發現一個問題:為什麼Kotlin中Personname屬性並沒有在Java的Person中被定義,只實現了該屬性的getset方法。

      這其中涉及到Kotlin的幕後欄位的問題, Kotlin 什麼是幕後欄位? 中講得很清楚:

只有擁有幕後欄位的屬性轉換成Java程式碼時,才有對應的Java變數。

Kotlin屬性擁有幕後欄位需要滿足以下條件之一:

  • 使用預設 getter / setter 的屬性,一定有幕後欄位。對於 var 屬性來說,只要 getter / setter 中有一個使用預設實現,就會生成幕後欄位;
  • 在自定義 getter / setter 中使用了 field 的屬性

      所以也就能理解,為什麼擴充套件屬性不能使用 field,因為擴充套件屬性並不能真的在該類中新增新的屬性,不能具有幕後欄位。而且委託屬性中,該屬性的getset方法內部都是呼叫代理物件的getValue()setValue(),並沒有使用 field ,且都不是使用預設的getset方法。

總結

  • 類委託可以很方便的實現裝飾設計模式,開發者只用關心需要擴充套件的方法。
  • 委託屬性就是將該屬性的setget交由 代理物件 的setValuegetValue來處理。
  • 委託屬性也是一種 約定 。setValuegetValue都需帶有operator關鍵字修飾。
  • Kotlin標準庫提供 ReadOnlyPropertyReadWriteProperty 介面,方便開發者實現這些介面來提供正確getValue()方法 和 setValue()方法。
  • val屬性可以藉助 委託屬性 進行延遲初始化,使用lazy()設定初始化流程,並自動返回代理物件。
  • Delegates.observable()能在 被委託的屬性 改變時接收到通知,有點類似ACC的LiveData
  • Delegates.vetoable()能在 被委託的屬性 改變前接收通知,並能決定該屬性賦不賦予新值。
  • Delegates.notNull()可以用作任何型別的var變數進行 延遲初始化
  • 只有擁有幕後欄位的屬性轉換成Java程式碼時,才有對應的Java變數。

參考資料:

android Kotlin系列:

Kotlin知識歸納(一) —— 基礎語法

Kotlin知識歸納(二) —— 讓函式更好呼叫

Kotlin知識歸納(三) —— 頂層成員與擴充套件

Kotlin知識歸納(四) —— 介面和類

Kotlin知識歸納(五) —— Lambda

Kotlin知識歸納(六) —— 型別系統

Kotlin知識歸納(七) —— 集合

Kotlin知識歸納(八) —— 序列

Kotlin知識歸納(九) —— 約定

Kotlin知識歸納(十) —— 委託

Kotlin知識歸納(十一) —— 高階函式

Kotlin知識歸納(十二) —— 泛型

Kotlin知識歸納(十三) —— 註解

Kotlin知識歸納(十四) —— 反射

Kotlin知識歸納(十) —— 委託

相關文章