一文徹底搞懂Kotlin中的委托

0
回復
211
查看
打印 上一主題 下一主題
[復制鏈接]

37

主題

57

帖子

3077

安幣

管理員

Rank: 9Rank: 9Rank: 9

樓主
發表于 2020-4-17 17:45:01 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
如果對本篇文章感興趣,請前往,原文地址:http://www.clfrpjw.com.cn/blog-720372-83939.html

1. 什么是委托?

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



  • 約束: 約束是接口或者抽象類,它定義了通用的業務類型,也就是需要被代理的業務
  • 被委托對象: 具體的業務邏輯執行者
  • 委托對象: 負責對真是角色的應用,將約束累定義的業務委托給具體的委托對象。


2. 委托的具體場景

上一節講了委托的定義和它所包含的幾個角色,那么具體該怎么運用呢?我們以一個實際的例子來看看。
現在很多年輕人都愛完游戲,不管是吃雞、王者榮耀還是英雄聯盟。它們都是有等級之分的:青銅->白銀->黃金->鉑金->鉆石->宗師->王者,等級越高,代表你越厲害,就拿英雄聯盟來說,我們多數混跡在白銀黃金階段,要上鉆石宗師段位非常困難。比如你排位打了很久,就差幾場就能上宗師了,老是打不上去,這個時候怎么辦呢?好辦,現在有很多游戲代練,委托游戲代練給你打上去就好了。這其實就是一個委托模式。代碼該怎么寫呢?一起來看看:
首先,我們定義約束類,定義我們需要委托的業務,就拿這個場景來說,我們的業務就是打排位賽,升級。因此,定義個約束類(接口)IGamePlayer:




[代碼]java代碼:

// 約束類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技術雜貨鋪!"}
打印結果如下:
[email protected], thank you for delegating 'prop' to me!Hello, Android技術雜貨鋪! has been assigned to 'prop' in [email protected]




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 屬性實現ReadOnlyProperty,var屬性實現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:d120委托屬性為: 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 函數

vetoable 與 observable一樣,可以觀察屬性值的變化,不同的是,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的存儲和訪問。在我們軟件開發中,始終提倡的是高內聚,低耦合。而委托,就是內聚,可以降低耦合。另一方面,委托的使用,也能減少很多重復的樣板代碼。



作者:依然范特稀西
鏈接:https://www.jianshu.com/p/b13d27249959
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


  繼續閱讀全文



想在安卓巴士找到更多優質博文,可移步博客區

如果對本篇文章感興趣,請前往,
原文地址:
http://www.clfrpjw.com.cn/blog-720372-83939.html
分享到:  QQ好友和群 QQ空間 微信
收藏
收藏0
支持
支持0
反對
反對0
您需要登錄后才可以回帖 登錄 | 立即注冊

本版積分規則

領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

时时彩改欢乐生肖 山西扣点点麻将下载 湖北11选5技巧 体彩排列5开奖 陕西麻将 广东快乐十分开奖趋 悠闲麻将作弊辅助软件 网赚是什么 2018信誉最好的棋牌 四川快乐十二乐彩网 七乐彩100期开奖走势图带连线 浪潮信息股票怎么样 股票高位放量下跌意 多多棋牌官网 十分快乐开奖结果 六合秒秒官方网站 广西快3开奖预测号码