Kotlin 基礎 | 委託及其應用

語言: CN / TW / HK

作者:唐子玄

連結:https://juejin.cn/post/6947830195034259469

委託是常見的模式,它和程式語言無關,即把本來自己做的事情委託給另一個物件去做。裝飾者模式和代理模式都通過委託複用了行為。Kotlin 在語言層面支援了委託,這一篇結合例項介紹一下 Kotlin 的委託。

Kotlin 的裝飾者模式

裝飾者模式和繼承擁有相同的目的,都是為了擴充套件類,只不過它運用了更復雜的方式通:繼承 + 組合。裝飾者模式在複用原有型別和行為的基礎上為其擴充套件功能。

下面是裝飾者模式的例項:

interface Accessory {
    fun name(): String // 配件名字
    fun cost(): Int //  配件價格
    fun type(): String // 配件類別
}

這個介面用來描述一個抽象的配件,一個具體的配件需要實現三個方法,分別來定義配件名字、價格、類別。

羽毛、戒指、耳環是3個具體的配件,它的實現如下:

class Feather: Accessory{
    override fun name(): String = "Feather"
    override fun cost(): Int  = 20
    override fun type(): String  = "body accessory"
}

class Ring: Accessory{
    override fun name(): String = "Ring"
    override fun cost(): Int  = 30
    override fun type(): String  = "body accessory"
}

class Earrings: Accessory{
    override fun name(): String = "Earrings"
    override fun cost(): Int  = 15
    override fun type(): String  = "body accessory"
}

現需要新增羽毛戒指和羽毛耳環,按照繼承的思想可以這樣實現:

class FeatherRing: Accessory{
    override fun name(): String = "FeatherRing"
    override fun cost(): Int  = 35
    override fun type(): String  = "body accessory"
}

class FeatherEarrings: Accessory{
    override fun name(): String = "FeatherEarrings"
    override fun cost(): Int  = 45
    override fun type(): String  = "body accessory"
}

這樣寫的缺點是隻複用了型別,沒複用行為。每次新增型別的時候都得新增一個子類,會造成子類膨脹。若改用裝飾者模式,則可以減少一個子類:

class Feather(private var accessory: Accessory) : Accessory {
    override fun name(): String = "Feather" + accessory.name()
    override fun cost(): Int = 20 + accessory.cost()
    override fun type(): String  = accessory.type()
}

現在羽毛戒指和耳環分別可以這樣表達Feather(Ring())、Feather(Earrings())。

Feather運用組合持有了一個抽象的配件,這樣被注入配件的行為就得以複用。name()和cost()在複用行為的基礎上追加了新的功能,而type()直接將實現委託給了accessory。

運用 Kotlin 的委託語法可以進一步簡化Feather類:

class Feather(private var accessory: Accessory): Accessory by accessory {
    override fun name(): String = "Feather" + accessory.name()
    override fun cost(): Int = 20 + accessory.cost()
}

by 關鍵詞出現在類名後面,表示類委託,即把類的實現委託一個物件,該物件必須實現和類相同的介面,在這裡是Accessory介面。使用by的好處是消滅模板程式碼,就如上面所示,type()介面的實現就可以省略。

惰性初始化一次

惰性初始化也是一種常見的模式:延遲物件的初始化,直到第一次訪問它。當初始化消耗大量資源,惰性初始化顯得特別有價值。

支援屬性是一種實現惰性初始化的慣用技術:

class BitmapManager {
    // 支援屬性用於儲存一組 Bitmap
    private var _bitmaps: List<Bitmap>? = null
    // 供外部訪問的一組 Bitmap
    val bitmaps: List<Bitmap>
        get() {
            if (_bitmaps == null) {
                _bitmaps = loadBitmaps()
            }
            return _bitmaps!!
        }
}

支援屬性_bitmaps是私有的,它用來儲存一組 Bitmap,而另一個同樣型別的bitmaps用來提供一組 Bitmap 的訪問。

這樣只有當第一次訪問BitmapManager.bitmaps時,才會去載入 Bitmap。第二次訪問時,也不會重新載入 Bitmap,可直接返回_bitmap。

上面這段程式碼就是 Kotlin 預定義函式lazy()內部運用的技術,有了它就可以消滅模板程式碼:

class BitmapManager {
    val bitmaps by lazy { loadBitmaps() }
}

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

  1. 對於 val 修飾的只讀變數進行屬性委託時,被委託的物件必須實現getValue()介面,即定義如何獲取變數值。

  2. 對於 var 修飾的讀寫變數進行屬性委託時,被委託物件必須實現getValue()和setValue()介面,即定義如何讀寫變數值。

屬性委託的三種實現方式

lazy()方法的返回值是一個Lazy物件:

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

public interface Lazy<out T> {
    public val value: T
    public fun isInitialized(): Boolean
}

Lazy類並沒有直接實現getValue()方法。它使用了另一種更加靈活的方式:

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

getValue()被宣告為Lazy類的擴充套件函式。這是 Kotlin 獨有的在類體外為類新增功能的特性。在原有類不能被修改的時候,特別好用。

除了擴充套件函式,還有另外兩種方式可以實現被委託類(假設代理的型別為 String):

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "Delegate"
    }

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

這種方式新建了一個代理類,並且在類中通過關鍵詞operator過載了getValue()和setValue()這兩個運算子,分別對應取值和設定操作。

最後一種方式如下(假設代理的型別為 String):

class Delegate : ReadWriteProperty<Any?, String> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "Delegate"
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    }
}

即實現ReadWriteProperty介面中的getValue()和setValue()方法。

然後就可以像這樣使用代理類:

class Test {
    var str: String by Delegate()
}

屬性委託背後的實現如下:

class Test {
    private delegate = Delegate()
    var str : String
        get () = delegate.getValue(this, kProperty)
        set (value: String) = delegate.setValue(this, kProperty, value)
}

新建的Delegate類會被儲存到一個支援屬性delegate中,委託屬性的設定和取值方法的實現全權委託給代理類。

委託之後,當訪問委託屬性時就好比在呼叫代理類的方法:

val test = Text()
val str = test.str // 等價於 val str = test.delegate.getValue(test, kProperty)
val test.str = str // 等價於 test.delegate.setValue(test, Kproperty, str)

委託應用

更簡便地獲取傳參

委託可以隱藏細節,特別是當細節是一些模板程式碼的時候:

class TestFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val id = arguments?.getString("id") ?: ""
    }
}

class KotlinActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val id = intent?.getStringExtra("id") ?: ""
    }
}

獲取傳遞給 Activity 或 Fragment 值的程式碼就很模板。可以使用委託隱藏一下細節:

// 新建 Extras 類作為被委託類
class Extras<out T>(private val key: String, private val default: T) {
    // 過載取值操作符
    operator fun getValue(thisRef: Any, kProperty: KProperty<*>): T? =
        when (thisRef) {
            // 獲取傳遞給 Activity 的引數
            is Activity -> { thisRef.intent?.extras?.get(key) as? T ?: default }
            // 獲取傳遞給 Fragment 的引數
            is Fragment -> { thisRef.arguments?.get(key) as? T ?: default }
            else -> default
        }
}

然後就可以像這樣使用委託:

class TestActivity : AppCompatActivity() {
    private val id by Extras("id","0")
}

class TestFragment : Fragment() {
    private val id by Extras("id","0")
}

更簡便地獲取 map 值

有些類的屬性不是固定的,而是有時多,有時少,即動態的,比如:

class Person {
    private val attrs = hashMapOf<String, Any>()
    fun setAttrs( key: String, value: Any){
        attrs[key] = value
    }
    val name: String
        get() = attrs["name"]
}

有些Person有孩子,有些沒有,所以不同Person例項擁有的屬性集是不同的。這種場景用Map來儲存屬性就很合適。

上述程式碼可以用委託簡化:

class Person {
    private val attrs = hashMapOf<String, Any>()
    fun setAttrs( key: String, value: Any){
        attrs[key] = value
    }
    val name: String by attrs
}

將name的獲取委託給一個 map 物件。神奇之處在於,甚至都不需要指定key就可以正確地從 map 中獲取 name 屬性值。這是因為 Kotlin 標準庫已經為 Map 定義了getValue()和setValue()擴充套件函式。屬性名將自動作用於 map 的鍵。

總結

  1. Kotlin 委託分為類委託和屬性委託。它們都通過關鍵詞by來進行委託。

  2. 類委託可以用簡潔的語法將類的實現委託給另一個物件,以減少模板程式碼。

  3. 屬性委託可以將對屬性的訪問委託給另一個物件,以減少模板程式碼並隱藏訪問細節。

  4. 屬性委託有三種實現方式,分別是擴充套件方法、實現ReadWriteProperty介面、過載運算子。

掃描二維碼

獲取更多精彩

Android補給站

點個 在看 你最好看