Kotlin學習系列之:委託(Delegate)

1. 引入:委託作爲一種傳統的設計模式,在Java中要想實現這種設計模式,就需要自己進行類的結構設計來實現。而在Kotlin中,提供語言層面上的支持,我們可以通過by關鍵字很輕鬆就能實現。

2. 類委託(class delegate)

  • 自己動手實現委託:

    interface ServerApi {
        fun login(username: String, password: String)
    }
    
    
    class Retrofit : ServerApi {
    
        /*類比登錄操作
        */
        override fun login(username: String, password: String) {
            println("login successfully.")
        }
    }
    
    class RemoteRepository : ServerApi {
    
        private val serverApi: ServerApi = Retrofit()
    
        override fun login(username: String, password: String) {
            serverApi.login(username, password)
        }
    }
    
    fun main() {
    
        val repository = RemoteRepository()
        repository.login("David", "123456") //輸出 login successfully.
    }
    

    首先我們聲明瞭一個接口ServerApi,然後定義了其兩個實現類:Retrofit、RemoteRepository,並且我們在Retrofit類裏實現了具體的login方法,而在RemoteRepository裏對於login方法的實現邏輯就直接委託給了serverApi(Retrofit對象),這樣我們就自己實現了一個委託模式。但是我們發現:無論我們在ServerApi中定義多少個抽象方法,RemoteRepository類的結構是有規律可言的,或者不禮貌地說這些代碼比較冗餘。那下面我們就來看看如何通過by關鍵字輕鬆實現類委託。

  • 使用by關鍵字,改寫RemoteRepository類:

    class RemoteRepository(retrofit: Retrofit) : ServerApi by retrofit

    搞定。可以看出語法就是:在要實現的接口後面 + by + 委託對象。這樣我們使用了Kotlin的類委託。

  • 透過現象看本質:

    按照慣例,我們現在去反編譯,從字節碼層面上去理解Kotlin的類委託。實際上大家可以去猜測一下底層實現(應該和前面我們手動實現的一樣):

    的確,和我們之前自己手動實現的一樣:編譯器會自動在被委託類添加了一個委託類對象作爲它的屬性,並且在構造方法中將我們指定的委託對象賦值給了它,然後實現了抽象方法,實現的邏輯就是委託給這個添加的委託類對象。

  • 對於被委託類中某些方法,可以提供自己的實現

    interface ServerApi {
        fun login(username: String, password: String)
        fun register(username: String, password: String)
    }
    
    class Retrofit : ServerApi {
    
        override fun login(username: String, password: String) {
            println("login: username = $username, password = $password")
        }
    
        override fun register(username: String, password: String) {
            println("register: username = $username, password = $password")
        }
    }
    
    class RemoteRepository(retrofit: Retrofit) : ServerApi by retrofit{
    
        override fun register(username: String, password: String) {
            println("register in RemoteRepository.")
        }
    }
    

    這樣的話,對於register的調用,就會執行自己的邏輯,編譯器就不會再爲你提供實現。

  • 多個委託:

    interface NewsApi {
        fun getNewsList()
    }
    
    class NewsApiImpl : NewsApi {
        override fun getNewsList() {
            println("NewsApiImpl: getNewsList()")
        }
    }
    
    class RemoteRepository(retrofit: Retrofit) : ServerApi by retrofit, NewsApi by NewsApiImpl()
    

    如果需要多個委託,採用這種語法就可以,一一對應。

3. 屬性委託(delegated property)

  • 引入:在kotlin中,不光光支持類委託,對於屬性的訪問(set、get),我們也可以指定它的委託
  • 如何實現屬性委託:

    class Delegate {
    
        operator fun getValue(thisRef: Any?, property: KProperty<*>) = "hello world"
    
        operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
            println("the new value is $value")
        }
    }
    
    class RealSub {
        var str: String by Delegate()
    }
    
    fun main() {
        val sub = RealSub()
        sub.str = "hello"   
        println(sub.str)
    }
    

    實際上可以分爲兩步:

    a. 定義一個屬性委託類(如這裏的Delegate),然後在這個類中提供兩個方法:getValue()、setValue(),他們的方法簽名必須按照如下格式:

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {}
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {} 
    

    operator關鍵字:表示運算符重載,可參考此篇文章
    第一個參數:表示被委託屬性所屬的對象
    第二個參數:類似Java中的Field,屬性的反射對象
    第三個參數(setValue中):被委託屬性對象要賦的值
    返回值(getValue中):被委託屬性對象的值

    b. 在被委託的屬性(如這裏的str)後添加:by 委託對象

    我們再來看main方法的測試代碼:

    sub.str = "hello",就會觸發setValue方法的調用,打印:the new value is hello
    println(sub.str),就會觸發getValue方法的調用,返回"hello world", 故打印:hello world

    總結來說,屬性委託就是對於某屬性的訪問,委託給了我們指定的委託對象中的getValue、setValue(var類型屬性)方法。剛剛我們是自己實現了這個了屬性委託,實際上Kotlin標準庫中也爲了提供幾種常用的屬性委託,能夠爲我們的開發帶來極大的便利。

4. 標準庫中的屬性委託之:lazy

  • 使用場景:延遲val屬性的初始化時機(第一次訪問的時候纔會去初始化)
  • 示例代碼:

    class LazyTest {
        val lazyValue: String by lazy {
            println("I'm in lazy.")
            "lazyValue"
        }
    }
    
    fun main() {
    
        val lazyTest = LazyTest()
        println(lazyTest.lazyValue)
        println(lazyTest.lazyValue)
    }   
    

    輸出結果爲:

    I'm in lazy.
    lazyValue
    lazyValue
    

    下面我們來解釋一下這段代碼,從而理解lazy委託的特點。在by關鍵字後跟上一個lazy+lambda表達式,那麼這個lazy是個啥:

    public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
    

    actual關鍵字可以先忽略,它與多平臺相關。忽略後,可以看到這個lazy就是個普通的function,返回一個Lazy對象,那麼也就是說返回的這個Lazy對象作爲我們lazyValue屬性的委託對象。再看這個Lazy:

    public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
    

    定義了一個擴展方法:getValue,這個getValue方法簽名對我們來說應該很熟悉了。同時我們還可以得出一個結論,對於委託類,除了以member的形式來定義getValue/setValue,還可以通過extension的方式來定義

    最後注意一下lazy函數的參數,一個不帶參數、返回一個T類型的lambda表達式(如果lambda表達式是function的最後一個參數,那麼推薦將其寫到外面)。這個lambda表達式的返回值就是我們要賦給屬性的值。

    上面僅僅是對lazy委託本身進行了分析,那麼它有什麼特點呢?我們還得結合main方法中的測試代碼來看:

    當第一次訪問lazyValue時,打印出了:

    I'm in lazy.
    lazyValue.
    

    當第二次訪問lazyValue時,僅打印了:

    lazyValue.
    

    可以看出,lazy後的lambda表達式只是在被委託屬性第一次被訪問的時候執行了一次,並且將返回值用來初始化了被委託屬性,之後對於被委託屬性的訪問,直接使用初始值。這裏說的訪問,確切地說是get()。

  • 如果你對多線程編程敏感的話,可以隱約意識到,在多線程環境下,這裏會不會出現多線程同步的問題,畢竟lambda表達式裏不是原子操作。不慌,我們來看lazy的另一個重載方法:

    public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
        when (mode) {
            LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
            LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
            LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
        }
    

    它會多一個參數,LazyThreadSafetyMode類型mode,LazyThreadSafetyMode實際上是一個枚舉類,它有三個枚舉對象:

    LazyThreadSafetyMode.SYNCHRONIZED: 這種模式下,lambda表達式中的代碼是加了鎖的,確保只有一個線程能夠來執行初始化
    LazyThreadSafetyMode.PUBLICATION: 這種模式下,lambda表達式中的代碼是允許多個線程同時訪問的,但是隻有第一個返回的值作爲初始值
    LazyThreadSafetyMode.NONE: 這種模式下,lambda表達式中的代碼在多線程的情況下的行爲是不確定的,這種模式並不推薦使用,除非你確保它永遠不可能在多線程環境下使用,然後使用這種模式可以避免因鎖所帶來的額外開銷。

    很慶幸,默認情況下,lazy的模式是第一種,所以默認情況下是不會出現同步問題的。

5. 標準庫的屬性委託之:observable/vetoable

  • 特點:可以對屬性值的變化進行監聽
  • 示例代碼:

    class Person {
        var name: String by Delegates.observable("<no name>") { property, oldValue, newValue ->
            println("property'name is ${property.name}, oldValue = $oldValue, newValue = $newValue")
        }
    }
    
    fun main() {
        val person = Person()
        person.name = "Alice"
        person.name = "Bob"
    }
    

    我們關注by後面的部分就可以了,調用了Delegates.observable(),將它的返回值作爲委託對象:

    public object Delegates {
    
        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)
            }
    }
    

    Delegates是個對象,observable接收兩個參數:一個初始值,賦給被委託屬性;一個lambda表達式,lambda有三個回調參數,描述屬性的KProperty、舊值以及新值。一旦被委託屬性的值發生變化(即調用set方法)時,就會回調lambda表達式。

    現在再來看main函數中的代碼就簡單多了:

    person.name = "Alice" => 打印:

    property'name is name, oldValue = <no name>, newValue = Alice
    

    person.name = "Bob" => 打印:

    property'name is name, oldValue = Alice, newValue = Bob
    

    回過頭再來關注一下這個observable方法的返回值類型:ReadWriteProperty

    /**
     * Base interface that can be used for implementing property delegates of read-write properties.
     *
     * This is provided only for convenience; you don't have to extend this interface
     * as long as your property delegate has methods with the same signatures.
     *
     * @param R the type of object which owns the delegated property.
     * @param T the type of the property value.
     */
    public interface ReadWriteProperty<in R, T> {
        /**
         * Returns the value of the property for the given object.
         * @param thisRef the object for which the value is requested.
         * @param property the metadata for the property.
         * @return the property value.
         */
        public operator fun getValue(thisRef: R, property: KProperty<*>): T
    
        /**
         * Sets the value of the property for the given object.
         * @param thisRef the object for which the value is requested.
         * @param property the metadata for the property.
         * @param value the value to set.
         */
        public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
    }
    

    在這個接口中,我們看到了有熟悉方法簽名的getValue、setValue方法。一起來讀一下這個接口的文檔註釋:

    它是一個用來對可讀可寫(即var)的屬性實現屬性委託的接口;但是它的存在僅僅是爲了方便,只要我們的屬性委託擁有相同的方法簽名,開發者不必來繼承這個接口。

    與之類似的還有個ReadOnlyProperty:

    /**
     * Base interface that can be used for implementing property delegates of read-only properties.
     *
     * This is provided only for convenience; you don't have to extend this interface
     * as long as your property delegate has methods with the same signatures.
     *
     * @param R the type of object which owns the delegated property.
     * @param T the type of the property value.
     */
    public interface ReadOnlyProperty<in R, out T> {
        /**
         * Returns the value of the property for the given object.
         * @param thisRef the object for which the value is requested.
         * @param property the metadata for the property.
         * @return the property value.
         */
        public operator fun getValue(thisRef: R, property: KProperty<*>): T
    }
    

    註釋基本同ReadWriteProperty類似,只不過它是服務於val屬性。

    同observable委託有相同功能的還有一個:vetoable。

     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)
    }
    

    發現它的lambda會要求一個返回值,這個返回值有什麼作用呢?這與observable和vetoable的回調時機不同有關:observable的回調時機是在屬性值修改之後,vetoable的回調時機在屬性值被修改之前。如果返回值爲true,屬性值就會被修改成新值;如果返回值爲false,此次修改就會直接被丟棄。

    我們來看示例代碼:

    class Adult {
    
        var age: Int by Delegates.vetoable(18) { property, oldValue, newValue ->
            println("property'name is ${property.name}, oldValue = $oldValue, newValue = $newValue")
            newValue >= 18
        }
    }
    
    fun main(){
        val adult = Adult()
        adult.age = 25
        println("adult.age = ${adult.age}")
        adult.age = 16
        println("adult.age = ${adult.age}")
    }
    

    當adult.age = 25時,屬性值被成功修改;adult.age = 16,修改操作被丟棄,修改失敗,屬性值還是原來的。

6. 對比一下lazy委託和observable委託:lazy委託專注於getValue(),observable委託專注於setValue()

7. map委託

  • 特點:對於屬性的訪問,直接委託給一個map對象。
  • 要求:map的key要同屬性名保持一致。
  • 對於val屬性:

    class User(map: Map<String, Any?>) {
        val name: String by map
        val age: Int by map
    }
    
    
    fun main() {
    
        val user = User(mapOf(
                "name" to "David Lee",
                "age" to 25
        ))
    
        println(user.name)  //輸出 David Lee
        println(user.age)   //輸出 25
    
    }
    
  • 對於var屬性:

    class Student(map: MutableMap<String, Any?>) {
    
        var name: String by map
        var age: Int by map
        var address: String by map
    }
    
    fun main(){
        val map: MutableMap<String, Any?> = mutableMapOf(
                    "name" to "Alice",
                    "age" to 23,
                    "address" to "beijing"
            )
    
        val student = Student(map)
        println(student.name)       //Alice
        println(student.age)        //23
        println(student.address)    //beijing
    
        println("---------------------------------")
    
        student.address = "hefei"
        println(student.address)    // hefei
        println(map["address"])     // hefei
    }
    

    對比可以得知:

    • val的map委託的對象是Map<String, Any?>,var的map委託的對象MutableMap<String, Any?>
    • 對於var屬性,對於MutableMap中的value的修改,會同步到屬性值;反之亦然。

8. 現在再回過頭反編譯一下我們自己動手實現的屬性委託:

getStr的反編譯結果就沒貼了,同setStr類似。通過這倆截圖,我們可以知道:kotlin編譯器爲RealSub類生成了兩個重要的部分:

  • Delegate類型的實例成員:對於setStr()的實現邏輯,委託給Delegate類型的委託對象
  • static final的KProperty[] $$delegatedProperties: 在static代碼塊中初始化,存儲被委託屬性的KProperty,然後在後續的setValue、getValue的調用中作爲其第二個參數。

9. 提供委託

終於寫到最後一個部分了,有點興奮也有點疲勞。前面我們介紹的屬性委託,我們介入的環節都是對於屬性的訪問,實際上我們還可以對於委託對象的生成(或者說選取)進行介入:

class People {
    val name: String by DelegateProvider()
    val address: String by DelegateProvider()
}

class DelegateProvider {

    operator fun provideDelegate(thisRef: People, property: KProperty<*>): ReadOnlyProperty<People, String> {
        println("I'm in provideDelegate.")
        checkProperty()
        return RealDelegate()
    }

    private fun checkProperty() {
        val random = Random.Default
        if (!random.nextBoolean()) {
            throw RuntimeException("failed to create delegate.")
        }
    }
}

class RealDelegate : ReadOnlyProperty<People, String> {
     override fun getValue(thisRef: People, property: KProperty<*>): String {
         return "kotlin"
     }
}

先撇開中間的DelegateProvider類不看,其他兩個類的實現符合我們之前介紹的理論。那麼中間的這個類有什麼特點或者說什麼要求呢?必須提供一個provideDelegate的方法,同樣地對於它的方法簽名是有要求的:

operator fun provideDelegate(thisRef: T, property: KProperty<*>): RealOnlyProperty<T, R>

或者

operator fun provideDelegate(thisRef: T, property: KProperty<*>): ReadWriteProperty<T, R>

再回到代碼實現中來,我們這裏通過checkProperty方法來模擬相關邏輯檢查,添加main方法進行測試:

fun main() {

    val people = People()
    println(people.name)
    println(people.address)
}

然後看人品隨機,多運行幾次吧,肯定有不拋異常的時候。篇幅有點長,謝謝耐心閱讀!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章