如何正確地使用Kotlin的屬性代理

簡述:
今天繼續(xù)Kotlin原創(chuàng)系列的第十一講,一起來揭開Kotlin屬性代理的漂亮外衣。屬性代理可以說是Kotlin獨有的強大的功能之一,特別是對于框架開發(fā)的小伙伴來說非常有用,因為會經(jīng)常涉及到更改存儲和修改屬性的方式操作,例如Kotlin中的SQL框架Exposed源碼就大量使用了屬性代理。相信你已經(jīng)在代碼也使用了諸如Delegates.observable()、Delegates.notNull()、Delegates.vetoable()或者自定義的屬性代理。也許你還停留用的階段或者對它還有點陌生,不用擔心這篇文章將會基本上解決你所有的疑惑。廢話不多說,直接來看一波章節(jié)導圖:

image

一、屬性代理的基本定義

  • 1、基本定義

屬性代理是借助于代理設計模式,把這個模式應用于一個屬性時,它可以將訪問器的邏輯代理給一個輔助對象。

可以簡單理解為屬性的setter、getter訪問器內(nèi)部實現(xiàn)是交給一個代理對象來實現(xiàn),相當于使用一個代理對象來替換了原來簡單屬性字段讀寫過程,而暴露外部屬性操作還是不變的,照樣是屬性賦值和讀取,只是setter、getter內(nèi)部具體實現(xiàn)變了。

  • 2、基本語法格式
class Student{
    var name: String by Delegate()
}

class Delegate{
    operator fun <T> getValue(thisRef: Any?, property: KProperty<*>): T{
        ...
    }
    operator fun <T> setValue(thisRef: Any?, property: KProperty<*>, value: T){
        ...
    }
}

屬性name將它訪問器的邏輯委托給了Delegate對象,通過by關鍵字對表達式Delegate()求值獲取這個對象。任何符合屬性代理規(guī)則都可以使用by關鍵字。屬性代理類必須要遵循getValue(),setValue()方法約定,getValue、setValue方法可以是普通方法也可以是擴展方法,并且是方法是支持運算符重載。如果是val修飾的屬性只需要具備getValue()方法即可。

屬性代理基本流程就是代理類中的getValue()方法包含屬性getter訪問器的邏輯實現(xiàn),setValue()方法包含了屬性setter訪問器的邏輯實現(xiàn)。當屬性name執(zhí)行賦值操作時,會觸發(fā)屬性setter訪問器,然后在setter訪問器內(nèi)部調(diào)用delegate對象的setValue()方法;執(zhí)行讀取屬性name操作時,會在getter訪問器中調(diào)用delegate對象的getValue方法.

  • 3、by關鍵字

by關鍵字實際上就是一個屬性代理運算符重載的符號,任何一個具備屬性代理規(guī)則的類,都可以使用by關鍵字對屬性進行代理。

二、常見屬性代理基本使用

屬性代理是Kotlin獨有的特性,我們自己去自定義屬性代理,當然Kotlin還提供了幾種常見的屬性代理實現(xiàn)。例如:Delegates.notNull(), Delegates.observable(), Delegates.vetoable()

  • 1、Delegates.notNull()的基本使用

Delegate.notNull()代理主要用于可以不在構(gòu)造器初始化時候初始化而是可以延遲到之后再初始化這個var修飾的屬性,它和lateinit功能類似,但是也有一些不同,不過它們都需要注意的一點是屬性的生命周期,開發(fā)者要做到可控,也就是一定要確保屬性初始化是在屬性使用之前,否則會拋出一個IllegalStateException.

package com.mikyou.kotlin.delegate

import kotlin.properties.Delegates

class Teacher {
    var name: String by Delegates.notNull()
}
fun main(args: Array<String>) {
    val teacher = Teacher().apply { name = "Mikyou" }
    println(teacher.name)
}

可能有的人并沒有看到notNull()有什么大的用處,先說下大背景吧就會明白它的用處在哪了?

大背景: 在Kotlin開發(fā)中與Java不同的是在定義和聲明屬性時必須要做好初始化工作,否則編譯器會提示報錯的,不像Java只要定義就OK了,管你是否初始化呢。我解釋下這也是Kotlin優(yōu)于Java地方之一,沒錯就是空類型安全,就是Kotlin在寫代碼時就讓你明確一個屬性是否初始化,不至于把這樣的不明確定義拋到后面運行時。如果在Java你忘記了初始化,那么恭喜你在運行時你就會拿到空指針異常。

問題來了: 大背景說完了那么問題也就來了,相比Java,Kotlin屬性定義時多出了額外的屬性初始化的工作。但是可能某個屬性的值在開始定義的時候你并不知道,而是需要執(zhí)行到后面的邏輯才能拿到。這時候解決方式大概有這么幾種:

方式A: 開始初始化的時給屬性賦值個默認值

方式B: 使用Delegates.notNull()屬性代理

方式C: 使用lateinit修飾屬性

以上三種方式有局限性,方式A就是很暴力直接賦默認值,對于基本類型還可以,但是對于引用類型的屬性,賦值一個默認引用類型對象就感覺不太合適了。方式B適用于基本數(shù)據(jù)類型和引用類型,但是存在屬性初始化必須在屬性使用之前為前提條件。方式C僅僅適用于引用類型,但是也存在屬性初始化必須在屬性使用之前為前提條件。

優(yōu)缺點分析:

屬性使用方式 優(yōu)點 缺點
方式A(初始化賦默認值) 使用簡單,不存在屬性初始化必須在屬性使用之前的問題 僅僅適用于基本數(shù)據(jù)類型
方式B(Delegates.notNull()屬性代理) 適用于基本數(shù)據(jù)類型和引用類型 1、存在屬性初始化必須在屬性使用之前的問題;
2、不支持外部注入工具將它直接注入到Java字段中
方式C(lateinit修飾屬性) 僅適用于引用類型 1、存在屬性初始化必須在屬性使用之前的問題;
2、不支持基本數(shù)據(jù)類型

使用建議: 如果能對屬性生命周期做很好把控的話,且不存在注入到外部字段需求,建議使用方式B;此外還有一個不錯建議就是方式A+方式C組合,或者方式A+方式B組合。具體看實際場景需求。

  • 2、Delegates.observable()的基本使用

Delegates.observable()主要用于監(jiān)控屬性值發(fā)生變更,類似于一個觀察者。當屬性值被修改后會往外部拋出一個變更的回調(diào)。它需要傳入兩個參數(shù),一個是initValue初始化的值,另一個就是回調(diào)lamba, 回調(diào)出property, oldValue, newValue三個參數(shù)。

package com.mikyou.kotlin.delegate

import kotlin.properties.Delegates

class Person{
    var address: String by Delegates.observable(initialValue = "NanJing", onChange = {property, oldValue, newValue ->
        println("property: ${property.name}  oldValue: $oldValue  newValue: $newValue")
    })
}

fun main(args: Array<String>) {
    val person = Person().apply { address = "ShangHai" }
    person.address = "BeiJing"
    person.address = "ShenZhen"
    person.address = "GuangZhou"
}

運行結(jié)果:

property: address  oldValue: NanJing  newValue: ShangHai
property: address  oldValue: ShangHai  newValue: BeiJing
property: address  oldValue: BeiJing  newValue: ShenZhen
property: address  oldValue: ShenZhen  newValue: GuangZhou

Process finished with exit code 0

  • 3、Delegates.vetoable()的基本使用

Delegates.vetoable()代理主要用于監(jiān)控屬性值發(fā)生變更,類似于一個觀察者,當屬性值被修改后會往外部拋出一個變更的回調(diào)。它需要傳入兩個參數(shù),一個是initValue初始化的值,另一個就是回調(diào)lamba, 回調(diào)出property, oldValue, newValue三個參數(shù)。與observable不同的是這個回調(diào)會返回一個Boolean值,來決定此次屬性值是否執(zhí)行修改。

package com.mikyou.kotlin.delegate

import kotlin.properties.Delegates

class Person{
    var address: String by Delegates.vetoable(initialValue = "NanJing", onChange = {property, oldValue, newValue ->
        println("property: ${property.name}  oldValue: $oldValue  newValue: $newValue")
        return@vetoable newValue == "BeiJing"
    })
}

fun main(args: Array<String>) {
    val person = Person().apply { address = "NanJing" }
    person.address = "BeiJing"
    person.address = "ShangHai"
    person.address = "GuangZhou"
    println("address is ${person.address}")
}

三、常見屬性代理的源碼分析

以上我們介紹了常見的屬性代理基本使用,如果僅僅停留在使用的階段,確實有點low了, 那么讓我們一起先來揭開它們的第一層外衣。先來看波Kotlin標準庫源碼中常見的屬性代理包結(jié)構(gòu)。

  • 1、源碼包結(jié)構(gòu)


    image
  • 2、關系類圖

image

Delegates: 是一個代理單例對象,里面有notNull、observable、vetoable靜態(tài)方法,每個方法返回不同的類型代理對象

NotNullVar: notNull方法返回代理對象的類

ObserableProperty: observable、vetoable方法返回代理對象的類

ReadOnlyProperty: 只讀屬性代理對象的通用接口

ReadWriteProperty: 讀寫屬性代理對象的通用接口

  • 3、Delegates.notNull()源碼分析

notNull()首先是一個方法,返回的是一個NotNullVar屬性代理實例;那么它處理核心邏輯就是NotNullVar內(nèi)部的setValue和getValue方法,一起來瞅一眼。

  public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
    }

    public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        this.value = value
    }

通過源碼可以看到一旦getValue中的value是為null,那么就會拋出一個IllegalStateException,也就是在使用該屬性之前沒有做初始化。實際上可以理解在訪問器getter加了一層判空的代理實現(xiàn)。

  • 4、Delegates.observable()源碼分析

observable()是一個方法,返回的是一個ObservableProperty屬性代理實例;那它是怎么做到在屬性值發(fā)生變化通知到外部的呢,其實很簡單,首先在內(nèi)部保留一個oldValue用于存儲上一次的值,然后就在ObservableProperty類setValue方法執(zhí)行真正賦值之后再向外部拋出了一個afterChange的回調(diào),并且把oldValue,newValue,property回調(diào)到外部,最終利用onChange方法回調(diào)到最外層。

 public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        val oldValue = this.value
        if (!beforeChange(property, oldValue, value)) {
            return
        }
        this.value = value
        afterChange(property, oldValue, value)
    }
 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)
 }    
  • 5、Delegates.vetoable()源碼分析
    vetoable()是一個方法,返回的是一個ObservableProperty屬性代理實例;通過上面源碼就可以發(fā)現(xiàn),在setValue方法中執(zhí)行真正賦值之前,會有一個判斷邏輯,根據(jù)beforeChange回調(diào)方法返回的Boolean決定是否繼續(xù)執(zhí)行下面的真正賦值操作。如果beforChange()返回false就終止此次賦值,那么observable也不能得到回調(diào),如果返回true則會繼續(xù)此次賦值操作,并執(zhí)行observable的回調(diào)。

四、屬性代理背后的原理和源碼反編譯分析

如果說第三節(jié)是揭開屬性代理第一層外衣,那么第四節(jié)將是揭開最后一層外衣了,你會看到屬性代理真正背后的原理,看完你會發(fā)現(xiàn)其實挺簡單的。不多說先上一個簡單例子

class Teacher {
    var name: String by Delegates.notNull()
    var age: Int by Delegates.notNull()
}

實際上,以上那行代碼是經(jīng)歷了兩個步驟:

class Teacher {
    private val delegateString: ReadWriteProperty<Teacher, String> = Delegates.notNull()
    private val delegateInt: ReadWriteProperty<Teacher, Int> = Delegates.notNull()
    var name: String by delegateString
    var age: Int by delegateInt
}

Kotlin反編譯后Java源碼

public final class Teacher {
   // $FF: synthetic field
   //關鍵點一
   static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Teacher.class), "name", "getName()Ljava/lang/String;")), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Teacher.class), "age", "getAge()I"))};
   //關鍵點二
   @NotNull
   private final ReadWriteProperty name$delegate;
   @NotNull
   private final ReadWriteProperty age$delegate;

   //關鍵點三
   @NotNull
   public final String getName() {
      return (String)this.name$delegate.getValue(this, $$delegatedProperties[0]);
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.name$delegate.setValue(this, $$delegatedProperties[0], var1);
   }

   public final int getAge() {
      return ((Number)this.age$delegate.getValue(this, $$delegatedProperties[1])).intValue();
   }

   public final void setAge(int var1) {
      this.age$delegate.setValue(this, $$delegatedProperties[1], var1);
   }

   public Teacher() {
      this.name$delegate = Delegates.INSTANCE.notNull();
      this.age$delegate = Delegates.INSTANCE.notNull();
   }
}

分析過程:

  • 1、首先, Teacher類的name和age屬性會自動生成對應的setter,getter方法,并且會自動生成對應的name$delegate、age$delegate委托對象,如代碼中標識的關鍵點二。
  • 2、然后,$$delegatedProperties的KProperty數(shù)組中會保存通過Kotlin反射出當前Teacher類中的中name,age屬性,反射出來每個屬性單獨對應保存在KProperty數(shù)組中。
  • 2、然后,在對應屬性setter,getter方法中是把具體的實現(xiàn)委托給對應的name$delegate、age$delegate對象的setValue、getValue方法來實現(xiàn)的,如代碼中標識的關鍵點三。
  • 3、最后在delegate對象中的setValue和getValue方法中的傳入對應反射出來的屬性以及相應的值。

五、自己動手實現(xiàn)屬性代理

有以上的介紹,自己寫個自定義的屬性代理應該很簡單了吧。實現(xiàn)一個簡單的屬性代理最基本架子就是setValue,getValue方法且無需實現(xiàn)任何的接口。

在Android中SharedPreferences實際上就是個很好場景,因為它涉及到了屬性存儲和讀取。自定義屬性代理實現(xiàn)Android中SharedPreferences可以直接實現(xiàn)自帶的ReadWriteProperty接口,當然也可以自己去寫一個類然后去定義相應的setValue方法和getValue方法。

class PreferenceDelegate<T>(private val context: Context, private val name: String, private val default: T, private val prefName: String = "default")
    : ReadWriteProperty<Any?, T> {
    private val prefs: SharedPreferences by lazy {
        context.getSharedPreferences(prefName, Context.MODE_PRIVATE)
    }

    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("setValue from delegate")
        return getPreference(key = name)
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        println("setValue from delegate")
        putPreference(key = name, value = value)
    }

    private fun getPreference(key: String): T {
        return when (default) {
            is String -> prefs.getString(key, default)
            is Long -> prefs.getLong(key, default)
            is Boolean -> prefs.getBoolean(key, default)
            is Float -> prefs.getFloat(key, default)
            is Int -> prefs.getInt(key, default)
            else -> throw IllegalArgumentException("Unknown Type.")
        } as T
    }

    private fun putPreference(key: String, value: T) = with(prefs.edit()) {
        when (value) {
            is String -> putString(key, value)
            is Long -> putLong(key, value)
            is Boolean -> putBoolean(key, value)
            is Float -> putFloat(key, value)
            is Int -> putInt(key, value)
            else -> throw IllegalArgumentException("Unknown Type.")
        }
    }.apply()

}

六、結(jié)語

到這里屬性代理的內(nèi)容就結(jié)束了,有沒有覺得Kotlin語言糖設計還是很巧妙的。雖然很多人抵觸語法糖,但不可否認的是它給我們開發(fā)在效率上帶來了很大的提升。有時候我們更多地是需要透過語法糖外衣,看到其背后的原理,弄清整個語法糖設計思路和技巧,以一個全局眼光去看待它,就會覺得它也就那么回事。最后,感謝一波bennyHuo大佬,我是先看到他sharedPreferences的屬性擴展例子,感覺很不錯,然后決定去深入探究一下屬性擴展,這下應該對Kotlin屬性擴展有了比較深的認識了。

<div align="center"><img src="https://user-gold-cdn.xitu.io/2018/5/14/1635c3fb0ba21ec1?w=430&h=430&f=jpeg&s=39536" width="200" height="200"></div>

歡迎關注Kotlin開發(fā)者聯(lián)盟,這里有最新Kotlin技術文章,每周會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 本文是在學習和使用kotlin時的一些總結(jié)與體會,一些代碼示例來自于網(wǎng)絡或Kotlin官方文檔,持續(xù)更新... 對...
    竹塵居士閱讀 3,461評論 0 8
  • 前言 人生苦多,快來 Kotlin ,快速學習Kotlin! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,669評論 9 118
  • 1用戶屬性(Custom delegate ) 語法結(jié)構(gòu)是: val/var : <Type> by <expr...
    泛光燈閱讀 1,525評論 0 1
  • 面向?qū)ο缶幊蹋∣OP) 在前面的章節(jié)中,我們學習了Kotlin的語言基礎知識、類型系統(tǒng)、集合類以及泛型相關的知識。...
    Tenderness4閱讀 4,607評論 1 6
  • 從前,有一個小姑娘叫茉莉,她特別喜歡裙子??墒?,她家里很窮,買不起那么貴重的的裙子。所以她就拿來報紙,在報紙...
    TIANER閱讀 303評論 1 2

友情鏈接更多精彩內(nèi)容