[Kotlin]初期化の遅延 – lateinitとby lazyの使い方

Kotlinでは「プロパティには何らかの値が代入されていなければならない」という決まりがあります。

しかし例外というのは付き物です。今回はKotlinに、プロパティの初期化を「ちょっと待って!」と訴えるlateinitby lazyをご紹介します。

lateinit

プロパティの値が、そのプロパティを定義した時点ではまだ用意できないという場合、lateinitでそのプロパティの初期化を遅らせることができます。
lateinitはnull許容型の代わりに使うことができ、そのプロパティを参照するまでに必ず値が代入されることを開発者の責任で保証します。

その代わり、そのプロパティには(定義した時点での)初期化が必要無くなります。「使う時までに、どこかで値が代入されればOK」というわけです。

lateinitの例

例えばAndroidのActivity(画面表示に関するクラス)のプロパティは、onCreateメソッドによって初期化されることがあります。ミュージックプレイヤーのコードを抜粋したものが以下です。

class MainActivity: AppCombatActivity{
    private lateinit var player: MediaPlayer     //この時点では初期化できない

    override fun onCreate(savedInstanceState: Bundle?){
        ...
        player = MediaPlayer.create(this,R.raw.music)   //ここで初期化
    }
}

2行目のplayerプロパティは、定義した時点ではまだ初期化できません。onCreateソッドの中で初期化される必要があるためです。
こんな風に「プロパティの値は他の場所で生成する」といった事情があるのであれば、lateinitを使うべき場面かもしれません。

注意点とisInitialized

lateinitは可視性修飾子の後に加えるだけで簡単に使えますが、使う際の注意点として、

  1. null許容型には使用不可
  2. リードオンリーの(valで定義した)プロパティには使用不可
  3. 型がIntやFloat、Booleanなどのプリミティブ型の場合、使用不可 (Charは×、StringはOK)
  4. 可能であれば可視性はprivateにすべき (初期化後の意図しない代入を防ぐ)
  5. もし初期化される前に参照されれば「UnitinitializedPropertyAccessException」(初期化されていないプロパティにアクセスした)例外エラーが送出される

といった点が挙げられます。

またlateinitを使って初期化されるプロパティは、isInitializedプロパティで初期化されているかどうかを調べることができます。状況に応じて使用しましょう。

Kotlin: 「例外エラーになったら…あんたの責任だかんね!」

lazy initialization

もう1つの手段がlazy initializationです。こちらは「値が用意できない」という状況では使えません。後で初期化したほうが都合がいい場面です。例えばこんなコードがあったとします。

import java.io.File

class Music{
    val artist: String = ""

    fun getArt(): String{
        val songData = File("musicFiles/lib.csv")   //csvファイルの読み込み
            .readText()
            .split("\n")
            .shuffled()
            .first()
        return songData.split(",")[0]   //アーティスト名を出力
    }
}

getArtメソッドは別ファイルのcsvからデータを読み込み、アーティスト名をランダムに出力します。ちなみにサンプルのcsvがこちらです。

山崎まさよし,あじさい,04:29,STEREO 2
aiko,キラキラ,05:08,まとめ Ⅱ
Bank Band with Salyu,to U,07:13,to U
Bon Jovi,Livin' On A Prayer,04:08,Tokyo Road
Taylor Swift,Style,03:51,1989
米津玄師,アイネクライネ,04:50,YANKEE

このメソッドの出力をartistプロパティに代入したいとすると、まず初期化ブロックが考えられます。
init{}の中ににこのメソッド丸ごと記述すれば、インスタンス生成時にartistプロパティは初期化されます。しかし、このcsvが5万行あればどうでしょう?

lazy initializationの例

5万行のファイルを読み込む作業は誰がどう見ても時間がかかる作業です。インスタンス生成のたびに、「参照するかどうか分からない」プロパティのために多くの時間をかけるのは馬鹿げています。

ならせめて、このプロパティを実際に使うときに初期化した方がマシではないでしょうか。

import java.io.File

class Music{
    val artist by lazy{getArt()}  //lazyによって初期化

    private fun getArt(): String{
        val songData = File("musicFiles/lib.csv")
            .readText()
            .split("\n")
            .shuffled()
            .first()
        return songData.split(",")[0]
    }
}

4行目はプロパティの初期化を行う処理ですが、お馴染みの“=”はありません。代わりにby lazyを使って初期化しています。

by

このbyは「~によって」のbyです。Kotlinではこれを「Delegated Properties」(委譲プロパティ)とよんでいます。
簡単に言うと、「このプロパティは〇〇によって初期化します」という意味。

Page not found · GitHub Pages

lazy

byの後ろにlazy。これはラムダ式を取る関数です。

fun <T> lazy(initializer: () -> T): Lazy<T>

lazy関数に渡されたラムダは、そのプロパティが初めて参照される時まで実行されません。また一度実行された後はその結果がキャッシュされ、以降はキャッシュの値がそのまま出力されます。

注意点と特徴

lazy関数はbyと共に利用され、プロパティの初期化を、そのプロパティが初めて参照されるときまで遅延させます。特徴として、

  1. lazy関数は同じプロパティに対して、一度しか呼び出されない
  2. valプロパティでのみ使用可能
  3. プリミティブ型でも使用可能

といった点が挙げられます。
インスタンス生成時にプロパティに代入する値が用意できるのかできないのかでlateinitby lazyを使い分けましょう。

Kotlin: 「待っててあげる。…私だってそのくらいできるわよ!」

タイトルとURLをコピーしました