一文完全搞懂Kotlin中的委託

1. 什麼是委託?

委託,也就是委託模式,它是23種經典設計模式種的一種,又名代理模式,在委託模式中,有2個對象參與同一個請求的處理,接受請求的對象將請求委託給另外一個對象來處理。委託模式是一項技巧,其餘的幾種設計模式如:策略模式、狀態模式和訪問者模式都是委託模式的具體場景應用。html

委託模式中,有三個角色,約束、委託對象和被委託對象。git

委託模式中的角色.png

  • 約束: 約束是接口或者抽象類,它定義了通用的業務類型,也就是須要被代理的業務github

  • 被委託對象: 具體的業務邏輯執行者json

  • 委託對象: 負責對真是角色的應用,將約束累定義的業務委託給具體的委託對象。設計模式

2. 委託的具體場景

上一節講了委託的定義和它所包含的幾個角色,那麼具體該怎麼運用呢?咱們以一個實際的例子來看看。api

如今不少年輕人都愛完遊戲,不論是吃雞、王者榮耀仍是英雄聯盟。它們都是有等級之分的:青銅->白銀->黃金->鉑金->鑽石->宗師->王者,等級越高,表明你越厲害,就拿英雄聯盟來講,咱們多數混跡在白銀黃金階段,要上鑽石宗師段位很是困難。好比你排位打了好久,就差幾場就能上宗師了,總是打不上去,這個時候怎麼辦呢?好辦,如今有不少遊戲代練,委託遊戲代練給你打上去就行了。這其實就是一個委託模式。代碼該怎麼寫呢?一塊兒來看看:安全

首先,咱們定義約束類,定義咱們須要委託的業務,就拿這個場景來講,咱們的業務就是打排位賽,升級。所以,定義個約束類(接口)IGamePlayer:bash

// 約束類
interface IGamePlayer {
    // 打排位賽
    fun rank()
    // 升級
    fun upgrade()
}
複製代碼

約束類中,定義了咱們要代理的業務rank()upgrade(),而後,咱們就定義被委託對象,也就是遊戲代練微信

// 被委託對象,本場景中的遊戲代練
class RealGamePlayer(private val name: String): IGamePlayer{
    override fun rank() {
        println("$name 開始排位賽")
    }

    override fun upgrade() {
       println("$name 升級了")
    }

}
複製代碼

如上,咱們定義了一個被委託對象RealGamePlayer, 它有一個屬性name,它實現了咱們約定的業務(實現了接口方法)。多線程

接下來,就是委託角色

// 委託對象
class DelegateGamePlayer(private val player: IGamePlayer): IGamePlayer by player
複製代碼

咱們定義了一個委託類DelegateGamePlayer, 如今遊戲代練有不少,水平有高有低,若是發現水平不行,咱們能夠隨時換,所以,咱們把被委託對象做爲委託對象的屬性,經過構造方法傳進去。

注意:在kotlin 中,委託用關鍵字by 修飾,by後面就是你委託的對象,能夠是一個表達式。所以在本例中,經過by player 委託給了具體的被委託對象。

最後,看一下場景測試類:

// Client 場景測試
fun main() {
    val realGamePlayer = RealGamePlayer("張三")
    val delegateGamePlayer = DelegateGamePlayer(realGamePlayer)
    delegateGamePlayer.rank()
    delegateGamePlayer.upgrade()
}
複製代碼

咱們定義了一個遊戲代練,叫張三,將它傳遞給委託類,而後就能夠開始排位和升級的業務了,而最終誰完成了排位賽和升級了,固然是咱們的被委託對象,也就是遊戲代練--張三。

運行,結果以下:

張三 開始排位賽
張三 升級了
複製代碼

小結:以上就是委託的應用,再來回顧一下它的定義:2個對象參與處理同一請求,這個請求就是咱們約束類的邏輯,所以委託類(DelegateGamePlayer)和被委託類(RealGamePlayer)都須要實現咱們的約束接口IGamePlayer

3. 屬性委託

在Kotlin 中,有一些常見的屬性類型,雖然咱們能夠在每次須要的時候手動實現它們,可是很麻煩,各類樣板代碼存在,咱們知道,Kotlin但是宣稱要實現零樣板代碼的。爲了解決這些問題呢?Kotlin標準爲咱們提供了委託屬性

class Test {
    // 屬性委託
    var prop: String by Delegate()
}
複製代碼

委託屬性的語法以下:

val/var <屬性名>: <類型> by <表達式>

跟咱們前面將的委託相似,只不過前面是類委託,這裏屬性委託

3.1 屬性委託的原理

前面講的委託中,咱們有個約束角色,裏面定義了代理的業務邏輯。而委託屬性呢?其實就是上面的簡化,被代理的邏輯就是這個屬性的get/set方法。get/set會委託給被委託對象setValue/getValue方法,所以被委託類須要提供setValue/getValue這兩個方法。若是是val 屬性,只需提供getValue。若是是var 屬性,則setValue/getValue都須要提供。

好比上面的Delegate類:

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}
複製代碼

其中的參數解釋以下:

  • thisRef —— 必須與 屬性全部者 類型(對於擴展屬性——指被擴展的類型)相同或者是它的超類型;
  • property —— 必須是類型 KProperty<*>或其超類型。
  • value —— 必須與屬性同類型或者是它的子類型。

測試以下:

fun main() {
    println(Test().prop)
    Test().prop = "Hello, Android技術雜貨鋪!"
}
複製代碼

打印結果以下:

Test@5197848c, thank you for delegating 'prop' to me!
Hello, Android技術雜貨鋪! has been assigned to 'prop' in Test@17f052a3.
複製代碼
3.2 另外一種實現屬性委託的方式

上面咱們講了,要實現屬性委託,就必需要提供getValue/setValue方法,對於比較的同窗可能就要說了,這麼複雜的參數,還要每次都要手寫,真是麻煩,一不當心就寫錯了。確實是這樣,爲了解決這個問題, Kotlin 標準庫中聲明瞭2個含所需 operator方法的 ReadOnlyProperty / ReadWriteProperty 接口。

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
複製代碼

被委託類 實現這兩個接口其中之一就能夠了,val 屬性實現ReadOnlyPropertyvar屬性實現ReadOnlyProperty。 

// val 屬性委託實現
class Delegate1: ReadOnlyProperty<Any,String>{
    override fun getValue(thisRef: Any, property: KProperty<*>): String {
        return "經過實現ReadOnlyProperty實現,name:${property.name}"
    }
}
// var 屬性委託實現
class Delegate2: ReadWriteProperty<Any,Int>{
    override fun getValue(thisRef: Any, property: KProperty<*>): Int {
        return  20
    }

    override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) {
       println("委託屬性爲: ${property.name} 委託值爲: $value")
    }

}
// 測試
class Test {
    // 屬性委託
    val d1: String by Delegate1()
    var d2: Int by Delegate2()
}
複製代碼

如上代碼所示,定義了2個屬性代理,都經過 ReadOnlyProperty / ReadWriteProperty 接口實現。

測試代碼以下:

val test = Test()
    println(test.d1)
    println(test.d2)
    test.d2 = 100
複製代碼

打印結果:

經過實現ReadOnlyProperty實現,name:d1
20
委託屬性爲: d2 委託值爲: 100
複製代碼

能夠看到,與手動實現setValue/getValue效果同樣,可是這樣寫代碼就方便了不少了。

4. Kotlin 標準庫中提供幾個委託

Kotlin 標準庫中提供了幾種委託,例如:

  • 延遲屬性(lazy properties): 其值只在首次訪問時計算;
  • 可觀察屬性(observable properties): 監聽器會收到有關此屬性變動的通知;
  • 把多個屬性儲存在一個映射(map)中,而不是每一個存在單獨的字段中。
4.1 延遲屬性 lazy

lazy() 是接受一個 lambda 並返回一個 Lazy <T> 實例的函數,返回的實例能夠做爲實現延遲屬性的委託: 第一次調用 get() 會執行已傳遞給 lazy() 的 lambda 表達式並記錄結果, 後續調用 get() 只是返回記錄的結果。

val lazyProp: String by lazy {
    println("Hello,第一次調用纔會執行我!")
    "西哥!"
}

// 打印lazyProp 3次,查看結果
fun main() {
    println(lazyProp)
    println(lazyProp)
    println(lazyProp)
}
複製代碼

打印結果以下:

Hello,第一次調用纔會執行我!
西哥!
西哥!
西哥!
複製代碼

能夠看到,只有第一次調用,纔會執行lambda表達式中的邏輯,後面調用只會返回lambda表達式的最終值。

4.1.1 lazy 也能夠接受參數

lazy延遲初始化是能夠接受參數的,提供了以下三個參數:

/** * Specifies how a [Lazy] instance synchronizes initialization among multiple threads. */
public enum class LazyThreadSafetyMode {

    /** * Locks are used to ensure that only a single thread can initialize the [Lazy] instance. */
    SYNCHRONIZED,

    /** * Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value, * but only the first returned value will be used as the value of [Lazy] instance. */
    PUBLICATION,

    /** * No locks are used to synchronize an access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined. * * This mode should not be used unless the [Lazy] instance is guaranteed never to be initialized from more than one thread. */
    NONE,
}
複製代碼

三個參數解釋以下:

  • LazyThreadSafetyMode.SYNCHRONIZED: 添加同步鎖,使lazy延遲初始化線程安全

  • LazyThreadSafetyMode. PUBLICATION: 初始化的lambda表達式能夠在同一時間被屢次調用,可是隻有第一個返回的值做爲初始化的值。

  • LazyThreadSafetyMode. NONE:沒有同步鎖,多線程訪問時候,初始化的值是未知的,非線程安全,通常狀況下,不推薦使用這種方式,除非你能保證初始化和屬性始終在同一個線程

使用以下:

val lazyProp: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
    println("Hello,第一次調用纔會執行我!")
    "西哥!"
}
複製代碼

若是你指定的參數爲LazyThreadSafetyMode.SYNCHRONIZED,則能夠省略,由於lazy默認就是使用的LazyThreadSafetyMode.SYNCHRONIZED

4.2 可觀察屬性 Observable

若是你要觀察一個屬性的變化過程,那麼能夠將屬性委託給Delegates.observable, observable函數原型以下:

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)
        }
複製代碼

接受2個參數:

  • initialValue: 初始值
  • onChange: 屬性值被修改時的回調處理器,回調有三個參數property,oldValue,newValue,分別爲: 被賦值的屬性、舊值與新值。

使用以下:

var observableProp: String by Delegates.observable("默認值:xxx"){
    property, oldValue, newValue ->
    println("property: $property: $oldValue -> $newValue ")
}
// 測試
fun main() {
    observableProp = "第一次修改值"
    observableProp = "第二次修改值"
}
複製代碼

打印以下:

property: var observableProp: kotlin.String: 默認值:xxx -> 第一次修改值 
property: var observableProp: kotlin.String: 第一次修改值 -> 第二次修改值 
複製代碼

能夠看到,每一次賦值,都能觀察到值的變化過程。

4.2.1 vetoable 函數

vetoableobservable同樣,能夠觀察屬性值的變化,不一樣的是,vetoable能夠經過處理器函數來決定屬性值是否生效

來看這樣一個例子:聲明一個Int類型的屬性vetoableProp,若是新的值比舊值大,則生效,不然不生效。

代碼以下:

var vetoableProp: Int by Delegates.vetoable(0){
    _, oldValue, newValue ->
    // 若是新的值大於舊值,則生效
    newValue > oldValue
}
複製代碼

測試代碼:

fun main() {
    println("vetoableProp=$vetoableProp")
    vetoableProp = 10
    println("vetoableProp=$vetoableProp")
    vetoableProp = 5
    println("vetoableProp=$vetoableProp")
    vetoableProp = 100
    println("vetoableProp=$vetoableProp")
}
複製代碼

打印以下:

vetoableProp=0
 0 -> 10 
vetoableProp=10
 10 -> 5 
vetoableProp=10
 10 -> 100 
vetoableProp=100
複製代碼

能夠看到10 -> 5 的賦值沒有生效。

4.3 屬性存儲在映射中

還有一種狀況,在一個映射(map)裏存儲屬性的值,使用映射實例自身做爲委託來實現委託屬性,如:

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}
複製代碼

測試以下:

fun main() {
    val user = User(mapOf(
        "name" to "西哥",
        "age"  to 25
    ))
   println("name=${user.name} age=${user.age}") 
}
複製代碼

打印以下:

name=西哥 age=25
複製代碼

使用映射實例自身做爲委託來實現委託屬性,可使用在json解析中,由於json自己就能夠解析成一個map。不過,說實話,我暫時尚未發現這種使用場景的好處或者優點,若是有知道的同窗,評論區告知,謝謝!

5. 總結

委託在kotlin中佔有舉足輕重的地位,特別是屬性委託,lazy延遲初始化使用很是多,還有其餘一些場景,好比在咱們安卓開發中,使用屬性委託來封裝SharePreference,大大簡化了SharePreference的存儲和訪問。在咱們軟件開發中,始終提倡的是高內聚,低耦合。而委託,就是內聚,能夠下降耦合。另外一方面,委託的使用,也能減小不少重複的樣板代碼。

參考:www.kotlincn.net/docs/refere…

若是你喜歡個人文章,就關注下個人公衆號 Android技術雜貨鋪 、 簡書 或者Github! 微信公衆號:Android技術雜貨鋪

簡書:www.jianshu.com/u/35167a70a…

GitHub:github.com/pinguo-zhou…

公衆號.png
相關文章
相關標籤/搜索