Kotlin物件的懶載入方式?by lazy 與 lateinit 的異同

語言: CN / TW / HK

theme: smartblue highlight: agate


持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第8天,點選檢視活動詳情

前言

屬性或物件的延時載入是我們相當常用的,一般我們都是使用 lateinit 和 by lazy 來實現。

他們兩者都是延時初始化,那麼在使用時那麼他們兩者有什麼區別呢?

lateinit

見名知意,延時初始化的標記。lateinit var可以讓我們宣告一個變數並且不用馬上初始化,在我們需要的時候進行手動初始化即可。

如果我們不初始化會怎樣?

```kotlin private lateinit var name: String

findViewById<Button>(R.id.btn_load).click {

    YYLogUtils.w("name:$name age:$age")

}

```

會報錯:

image.png

所以對應這一種情況我們會有一個是否初始化的判斷

```kotlin private lateinit var name: String

findViewById<Button>(R.id.btn_load).click {

    if (this::name.isInitialized) {
       YYLogUtils.w("name:$name age:$age")
    }

}

```

lateinit var的作用相對較簡單,其實就是讓編譯期在檢查時不要因為屬性變數未被初始化而報錯。(注意一定要記得初始化哦!)

by lazy

by lazy 委託延時處理,分為委託和延時

其實如果我們不想延時初始化,我們直接使用委託by也可以實現。

```kotlin private var age: Int by Delegates.observable(18) { property, oldValue, newValue -> YYLogUtils.w("發生了回撥 property:$property oldValue:$oldValue newValue:$newValue") }

findViewById<Button>(R.id.btn_load).click {
    age = 25

    YYLogUtils.w("name:$name age:$age")

}

```

我們通過 by Delegates 的方式就可以指定委託物件,這裡我用的 Delegates.obsevable 它的作用是修改 age 的值之後會有回撥的處理。

執行的效果:

image.png

除了 Delegates.obsevable 它還有其他的用法。

```kotlin public object Delegates {

public fun <T : Any> notNull(): ReadWriteProperty<Any?, T> = NotNullVar()

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)
    }

public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean):
        ReadWriteProperty<Any?, T> =
    object : ObservableProperty<T>(initialValue) {
        override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue)
    }

}

private class NotNullVar() : ReadWriteProperty { private var value: T? = null

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
}

} ```

  • notNull方法我們可以看到就是說這個物件不能為null,否則就會丟擲異常。
  • observable方法主要用於監控屬性值發生變更,類似於一個觀察者。當屬性值被修改後會往外部丟擲一個變更的回撥。
  • vetoable方法跟observable類似,都是用於監控屬性值發生變更,當屬性值被修改後會往外部丟擲一個變更的回撥。與observable不同的是這個回撥會返回一個Boolean值,來決定此次屬性值是否執行修改。

其實用不用委託沒什麼區別,就是看是否需要屬性變化的回撥監聽,否則我們直接用變數即可

```kotlin private var age: Int = 18

findViewById<Button>(R.id.btn_load).click {
    age = 25

    YYLogUtils.w("name:$name age:$age")

}

```

如果我們想實現延時初始化的關鍵就是 lazy 關鍵字,所以,lazy是如何工作的呢? 讓我們一起在Kotlin標準庫參考中總結lazy()方法,如下所示:

image.png

  • lazy() 返回的是一個儲存在lambda初始化器中的Lazy型別例項。
  • getter的第一次呼叫執行傳遞給lazy()的lambda並存儲其結果。
  • 後面再呼叫的話,getter呼叫只返回儲存中的值。

簡單地說,lazy建立一個例項,在第一次訪問屬性值時執行初始化,儲存結果並返回儲存的值。

```kotlin private val age: Int by lazy { 18 / 2 }

findViewById<Button>(R.id.btn_load).click {
    age = 25

    YYLogUtils.w("name:$name age:$age")

}

```

由於我們使用的是 by lazy ,歸根到底還是一種委託,只是它是一種特殊的委託,它的過程是這樣的:

我們的屬性 age 需要 by lazy 時,它生成一個該屬性的附加屬性:age?delegate。 在構造器中,將使用 lazy(()->T) 建立的 Lazy 例項物件賦值給 age?delegate。 當該屬性被呼叫,即其getter方法被呼叫時返回 age?delegate.getVaule(),而 age?delegate.getVaule()方法的返回結果是物件 age?delegate 內部的 _value 屬性值,在getVaule()第一次被呼叫時會將_value進行初始化並儲存起來,往後都是直接將_value的值返回,從而實現屬性值的唯一一次的初始化,並無法再次修改。所以它是隻讀的。

當我們呼叫這個 age 這個屬性的時候才會初始化,它屬於一種懶載入,既然是懶載入,就必然涉及到執行緒安全的問題,我們看看lazy是怎麼解決的。

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

public actual fun lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy = when (mode) { LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer) LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer) LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer) }

public actual fun lazy(lock: Any?, initializer: () -> T): Lazy = SynchronizedLazyImpl(initializer, lock)
```

我們需要考慮的是執行緒安全和非執行緒安全

  • SYNCHRONIZED通過加鎖來確保只有一個執行緒可以初始化Lazy例項,是執行緒安全的

  • PUBLICATION表示不加鎖,可以併發訪問多次呼叫,但是我之接收第一個返回的值作為Lazy的例項,其他後面返回的是啥玩意兒我不管。這也是執行緒安全的

  • NONE不加鎖,是執行緒不安全的

總結

總的來說其實 lateinit 是延遲初始化, by lazy 是懶載入即初始化方式已確定,只是在使用的時候執行。

雖然兩者都可以推遲屬性初始化的時間,但是 lateinit var 只是讓編譯期忽略對屬性未初始化的檢查,後續在哪裡以及何時初始化還需要開發者自己決定。而by lazy真正做到了宣告的同時也指定了延遲初始化時的行為,在屬性被第一次被使用的時候能自動初始化。

並且 lateinit 是可讀寫的,by lazy 是隻讀的。

那我們什麼時候該使用 lateinit,什麼時候使用 by lazy ?

其實大部分情況下都可以通用,只是 by lazy 一般用於非空只讀屬性,需要延遲載入情況,而 lateinit 一般用於非空可變屬性,需要延遲載入情況。

慣例,如有錯漏還請指出,如果有更好的方案也歡迎留言區交流。

如果感覺本文對你有一點點的啟發,還望你能點贊支援一下,你的支援是我最大的動力。

Ok,這一期就此完結。

「其他文章」