Kotlin常用的by lazy你真的瞭解嗎

語言: CN / TW / HK

highlight: a11y-dark

「這是我參與2022首次更文挑戰的第10天,活動詳情檢視:2022首次更文挑戰

前言

在使用Kotlin語言進行開發時,我相信很多開發者都信手拈來地使用by或者by lazy來簡化你的屬性初始化,但是by lazy涉及的知識點真的瞭解嗎 假如讓你實現這個功能,你會如何設計。

正文

話不多說,我們從簡單的屬性委託by來說起。

委託屬性

什麼是委託屬性呢,比較官方的說法就是假如你想實現一個比較複雜的屬性,它們處理起來比把值儲存在支援欄位中更復雜,但是卻不想在每個訪問器都重複這樣的邏輯,於是把獲取這個屬性例項的工作交給了一個輔助物件,這個輔助物件就是委託。

比如可以把這個屬性的值儲存在資料庫中,一個Map中等,而不是直接呼叫其訪問器。

看完這個委託屬性的定義,假如你不熟悉Kotlin也可以理解,就是我這個類的例項由另一個輔助類物件來提供,但是這時你可能會疑惑,上面定義中說的支援欄位和訪問器是什麼呢,這裡順便給不熟悉Kotlin的同學普及一波。

Java的屬性

當你定義一個Java類時,在定義欄位時並不是所有欄位都是屬性,比如下面程式碼:

```js //Java類 public class Phone {

//3個欄位
private String name;
private int price;
private int color;

//name欄位訪問器
private String getName() {
    return name;
}

private void setName(String name){
    this.name = name;
}

//price欄位訪問器
private int getPrice() {
    return price;
}

private void setPrice(int price){
    this.price = price;
}

} ``` 上面我在Phone類中定義了3個欄位,但是隻有name和price是Phone的屬性,因為這2個欄位有對應的get和set,也只有符合有getter和setter的欄位才叫做屬性。

這也能看出Java類的屬性值是儲存在欄位中的,當然你也可以定義setXX函式和getXX函式,既然XX屬性沒有地方儲存,XX也是類的屬性。

Kotlin的屬性

而對於Kotlin的類來說,屬性定義就非常簡單了,比如下面類:

js class People(){ val name: String? = null var age: Int? = null } 在Kotlin的類中只要使用val/var定義的欄位,它就是類的屬性,然後會自帶getter和setter方法(val屬性相當於Java的final變數,是沒有set方法的),比如下面:

js val people = People() //呼叫name屬性的getter方法 people.name //呼叫age屬性的setter方法 people.age = 12 這時就有了疑問,為什麼上面程式碼定義name時我在後面給他賦值了即null值,和Java一樣不賦值可以嗎 還有個疑問就是在Java中是把屬性的值儲存在欄位中,那Kotlin呢,比如name這個屬性的值就儲存給它自己嗎

帶著問題,我們繼續分析。

Kotlin屬性訪問器

前面我們可知Java中的屬性是儲存在欄位中,或者不要欄位,其實Kotlin也可以,這個就是給屬性定義自定義setter方法和getter方法,如下程式碼:

js class People(){ val name: String? = null var age: Int = 0 //定義了isAbove18這個屬性 var isAbove18: Boolean = false get() = age > 18 } 比如這裡自定義了get訪問器,當再訪問這個屬性時,便會呼叫其get方法,然後進行返回值。

Kotlin屬性支援欄位field

這時一想那Kotlin的屬性值儲存在哪裡呢,Kotlin會使用一個field的支援欄位來儲存屬性。如下程式碼:

```js class People{ val name: String? = null var age: Int = 0 //返回field的值 get() = field //設定field的值 set(value){ Log.i("People", "舊值是$field 新值是$value ") field = value }

var isAbove18: Boolean = false
    get() = age > 18

} ``` 可以發現每個屬性都會有個支援欄位field來儲存屬性的值。

好了,為了介紹為什麼Kotlin要有委託屬性這個機制,假如我在一個類中,需要定義一個屬性,這時獲取屬性的值如果使用get方法來獲取,會在多個類都要寫一遍,十分不符合程式碼設計,所以委託屬性至關重要。

委託屬性的實現

在前面說委託屬性的概念時就說了,這個屬性的值需要由一個新類來代理處理,這就是委託屬性,那我們也可以大概猜出委託屬性的底層邏輯,大致如下面程式碼:

js class People{ val name: String? = null var age: Int = 0 val isAbove18: Boolean = false //email屬性進行委託,把它委託給ProduceEmail類 var email: String by ProduceEmail() } 假如People的email屬性需要委託,上面程式碼編譯器會編譯成如下:

js class People{ val name: String? = null var age: Int = 0 val isAbove18: Boolean = false //委託類的例項 private val productEmail = ProduceEmail() //委託屬性 var email: String //訪問器從委託類例項獲取值 get() = productEmail.getValue() //設定值把值設定進委託類例項 set(value) = productEmail.setValue(value) } 當然上面程式碼是編譯不過的,只是說一下委託的實現大致原理。那假如想使ProduceEmail類真的具有這個功能,需要如何實現呢。

by約定

其實我們經常使用 by 關鍵字它是一種約定,是對啥的約定呢 是對委託類的方法的約定,關於啥是約定,一句話說明白就是簡化函式呼叫,具體可以檢視我之前的文章:

# Kotlin invoke約定,讓Kotlin程式碼更簡潔

那這裡的by約定簡化了啥函式呼叫呢 其實也就是屬性的get方法和set方法,當然委託類需要定義相應的函式,也就是下面這2個函式:

```js //by約定能正常使用的方法 class ProduceEmail(){

private val emails = arrayListOf("[email protected]")

//對應於被委託屬性的get函式
operator fun getValue(people: People, property: KProperty<*>): String {
    Log.i("zyh", "getValue: 操作的屬性名是 ${property.name}")
    return emails.last()
}

//對於被委託屬性的get函式
operator fun setValue(people: People, property: KProperty<*>, s: String) {
    emails.add(s)
}

} ``` 定義完上面委託類,便可以進行委託屬性了:

js class People{ val name: String? = null var age: Int = 0 val isAbove18: Boolean = false //委託屬性 var email: String by ProduceEmail() } 然後看一下呼叫地方: js val people = People() Log.i("zyh", "onCreate: ${people.email}") people.email = "[email protected]" Log.i("zyh", "onCreate: ${people.email}") 列印如下:

image.png

會發現每次呼叫email屬性的訪問器方法時,都會呼叫委託類的方法。

關於委託類中的方法,當你使用by關鍵字時,IDE會自動提醒,提醒如下:

image.png

比如getValue方法中的引數,第一個就是接收者了,你這個要委託的屬性是哪個類的,第二個就是屬性了,關於KProperty不熟悉的同學可以檢視文章:

# Kotlin反射全解析3 -- 大展身手的KProperty

它就代表這屬性,可以呼叫其中的一些方法來獲取屬性的資訊。

而且方法必須使用operator關鍵字修飾,這是過載操作符的必須步驟,想使用約定,就必須這樣幹。

by lazy的實現

由前面明白了by的原理,我們接著來看一下我們經常使用的by lazy是個啥,直接看程式碼:

js //這裡使用by lazy惰性初始化一個例項 val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { DataStoreManager(store) } 比如上面程式碼,使用惰性初始化初始了一個例項,我們來看一下這個by的實現:

js //by程式碼 @kotlin.internal.InlineOnly public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value 哦,會發現它是Lazy類的一個擴充套件函式,按照前面我們對by的理解,它就是把被委託的屬性的get函式和getValue進行配對,所以可以想象在Lazy< T >類中,這個value便是返回的值,我們來看一下:

```js //惰性初始化類 public interface Lazy {

//懶載入的值,一旦被賦值,將不會被改變
public val value: T

//表示是否已經初始化
public fun isInitialized(): Boolean

} ``` 到這裡我們注意一下 by lazy的lazy,這個就是一個高階函式,來建立Lazy例項的,lazy原始碼:

js //lazy原始碼 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) } 這裡會發現第一個引數便是執行緒同步的模式,第二個引數是初始化器,我們就直接看一下最常見的SYNCHRONIZED的模式程式碼: ```js //執行緒安全模式下的單例 private class SynchronizedLazyImpl(initializer: () -> T, lock: Any? = null) : Lazy, Serializable { private var initializer: (() -> T)? = initializer //用來儲存值,當已經被初始化時則不是預設值 @Volatile private var _value: Any? = UNINITIALIZED_VALUE //鎖 private val lock = lock ?: this

override val value: T
    //見分析1
    get() {
        //第一次判空,當例項存在則直接返回
        val _v1 = _value
        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 {
                //真正初始化
                val typedValue = initializer!!()
                _value = typedValue
                initializer = null
                typedValue
            }
        }
    }

//是否已經完成
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

private fun writeReplace(): Any = InitializedLazyImpl(value)

} ``` 分析1:這個單例實現是不是有點眼熟,沒錯它就是雙重校驗鎖實現的單例,假如你對雙重校驗鎖的實現單例方式還不是很明白可以檢視文章:

# Java雙重校驗鎖單例原理 趕快看進來

這裡實現懶載入單例的模式就是雙重校驗鎖,2次判空以及volatile關鍵字都是有作用的,這裡不再贅述。

總結

先搞明白by的原理,再理解by lazy就非常好理解了,雖然這些關鍵字我們經常使用,不過看一下其原始碼實現還是很舒爽的,尤其是Kotlin的高階函式的一些SDK寫法還是很值的學習。