Kotlinの特徴の一つにNullSafetyの仕組みがあります。
Javaではif文で判定するしかなかったNull判定を、変数宣言時や代入時に行うことで後続処理でのNullPointerException
の発生を防げます。
そこで、KotlinでのNonNullで変数を定義する方法をまとめます。
対象読者
Kotlinをがっつりとは書いていない方
最近AndroidアプリをKotlinに移行し始めた方
lateinitを使う
lateinitは変数宣言に初期値を代入せず、後から代入されることを約束する際につけます。
class LateInit{
var user:User // コンパイルエラー
var user2:User? = null // エラーにはならないけどNullable😵
lateinit var user3:User //Good👍
lateinit val user4:User // valはfinal扱いなので、あとから代入はできないためコンパイルエラー
}
使う所
Androidを開発しているとよく使います
ActivityやFragmentは、システム側がインスタンスを生成するため、引数付きコンストラクタやinitを使う事はできません。
そのため、各値の初期化はonCreate()内にて行います。
class MainActivity(){
lateinit var user: User
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val name = getIntent().getStringExtra("name") ?: ""
user = User(name)
}
//...
}
代入する前に呼び出したら…
落ちます
class UnInit {
lateinit var user: User
fun run() {
println(user) //ここで落ちます
println(user.name) //ここには到達しない
}
}
出力結果
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property user has not been initialized
at NonNull.run(NonNull.kt:5)
at MainKt.main(Main.kt:6)
lateinitを宣言した変数に初期化されていない時専用の例外が出ます。
Javaで同じ様に書くと、一行下に書いたuser.name
でNullPointerExceptionが出力されるはずなのでより正確に例外の原因がわかります
by lazyを使う
変数を最初に呼び出されるまで初期化を遅延します。
val user: User by lazy {
User("John")
}
fun onCreate3() {
print("${user}")
}
実行結果
User(name=John)
注意点
by lazyによって取得された値は、AndroidのLifecycle関係なく保持されます
findViewByIdで取得したレイアウトはFragmentが再生成された場合でも古いViewを取得し続けます
参考URL:https://qiita.com/takahirom/items/49e18ec7084bb3b1937e
kotlinを使い始めた時、
変数をvalに統一したい時によく使っていたのですが良くない実装だったので今はlateinitやKotlin Android Extensions
を使っています。
使うところ
上記理由により、今はあまり使う場面に遭遇していないです。
取得処理が重いファイル読み込みなどに使う事はあるかもしれません
?: を使う(エルビス演算子)
Nullableの型をそのまま使うと不正な文字列になったりクラッシュになったりする
fun showUser(user: User?) {
val showName = user?.name // nullableなので、?.nameにしないとコンパイルエラー
println("名前:$showName") // showNameはString?になる😵
}
fun run(){
showUser(User("John"))
showUser(null) // アプリの画面にnullって出る😭
}
出力結果
名前:John
名前:null
なので、Java同様Nullチェックが必要になる
fun showUser(user: User?) {
if(user == null)return
val showName = user.name // nullじゃないのは確定したため、?.じゃなくてもコンパイルが通る
println(showName) // showNameはStringになる
}
Kotlinではif文の他に?:
というエルビス演算子を使って書くことができる
fun showUser(user: User?) {
val showName = user?.name ?: return // userがnullなら代入せず離脱する
val showName2 = user?.name ?: "名無し" // userがnullならshowName2に"名無し"を代入する
val showName3 = user?.name ?: throw Exception() // userがnullなら例外を投げる
println(showName)
}
letを使う(スコープ関数)
?:だとうまくいかない場合
エルビス演算子は便利だけど、メンバ変数をNullチェックをしたい時にはすんなり行かない
class ShowUser(){
var user: User? = null
fun showUser() {
if (user == null) return
val showName = user.name
val showAge = user.age
println("名前:$showName 年齢:$showAge")
}
}
こういうコードを書くと、Smart cast to 'User' is impossible, because 'user' is a mutable property that could have been changed by this time
とでてコンパイルエラーになる
user
メンバ変数として定義しているため、Nullチェック後に値が変わるかもしれない(例えば、非同期処理で同時にuser
に値を代入する)
そのためuserをNullチェック後でもNonNullとして扱うことができず、Nullableのままになってしまう
解決方法
var user: User? = null
fun showUser() {
user?.let { user ->
val showName = user.name
val showAge = user.age
println("名前:$showName 年齢:$showAge")
}
}
?.とスコープ関数を使って、関数内はNonNullであることを確定させた状態で処理を行う
user?.let { user ->
//...
} ?: throw Exception() //エルビス演算子と組み合わせる事もできる(nullの時例外を投げる)
あとがき
ここで紹介した方法以外にNonNullにする方法はあると思います。
知っている方はコメントで紹介していただけると嬉しいです😊