はじめに
こんにちは、前回に引き続き、Kotlinの基本的な文法をまとめています。今回はKotlinにおける初期化の遅延についてとなります。
by lazy
や
lateinit
といったキーワードを初めて目にした時は「呼び出される時に初期化されるのね」という理解(間違いではないのですが言葉足らずでした)しかしておらず、改めて調べることでそれぞれの差異や機能の意図などを掴んでいこうと思います。
初期化の遅延とは
HaskellやScalaでは「遅延評価」と呼ばれているようです。
今回の記事では「インスタンス生成後にプロパティの値を定義できる」という意味で初期化の遅延という表現を使っています。
以下の記事で大まかな概要を理解できましたので、リンクを掲載しておきます。サンプルコードはScalaですが、概ね理解できると思います。
遅延評価とは何か
by lazy
- 型は何でもOK
- 一度だけ値の初期化を行う
- 値はキャッシュされ、二回目以降は最初の値を常に返す
- readonly
最後の「readonly」が注意点なのですが、これは後述します。
by lazy
の記述方法は以下のようになります。
by lazy
の後にラムダ式が記述でき、その中で行われた処理の結果を値として、プロパティ
msg
が初期化されています。
1 2 3 4 5 6 7 8 9 10 | fun main(args: Array<String>) { val sample = Sample(1, 2) println(sample.msg) // "引数は1と2です。" } class Sample(n: Int, d: Int) { public val msg: String by lazy { "引数は${n}と${d}です。" } } |
「常に同じ値を返す」という性質なので、findViewByIdで取得したViewを入れたりすると、少し厄介なことになります。
というのも常に同じ値を返すため、例えば
textView.text = newValue
などとしても変更が反映されません。これはKotlinでは有名なアンチパターンとのこと。
参考: FragmentでKotlinのby lazyを使ってfindViewByIdするとレイアウト反映できない&リークする件
処理が進むにつれて変更されていく値は、次に紹介する
lateinit
, あるいは
Delegates.notNull
を使います。
lateinit
- プリミティブ型は不可
- val不可、varのみ
- nullable不可
- private推奨(初期化が行われる前に外部からアクセスされるのを防ぐため?)
「あとで必ず初期化するけど、インスタンス生成時にはできない」という時に使用します。必ず初期化するのでnon-nullで扱うことが可能となります。
またval不可なので、変更されることが前提となります。ActivityやFragmentのonCreate, onCreateViewで、findViewByIdで取得したViewなどが入ることを想定しているようです。
サンプルコードは下記のようになります。プロパティで変数名だけ宣言する場合、Javaでは値がnullになりますが、lateinitを付けてonCreate内で初期化すればnon-nullとして扱えます。
1 2 3 4 5 6 7 8 9 | class SampleActivity : AppCompatActivity() { private lateinit var mTextView: TextView override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) mTextView = view.findViewById(R.id.helloText) mTextView.text = "hogehoge" // non-nullなので「?」不要 } } |
Delegates.notNull
Delegates.notNull
はプリミティブ型に使えます。プロパティにアクセスする前に値をセットする必要があるため、下記のコードはコンパイルエラーとなります。
1 2 3 4 5 6 7 8 9 10 | import kotlin.properties.Delegates fun main(args: Array<String>) { val sample = Sample() println(sample.nonNullMsg) // Exception in thread "main" java.lang.IllegalStateException: Property nonNullMsg should be initialized before get. } class Sample() { var nonNullMsg : String by Delegates.notNull() } |
下記のように
sample.nonNullMsg
に値を定義してから呼び出しましょう。
1 2 3 4 5 6 7 8 9 10 11 | import kotlin.properties.Delegates fun main(args: Array<String>) { val sample = Sample() sample.nonNullMsg = "hoge piyo" // ← ここで初期化 println(sample.nonNullMsg) } class Sample() { var nonNullMsg : String by Delegates.notNull() } |
ここで「何のためにDelegates.notNullがあるんだろう、lazyかlateinitでいいんじゃないの」という疑問が湧いたのですが
- lazyは値が不変な場合に使う
- lateinitはプリミティブ型に使えない
という特徴があったのでした。Delegates.notNullは「可変かつnon-nullな値の初期化を遅延したい」時に使うのが良さそうです。
lazyinitとnotNullの使い分けについてはこちらが参考になりました → Kotlin : ‘notNull delegate’ vs ‘lateinit’
さいごに
いかがでしたでしょうか。「プロパティでnon-nullを保ちつつ、コードの途中で初期化したい。なるべくスマートな書き方で」となると、今回挙げたような記法が一例になると思います。
また、こういった記法を知っていることで、コードの書き手の意図を汲みやすく、また読み手に自分の意図を伝えやすくもなります。
「書きやすく、読みやすい」コードはなかなか難しいですが、常に意識しておきたいです。