Kotlin使用心得(十一):lateinit vs lazy

Carter Chen
5 min readJun 21, 2018

--

“A tabby cat lying on the ground and yawning in Winden am See” by Martina Misar-Tummeltshammer on Unsplash

寫在前面

本文含有翻譯自下面文章的大量內文:

lateinit:延遲初始化

來看一下我們在Android中,是如何綁定一個TextView的:

將代碼轉換成Kotlin看看:

從代碼來看,我們必須先對textView這個變數指定null,進行初始化;後續在指定textViewtext屬性時,也要先確認textView是不為空的:

textView?.text = "hello world"

如果你能確定textView這個變數在後續一定會被初始化,然後你也不想每次都用看起來很G8的問號來判斷變數是否為空,那你可以加個lateinit關鍵字來修飾這個變數:

可以看到,我們再也不用事先進行初始化了,compiler也不會跟我們警告說要使用?來判斷textView這個變數是否為空。

上面我們使用lateinit進行「延遲初始化」的動作,那使用這種方式有什麼好處呢?其實它非常適合用於依賴注入(Dependency Injection)的場合,讓我們來看一下:

從代碼我們可以看到,當我們要實例化一個Driver,一定得依賴一個Vehicle,不管這個VehicleTaxi還是Truck。在這種情況中,Driver中的vehicle屬性一定不為空,所以我們也可以用lateinit來改寫:

延遲初始化val屬性?

上面我們都是將lateinit關鍵字加在var關鍵字前面,那lateinit也可以加在val關鍵字前面嗎?不知道能不能通過編譯?讓我們來試試看:

private lateinit val textView: TextView

Compiler:

‘lateinit’ modifier is allowed only on mutable properties

翻譯:省省吧,lateinit關鍵字只能用在會改變的屬性上。

使用lateinit的注意事項

先來一段使用lateinit進行初始化的完整代碼:

進行Decompile:

將初始化textViewtextView使用lateinit進行延遲初始化)的代碼片段comment掉:

再次Decompile:

實際使用IDE去運行,可以發現:

  • 在使用lateinit的情況下,使用lateinit的屬性預設為null
  • 使用lateinit後,就算你沒有初始化這個屬性,直接呼叫這個屬性的方法時,compiler也不會報錯

這可能是因為當你選擇使用lateinit的時候,等於你告訴compiler說你在之後一定會進行初始化,而compiler也完全地相信你了,所以不多加檢查,這部分需要多加注意。

Lazy:懶加載

當你用val聲明一個變數時:

private val textView: TextView

Compiler:

Property must be initialized or be abstract

翻譯:要記得初始化啊你……

在Kotlin中,變數的初始化都必須指定值。我們先前為了避免在使用var宣告變數時就先給定初始值,使用lateinit來應變,那如何避免在使用val宣告變數時就先給定初始值呢?

那就使用lazy吧!

我們將之前的代碼改成適合使用lazy的樣式:

可以看到,lazy的用法與委託是相同的,關鍵字前面都必須加個by

lazy使用起來和單例模式非常的像,只有「使用lazy初始化的物件」在「被第一次使用」時,才會執行lazy的程式碼片段,如果沒用到,就不會執行。

在上述的代碼中,textView什麼時候被用到呢?就是在設定textView.text的時候。

載入這個頁面時,會收到以下的log:

I/System.out: this is lazy function block

如果不去呼叫textView.text (或者是說不使用textView)的話,log就不會有東西print出來:

結果:

啥都沒有

如果呼叫了textView兩次,那還是只會呼叫一次lazy的function block一次,一但進行初始化後就不會再進行第二次初始化了:

結果(還是只有一行):

I/System.out: this is lazy function block

Lazy用法背後的機制

我們點進去lazy方法,看看它是怎麼實現的:

public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

可以看到,它必須返回Lazy<T>類型的結果,而這個結果就靠SynchronizedLazyImpl()類別來實現:

可以看到SynchronizedLazyImpl() 類別中指定_valueUNINITIALIZED_VALUE,意即初始狀態為「未初始化的」。

當外部嘗試讀取value屬性時,會先檢查_value是否已經被初始化過,如果都還沒被初始化,才會呼叫lazy function block中的代碼。

--

--