Kotlin 基礎 | 委託及其應用
作者:唐子玄
連結: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出現在屬性名後面,表示屬性委託,即將屬性的讀和寫委託給另一個物件,被委託的物件必須滿足一定的條件:
-
對於 val 修飾的只讀變數進行屬性委託時,被委託的物件必須實現getValue()介面,即定義如何獲取變數值。
-
對於 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 的鍵。
總結
-
Kotlin 委託分為類委託和屬性委託。它們都通過關鍵詞by來進行委託。
-
類委託可以用簡潔的語法將類的實現委託給另一個物件,以減少模板程式碼。
-
屬性委託可以將對屬性的訪問委託給另一個物件,以減少模板程式碼並隱藏訪問細節。
-
屬性委託有三種實現方式,分別是擴充套件方法、實現ReadWriteProperty介面、過載運算子。
掃描二維碼
獲取更多精彩
Android補給站

點個 在看 你最好看
- 協程到底是怎麼切換執行緒的?
- 元件化開花,就問你香不香
- Kotlin 基礎 | 拒絕語法噪音
- 玩轉LayoutInflater
- 淺談Android熱更新的前因後果
- 如何學好設計,做好架構? 核心思想才是關鍵
- Kotlin 內聯類 inline class請了解一下
- Android MaterialButton使用詳解,告別shape、selector
- 入木三分:從設計者角度看Retrofit原理
- 使用Jetpack Compose完成你的自定義Layout
- 為了能夠摸魚,我走上了歧路
- 自信,這是最好的ThreadLocal分析
- Android&Kotlin編譯速度原理剖析
- Android 面試之必問Java基礎
- Kotlin 基礎 | 委託及其應用
- 引入Jetpack架構後,你的App會發生哪些變化?
- Handler那些事
- android佈局優化的幾個建議
- 面試:ViewModel為何橫豎屏切換時不銷燬?
- Android Java Zygote啟動