Skip to main content

Kotlin 的 init 跟 constructor

·259 words·2 mins·
Kotlin
Author
Andreas Kung

直接看 code

class InitTest {
    private val first = mutableListOf<Int>().apply {
        println("Init first")
    }

    init {
        println("Init block")
    }
    
    private val second = mutableListOf<Int>().apply {
        println("Init second")
    }
}

val a = InitTest()

會印出的順序是?

Init first
Init block
Init second

為什麼 Init block 不是最先/最後執行?

根據 Kotlin Classes 文件,

During the initialization of an instance, the initializer blocks are executed in the same order as they appear in the class body, interleaved with the property initializers

init 區塊是跟著他在 code 裡面的順序執行, 如果有其他變數, 就會跟變數交錯被執行。

如果嘗試在 init 裡面使用後面才寫的 second 變數, IDE 會報錯

init{
    println(second)
    //e: Variable 'second' must be initialized
}

private val second = mutableListOf<Int>().apply {
    println("Init second")
}

如果把 init 的內容, 搬到另外一個方法, 但是又寫在 second 之後, compiler 就無法偵測出來了。但是這時候 second 還沒被初始化, 就會是 null。

init {
    call()
}

private val second = mutableListOf<Int>().apply {
    println("Init second")
}

fun call() {
    println("Init block $second")
    //Init block null
}

init block 不是 constructor
#

init 只是一段在初始化時候會被執行的 code, 也可以寫多個, 會按照順序執行。

通常大家以為的 init 用途, 其實應該用 secondary constructor

private val first = mutableListOf<Int>().apply {
    println("Init first")
}

init {
    call()
}

private val second = mutableListOf<Int>().apply {
    println("Init second")
}

fun call() {
    println("Init block $second")
}

constructor() {
    println("Init Constructor")
}

輸出結果

Init first
Init block null
Init second
Init Constructor

constructor 裡面才已經確定所有變數都正確初始化了。

在 Android 上的問題
#

上面的例子都是 single thread 的狀況, 100%可以重現變數是 null。如果是 Android ViewModel, 又在 init block 裡面透過 coroutine 在 Main 以外的 Dispatcher 做事。那就沒辦法確保, 是初始化變數的 Main Thread 會先執行, 或者是其他 Dispatcher 的工作會先被執行。

如果其他 Dispatcher 的 job 比 Main 晚執行到, 那變數就會被正常初始化, 如果其他 Dispatcher 比較早開始跑, 變數還沒初始化, 然後就 Null Pointer Exception 了。

Notes
#

  • 如果不確定, 請用 constructor。
  • 如果很想用 init, 那就要放在所有變數宣告之後。