Kotlin「lateinit property has not been initialized」の原因と直し方
lateinit property has not been initialized は、読む前に代入すれば直る。
代入が後になるなら、::name.isInitialized で確かめるか、by lazy やコンストラクタ引数に替える。
公開:
要約
lateinit property name has not been initialized は、lateinit(新しいタブで開く) のプロパティに値を入れる前に読むと投げられる例外である。読む前に代入すれば直る。lateinit はコンパイラの初期化チェックを外すので、コンパイルは通り、実行したときに初めて分かる。
class User {
lateinit var name: String
fun greet() = "Hello, $name"
}
fun main() { println(User().greet()) }Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property name has not been initialized
at User.getName(A.kt:2)
at User.greet(A.kt:3)
at AKt.main(A.kt:5)文言の name の位置には、プロパティの名前が入る。
スタックトレースの User.getName の次の行(ここでは User.greet)が、読んだ場所である。
kotlinc 1.9.25 と 2.4.21 で同じ文言になった。
解決策
1. 読む前に確かめる
代入されているかどうかは、::name.isInitialized で分かる。
class User {
lateinit var name: String
fun greet(): String {
if (!::name.isInitialized) return "name is not set"
return "Hello, $name"
}
}これを書けるのは、バッキングフィールドに触れる場所だけである。
クラスの外から user::name.isInitialized と書くと、backing field of 'var name: String' is not accessible at this point. でコンパイルが止まる。
外から知りたいなら、クラスの中に判定用のメソッドを用意する。
fun isReady() = ::name.isInitialized2. コンストラクタ引数に替える
インスタンスを作るときに値が決まるなら、lateinit は要らない。
class Repo(val url: String)3. by lazy に替える
初めて読むときに値を作れるなら、by lazy にする。
初回に一度だけブロックが走り、2 回目はその値を返す。val にできるので、あとから書き換えられない。
class Config {
val conf: String by lazy { "value" }
}4. 値が無い状態も扱うなら null を許す
未設定が正常な状態としてありうるなら、String? にする。null の扱いを書かないとコンパイルエラーになるので、実行時の例外にはならない。
class Repo {
var url: String? = null
fun show() = url ?: "unset"
}lateinit を書けない型
lateinit は、var で、null を許さない、プリミティブでない型にしか付けられない。
これらに付けるとコンパイルエラーになり、例外まで進まない。
| 書いたもの | kotlinc 2.4.21 のエラー |
|---|---|
lateinit var age: Int | 'lateinit' modifier is not allowed on properties of primitive types. |
lateinit var name: String? | 'lateinit' modifier is not allowed on properties of a type with nullable upper bound. |
lateinit val name: String | 'lateinit' modifier is allowed only on mutable properties. |
kotlinc 1.9.25 では、2 行目の文言が not allowed on properties of nullable types だった。
よくある原因
- 代入するメソッドを呼ぶ前に読んでいる。
setup()のようなメソッドで代入する設計だと、呼び忘れや呼ぶ順番の入れ替えがそのまま例外になる。 initブロックや親クラスのコンストラクタの中で読んでいる。
親のコンストラクタからopenメソッドを呼ぶと、子の代入より先に読むことになる。
このときスタックトレースにはBase.<init>のような行が出る。- 別のインスタンスのプロパティを読んでいる。
aでconnect()を呼んだあとに、新しく作ったbの値を読むと、bのプロパティは空のままである。