Kotlin學習快速入門(8)—— 委託

原文地址:Kotlin學習快速入門(8)—— 屬性委託 - Stars-One的雜貨小窩
委託其實是一種設計模式,但Kotlin把此特性編寫進了語法中,可以方便開發者快速使用

委託對應的關鍵字是by

屬性委託

先講下屬性委託吧,首先,複習下kotlin中設置set和get方法

默認的set和get我們可以隱藏,實際上一個簡單的類代碼如下:

class Person {
	var personName = ""
	// 這是默認的 get/set(默認是隱藏的)
	get() = field
	set(value) {
		field = value
	}
}

這裏具體知識點可以查看之前所說Kotlin學習快速入門(3)——類 繼承 接口 - Stars-One的雜貨小窩

當然,如果是數據bean類,我們會將get和set方法隱藏(或者使用data關鍵字來聲明一個數據類)

若我們需要在get或set方法的時候做一下邏輯處理,比如說上面的personName字段,我們只允許接收長度小於等於10的字符串,超過10長度的字符串就不接收(即不設置新數值),則是應該這樣寫:

class Person{
    var personName = ""
        // 這是重寫的 get/set
        get() = "PersonName $field"
        set(value) {
            field = if (value.length <= 10) value else field
        }
}

然後,我們再延伸出來,如果此規則不止應用於personName字段,還可用到其他類的字段中,這個時候就是使用到屬性委託。

簡單描述: 我們將此規則抽取出來,需要應用到此規則的字段的get/set方法委託給規則去做,這就叫屬性委託

延遲加載(懶加載)

在開始講屬性委託之前,先說明下延遲加載

Kotlin中提供了lazy方法,使用by+lazy{}聯用,我們就實現延遲加載(也可稱作懶加載)

fun main() {

    val demo = Demo()
    val textContent = demo.textContent
    val result = demo.textContent.substring(1)
    println(result)
    println("打印:$textContent")
}

class Demo{

    val textContent by lazy { loadFile() }

}
fun loadFile(): String {
    println("讀取文件...")
    //模擬讀取文件返回數據
    return "讀取的數據"
}

這裏的關鍵詞by出現在屬性名後面,表示屬性委託,即將屬性的讀和寫委託給另一個對象,被委託的對象必須滿足一定的條件:

  • 對於 val 修飾的只讀變量進行屬性委託時,被委託的對象必須實現getValue()接口,即定義如何獲取變量值。
  • 對於 var 修飾的讀寫變量進行屬性委託時,被委託對象必須實現getValue()setValue()接口,即定義如何讀寫變量值。

lazy()方法,接收一個lambda函數,返回值是一個Lazy對象,所以就可以簡寫成上面的樣子,其只實現了getValue()接口,所以,當你嘗試將textContent改爲var類型,IDE會提示報錯!!

也是因爲這點,屬於延遲加載的字段,是不可被再次修改了,所以採用lazy懶加載的方式,其實就是單例模式

Delegates.vetoable

還記得上述我們要實現的規則嗎,其實Kotlin中已經有了幾個默認的委託規則供我們快速使用(上述的lazy其實也是一個)

Delegates.vetoable()的規則就是上述規則的通用封裝,解釋爲:

但會在屬性被賦新值生效之前會傳遞給Delegates.vetoable()進行處理,依據Delegates.vetoable()的返回的布爾值判斷要不要賦新值。

如下面例子:

class Person {
    var personName by Delegates.vetoable("") { property, oldValue, newValue ->
        //當設置的新值滿足條件,則會設置爲新值
        newValue.length <= 10
    }
}

Delegates.notNull

設置字段不能爲null,不過想不到具體的應用情景

class Person {
    var personName by Delegates.notNull<String>()
}

Delegates.observable

使用Delegates.observable可以幫我們快速實現觀察者模式,只要字段數值發生改變,就會觸發

class Person{
    var age by Delegates.observable(0){ property, oldValue, newValue ->
        //這裏可以寫相關的邏輯
        if (newValue >= 18) {
            tip = "已成年"
        }else{
            tip = "未成年"
        }
    }

    var tip =""
}

上面的例子就比較簡單,設置age同時更新提示,用來判斷是否成年

 val person = Person()
    person.age = 17
    println(person.tip)

補充-自定義委託

上述都是官方定義好的一些情形,但如果不滿足我們的需求,這就需要自定義委託了

官方提供了兩個基礎類供我們自定義委託使用:

ReadWriteProperty 包含get和set方法,對應var關鍵字
ReadOnlyProperty 只有get方法,對應val關鍵字

PS:實際上,我們自己隨意創建個委託類也是可以的,不過這樣寫不太規範,所以我們一般直接實現官方給的上述兩個類即可

ReadWriteProperty和ReadOnlyProperty都需要傳兩個泛型,分別爲R,T

  • R 持有屬性的類型
  • T 字段類型

可能上面描述不太明白,下面給個簡單例子,Person類中有個name字段(String),首字母需要大寫:

class Person {
    var name by NameToUpperCase("")
}

class NameToUpperCase(var value:String) :ReadWriteProperty<Person,String>{
    //NameToUpperCase類中默認有個屬性字段,用來存數據
    
    override fun getValue(thisRef: Person, property: KProperty<*>): String {
        return this.value
    }

    override fun setValue(thisRef: Person, property: KProperty<*>, value: String) {
        //在設置數值的時候,將第一個字母轉爲大寫,一般推薦在setValue裏編寫邏輯
        this.value = value.substring(0,1).toUpperCase()+value.substring(1)
    }
}

個人看法,一般在setValue的時候進行設置數值比較好,因爲getValue作操作的話,會觸發多次,處理的邏輯複雜的話可能會浪費性能...

當然,再提醒下,上面的邏輯也可以直接去字段裏的setValue()裏面改,委託只是方便抽取出去供其他類應用同樣的規則

PS: 如果你的委託不是針對特定的類,R泛型可以改爲Any

類委託

這個一般與多態一起使用,不過個人想不到有什麼具體的應用情景,暫時做下簡單的記錄

interface IDataStorage{
    fun add()
    fun del()
    fun query()
}

class SqliteDataStorage :IDataStorage{
    override fun add() {
        println("SqliteDataStorage add")
    }

    override fun del() {
        println("SqliteDataStorage del")
    }

    override fun query() {
        println("SqliteDataStorage query")
    }

}

假如現在我們有個MyDb類,查詢的方法與SqliteDataStorage這個裏的方法有所區別,但其他方法都是沒有區別,這個時候就會用到類委託了

有以下幾種委託的使用方式

1.委託類作爲構造器形參傳入(常用)

class MyDb(private val storage:IDataStorage) : IDataStorage by storage{
    override fun add() {
        println("mydb add")
    }
}
val db = MyDb(SqliteDataStorage())
db.add()
db.query()

輸出結果:

mydb add
SqliteDataStorage query

如果是全部都是委託給SqliteDataStorage的話,可以簡寫爲這樣:

class MyDb(private val storage:IDataStorage) : IDataStorage by storage

2.新建委託類對象

class MyDb : IDataStorage by SpDataStorage(){
    override fun add() {
        println("mydb add")
    }
}

這裏測試的效果與上文一樣,不在重複贅述

參考

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