關(guān)于Kotlin委托你必須重視的幾個(gè)點(diǎn)
前言
委托模式是實(shí)現(xiàn)繼承的一個(gè)很好的替代方式,也是Kotlin語(yǔ)言的一種特性,可以很優(yōu)雅的實(shí)現(xiàn)委托模式。在開(kāi)發(fā)過(guò)程中也少不了使用它,但常常都會(huì)被低估。所以今天就讓它得到重視,去充分的掌握kotlin委托特性以及原理。
一、委托類
我們先完成一個(gè)委托類,常常用于實(shí)現(xiàn)類的委托模式,它的關(guān)鍵是通過(guò)by關(guān)鍵字:
interface Base{
fun print()
}
class BaseImpl(val x: Int): Base{
override fun print() { print(x) }
}
class Derived(b: Base): Base by b
fun main(){
val b = BaseImpl(10)
Deriived(b).print()
}
//最后輸出了10在這個(gè)委托模式中Derived相當(dāng)于是個(gè)包裝,雖然也實(shí)現(xiàn)了base,但并不關(guān)心它怎么實(shí)現(xiàn),通過(guò)by這個(gè)關(guān)鍵字,將接口的實(shí)現(xiàn)委托給了它的參數(shù)db。
相當(dāng)于Java代碼的結(jié)構(gòu):
class Derived implements Base{
Base b;
public Derived(Base b){ this.b = b}
}二、 委托屬性
前面講到Kotlin委托類是委托的是接口方法,委托屬性委托的,則是屬性的getter和setter方法。kotlin支持的委托屬性語(yǔ)法:
class Example {
var prop: String by Delegate()
}屬性對(duì)應(yīng)的get()和set()會(huì)被委托給它的getValue和setValue方法。當(dāng)然屬性的委托不是隨便寫(xiě)的,對(duì)于與val屬性它必須要提供一個(gè)getValue函數(shù),如果是var屬性的則要另外提供setValue屬性。先來(lái)看個(gè)官方提供的委托屬性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.")
}
}我們可以看到對(duì)于var修飾的屬性,必須要有g(shù)etValue和setValue方法,同時(shí)這兩個(gè)方法必須有operator關(guān)鍵字的修飾。
再來(lái)看第一個(gè)參數(shù)thisRef,它的類型是要這個(gè)屬性所有者的類型,或者是它的父類。當(dāng)我們不確定屬性會(huì)屬于哪個(gè)類,就可以將thisRef的類型定義為Any?了。
接著看另一個(gè)參數(shù)property,它的類型是必須要KProperty<*>或其超類型,它的值則是前面的字段的名字prop。
最后一個(gè)參數(shù),它的類型必須是委托屬性的類型,或者是它的父類。也就是說(shuō)例子中的 value: String 也可以換成 value: Any。
我們來(lái)測(cè)試下到底是不是這樣的:
fun main() {
println(Example().prop)
Example().prop = "Hello, World"
}則會(huì)看到輸出:
Example@5197848c, thank you for delegating 'prop' to me! Hello, World has been assigned to 'prop' in Example@17f052a3.
2.1 自定義委托
在知道了委托屬性怎么寫(xiě)之后,也可以根據(jù)需求來(lái)實(shí)現(xiàn)自己的屬性委托。但是每次寫(xiě)都要寫(xiě)那么多模板代碼,也是很麻煩的,所以官方也提供了接口類給我們快速實(shí)現(xiàn):
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)
}現(xiàn)在被委托的類只要實(shí)現(xiàn)這個(gè)接口的其中一個(gè)就可以了。對(duì)于 val 變量使用 ReadOnlyProperty,而 var 變量實(shí)現(xiàn)ReadWriteProperty。我們現(xiàn)在就用ReadWriteProperty 接口來(lái)實(shí)現(xiàn)一個(gè)自定義委托:
class Owner {
var text: String by StringDelegate()
}
class StringDelegate(private var s: String = "Hello"): ReadWriteProperty<Owner, String> {
override operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
return s
}
override operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
s = value
}
}三、委托進(jìn)階
3.1 懶加載委托
懶加載委托,也就是我們?cè)賹?duì)一些資源進(jìn)行操作的時(shí)候,希望它在被訪問(wèn)的時(shí)候采取觸發(fā),避免不必要的消耗。官方已經(jīng)幫我們提供了一個(gè)lazy()方法來(lái)快速創(chuàng)建懶加載委托:
val lazyData: String by lazy {
request()
}
fun request(): String {
println("執(zhí)行網(wǎng)絡(luò)請(qǐng)求")
return "網(wǎng)絡(luò)數(shù)據(jù)"
}
fun main() {
println("開(kāi)始")
println(lazyData)
println(lazyData)
}
//結(jié)果:
開(kāi)始
執(zhí)行網(wǎng)絡(luò)請(qǐng)求
網(wǎng)絡(luò)數(shù)據(jù)
網(wǎng)絡(luò)數(shù)據(jù)
復(fù)制代碼可以看到只有第一次調(diào)用,才會(huì)執(zhí)行l(wèi)ambda表達(dá)式里的邏輯,后面再調(diào)用只會(huì)返回lambda表達(dá)式的最終結(jié)果。
那么懶加載委托又是怎么實(shí)現(xiàn)的呢? 現(xiàn)在來(lái)看下它的源代碼:
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}在這個(gè)里面lazy()方法會(huì)接收一個(gè)LazyThreadSafetyMod類型的參數(shù),如果不傳這個(gè)參數(shù)的話,就會(huì)默認(rèn)使用SynchronizedLazyImpl方式。看解釋就可以知道它是用來(lái)多線程同步的,而另外兩個(gè)則不是多線程安全的。
LazyThreadSafetyMode.PUBLICATION:初始化方法可以被多次調(diào)用,但是值只是第一次返回時(shí)的返回值,也就是只有第一次的返回值可以賦值給初始化的值。
LazyThreadSafetyMode. NONE:如果初始化將總是發(fā)生在與屬性使用位于相同的線程,這種情況下可以使用,但它沒(méi)有同步鎖。
我們現(xiàn)在主要來(lái)看下SynchronizedLazyImpl里面做了什么事情:
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this
override val value: T
get() {
val _v1 = _value
//判斷是否已經(jīng)初始化過(guò),如果初始化過(guò)直接返回,不在調(diào)用高級(jí)函數(shù)內(nèi)部邏輯
//如果這兩個(gè)值不相同,就說(shuō)明當(dāng)前的值已經(jīng)被加載過(guò)了,直接返回
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
//調(diào)用高級(jí)函數(shù)獲取其返回值
val typedValue = initializer!!()
//將返回值賦值給_value,用于下次判斷時(shí),直接返回高級(jí)函數(shù)的返回值
_value = typedValue
initializer = null
typedValue
}
}
}
......
}通過(guò)上面代碼,可以發(fā)現(xiàn)SynchronizedLazyImpl覆蓋了lazy接口的返回值,并且重寫(xiě)了屬性的訪問(wèn)器,具體邏輯是與Java的雙重校驗(yàn)類似的。但Lazy接口又是怎么變成委托屬性的?
在Lazy.kt文件中發(fā)現(xiàn)它聲明了Lazy接口的getValue擴(kuò)展屬性,也就在最終賦值的時(shí)候會(huì)被調(diào)用,而我們?cè)谧远x委托中說(shuō)過(guò),對(duì)于val屬性,我們需要提供一個(gè)getValue函數(shù)。
## Lazy.kt //此擴(kuò)展允許使用 Lazy 的實(shí)例進(jìn)行屬性委托 public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
有了這個(gè)懶加載委托后,我們實(shí)現(xiàn)單例會(huì)變的更加簡(jiǎn)單:
class SingletonDemo private constructor() {
companion object {
val instance: SingletonDemo by lazy{
SingletonDemo() }
}
}3.2 Delegates.observable 觀察者委托
如果你要觀察一個(gè)屬性的變化過(guò)程,可以將屬性委托給Delegates.observable,它有三個(gè)參數(shù):被賦值的屬性、舊值和新值:
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}返回了一個(gè)ObservableProperty 對(duì)象,繼承自 ReadWriteProperty。再來(lái)看下它的內(nèi)部實(shí)現(xiàn):
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)
}initialValue是初始值,而另外個(gè)參數(shù)onChange是屬性值被修改時(shí)的回調(diào)處理器。
3.3 by map 映射委托
一個(gè)常見(jiàn)的用例是在一個(gè)映射(map)里存儲(chǔ)屬性的值,它可以使用Map/MutableMap來(lái)實(shí)現(xiàn)屬性委托:
class User(val map: Map<String, Any?>) {
val name: String by map
}
fun main(args: Array<String>) {
val map = mutableMapOf(
"name" to "哈哈"
)
val user = User(map)
println(user.name)
map["name"] = "LOL"
println(user.name)
}
//輸出:
哈哈
LoL不過(guò)在使用過(guò)程中會(huì)有個(gè)問(wèn)題,如果Map中不存在委托屬性名的映射值的時(shí)候,會(huì)再取值時(shí)拋異常:Key $key is missing in the map:
## MapAccessors.kt
public inline operator fun <V, V1 : V> MutableMap<in String, out @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1 = (getOrImplicitDefault(property.name) as V1)
@kotlin.internal.InlineOnly
public inline operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property: KProperty<*>, value: V) {
this.put(property.name, value)
}
## MapWithDefault.kt
internal fun <K, V> Map<K, V>.getOrImplicitDefault(key: K): V {
if (this is MapWithDefault)
return this.getOrImplicitDefault(key)
return getOrElseNullable(key, { throw NoSuchElementException("Key $key is missing in the map.") })
}所以在使用的時(shí)候要注意,必須要有映射值。
3.4 兩個(gè)屬性之間的直接委托
從 Kotlin 1.4 開(kāi)始,我們可以直接在語(yǔ)法層面將“屬性 A”委托給“屬性 B”,就如下示例:
class Item {
var count: Int = 0
var total: Int by ::count
}上面代碼total的值與count完全一致,因?yàn)槲覀儼裻otal這個(gè)屬性的getter和setter都委托給了count。可以用代碼來(lái)解釋下具體的邏輯:
class Item {
var count: Int = 0
var total: Int
get() = count
set(value: Int) {
count = value
}
}在寫(xiě)法上,委托名稱可以使用":"限定符,比如this::delegate 或MyClass::delegate。
這種用法在字段發(fā)生改變,又要保留原有的字段時(shí)非常有用??梢远x一個(gè)新字段,然后將其委托給原來(lái)的字段,這樣就不用擔(dān)心新老字段數(shù)值不一樣的問(wèn)題了。
3.5 提供委托
如果要在綁定屬性委托之前再做一些額外的判斷工作要怎么辦?我們可以定義provideDelegate來(lái)實(shí)現(xiàn):
class StringDelegate(private var s: String = "Hello") {
operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
return s
}
operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
s = value
}
}
class SmartDelegator {
operator fun provideDelegate(
thisRef: Owner,
prop: KProperty<*>
): ReadWriteProperty<Owner, String> {
//根據(jù)屬性委托的名字傳入不同的初始值
return if (prop.name.contains("log")) {
StringDelegate("log")
} else {
StringDelegate("normal")
}
}
}
class Owner {
var normalText: String by SmartDelegator()
var logText: String by SmartDelegator()
}
fun main() {
val owner = Owner()
println(owner.normalText)
println(owner.logText)
}
//結(jié)果:
normal
log這里我們創(chuàng)建了一個(gè)新的SmartDelegator,通過(guò)對(duì)成員方法provideDelegate再套了一層,然后在里面進(jìn)行一些邏輯判斷,最后才把屬性委托g(shù)etStringDelegate。
這種攔截屬性與其委托之間的綁定的能力,大大縮短了要實(shí)現(xiàn)相同功能,還要必須傳遞屬性名的邏輯。
四、委托栗子
4.1 簡(jiǎn)化Fragment / Activity 傳參
平時(shí)在針對(duì)Fragment傳參,每次都要寫(xiě)一大段代碼是不是很煩,現(xiàn)在有了委托這個(gè)法寶就來(lái)一起簡(jiǎn)化它,正常模式如下:
class BookDetailFragment : Fragment(R.layout.fragment_book_detail) {
private var bookId: Int? = null
private var bookType: Int? = null
companion object {
const val EXTRA_BOOK_ID = "bookId"
const val EXTRA_BOOK_TYPE = "bookType";
fun newInstance(bookId: Int, bookType: Int?) = BookDetailFragment().apply {
Bundle().apply {
putInt(EXTRA_BOOK_ID, bookId)
if (null != bookType) {
putInt(EXTRA_BOOK_TYPE, bookType)
}
}.also {
arguments = it
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
bookId = it.getInt(EXTRA_book_ID, 123)
bookType = it.getInt(EXTRA_BOOK_TYPE, 1)
}
}
}寫(xiě)了那么一大段,終于寫(xiě)好了傳參的基本方法,在獲取值的時(shí)候還要處理參數(shù)為空的情況,現(xiàn)在我們就抽取委托類用屬性委托的方式重新實(shí)現(xiàn)上面功能:
class BookDetailFragment : Fragment(R.layout.fragment_book_detail) {
private var bookId: Int by argument()
companion object {
fun newInstance(bookId: Int, bookType: Int) = BookDetailFragment().apply {
this.bookId = bookId
}
}
override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
Log.d("tag", "BOOKID:" + bookId);
}
}看上去減少了大量代碼,是不是很神奇,下面實(shí)現(xiàn)思路如下所示:
class FragmentArgumentProperty<T> : ReadWriteProperty<Fragment, T> {
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
//對(duì)Bunndle取值還要進(jìn)行單獨(dú)處理
return thisRef.arguments?.getValue(property.name) as? T
?: throw IllegalStateException("Property ${property.name} could not be read")
}
override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
val arguments = thisRef.arguments ?: Bundle().also { thisRef.arguments = it }
if (arguments.containsKey(property.name)) {
// The Value is not expected to be modified
return
}
//對(duì)Bunndle設(shè)值還要進(jìn)行單獨(dú)處理
arguments[property.name] = value
}
}
fun <T> Fragment.argument(defaultValue: T? = null) = FragmentArgumentProperty(defaultValue)4.2 簡(jiǎn)化SharedPreferences存取值
如果我們現(xiàn)在存取值可以這樣做是不是很方便:
private var spResponse: String by PreferenceString(SP_KEY_RESPONSE, "") // 讀取,展示緩存 display(spResponse) // 更新緩存 spResponse = response
答案是可以的,還是用委托屬性來(lái)改造,下面就是具體的實(shí)現(xiàn)示例:
class PreDelegate<T>(
private val name: String,
private val default: T,
private val isCommit: Boolean = false,
private val prefs: SharedPreferences = App.prefs) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return getPref(name, default) ?: default
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
value?.let {
putPref(name, value)
}
}
private fun <T> getPref(name: String, default: T): T? = with(prefs) {
val result: Any? = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default)
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw IllegalArgumentException("This type is not supported")
}
result as? T
}
private fun <T> putPref(name: String, value: T) = with(prefs.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type is not supported")
}
if (isCommit) {
commit()
} else {
apply()
}
}
}4.3 數(shù)據(jù)與View的綁定
有了委托之后,在不用到DataBinding,數(shù)據(jù)與View之間也可以進(jìn)行綁定。
operator fun TextView.provideDelegate(value: Any?, property: KProperty<*>) = object : ReadWriteProperty<Any?, String?> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String? = text
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
text = value
}
}給TextView寫(xiě)一個(gè)擴(kuò)展函數(shù),讓它支持了String屬性的委托。
val textView = findViewById<textView>(R.id.textView) var message: String? by textView textView.text = "Hello" println(message) message = "World" println(textView.text) //結(jié)果: Hello World
我們通過(guò)委托的方式,將 message 委托給了 textView。這意味著,message 的 getter 和 setter 都將與 TextView 關(guān)聯(lián)到一起。
五、小結(jié)
主要講解了 Kotlin 委托的用法和本質(zhì),總共有兩類委托類和委托屬性,特別是屬性委托要值得重視。在開(kāi)發(fā)中其實(shí)有很多場(chǎng)景可以用委托來(lái)進(jìn)行簡(jiǎn)化,減少很多重復(fù)的樣板代碼,可以說(shuō)跟擴(kuò)展比起來(lái)也毫不遜色。
參考
Kotlin Jetpack 實(shí)戰(zhàn) | 07. Kotlin 委托
Kotlin 委托的本質(zhì)以及 MMKV 的應(yīng)用
Kotlin | 委托機(jī)制 & 原理 & 應(yīng)用
總結(jié)
到此這篇關(guān)于關(guān)于Kotlin委托的文章就介紹到這了,更多相關(guān)Kotlin委托重視的幾個(gè)點(diǎn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決Android studio用真機(jī)調(diào)試時(shí)logcat一直輸出日志問(wèn)題
這篇文章主要介紹了解決Android studio用真機(jī)調(diào)試時(shí)logcat一直輸出日志問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-04-04
Android自定義控件制作顯示進(jìn)度的Button
這篇文章主要為大家詳細(xì)介紹了Android自定義控件制作顯示進(jìn)度的Button,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-08-08
Android HttpURLConnection.getResponseCode()錯(cuò)誤解決方法
在使用HttpURLConnection.getResponseCode()的時(shí)候直接報(bào)錯(cuò)是IOException錯(cuò)誤,一直想不明白,同一個(gè)程序我調(diào)用了兩次,結(jié)果有一個(gè)鏈接一直O(jiān)K,另一個(gè)卻一直報(bào)這個(gè)錯(cuò)誤2013-06-06
Android編程中出現(xiàn)The connection to adb is down問(wèn)題的解決方法
這篇文章主要介紹了Android編程中出現(xiàn)The connection to adb is down問(wèn)題的解決方法,涉及Android進(jìn)程與服務(wù)的相關(guān)操作技巧,需要的朋友可以參考下2015-12-12
Android使用recyclerview打造真正的下拉刷新上拉加載效果
這篇文章先介紹如何使用這個(gè)recyclerview,WZMRecyclerview 是一個(gè)集成了 下拉刷新、上拉加載、滑到底部自動(dòng)加載、添加刪除頭尾部 四個(gè)主要功能的recyclerview,需要的朋友可以參考下2016-11-11
Android用HandlerThread模擬AsyncTask功能(ThreadTask)
本文主要講用HandlerThread模擬AsyncTask功能,這里提供實(shí)例代碼以便參考,有需要的小伙伴可以參考下2016-07-07
Android學(xué)習(xí)教程之滑動(dòng)布局(14)
這篇文章主要為大家詳細(xì)介紹了Android學(xué)習(xí)教程之滑動(dòng)布局使用方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11
Android中PathMeasure仿支付寶支付動(dòng)畫(huà)
這篇文章主要為大家詳細(xì)介紹了Android中PathMeasure仿支付寶支付動(dòng)畫(huà),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
建造者模式_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
建造者實(shí)現(xiàn)抽象類的所有未實(shí)現(xiàn)的方法,具體來(lái)說(shuō)一般是兩項(xiàng)任務(wù),組建產(chǎn)品;返回組建好的產(chǎn)品2017-08-08
Android實(shí)現(xiàn)應(yīng)用程序的閃屏效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)應(yīng)用程序的閃屏效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07

