Kotlin / Programming

ザクっと理解するKotlinで見る遅延初期化

遅延初期化を使用することで、オブジェクトが必要になるまでインスタンス化を遅らせるなどのアプリケーションのパフォーマンスを改善することができる場合があります。

本記事では、Kotlinのプロパティにおける遅延初期化の実装方法について説明します。

遅延初期化とは

変数やプロパティが実際に使用されるまで初期化を遅らせることができる機能です。

通常、変数やプロパティはクラス呼び出しと同時に初期化されます。

しかし遅延初期化を使用すると、初期化のタイミングを遅らせることができます。

遅延初期化は、オブジェクトの初期化や性能の改善など、さまざまな目的で使用されます。

使用方法

非nullプロパティはコンストラクタ内で初期化をしなければいけません。

しかしlateinitキーワードをつけることでコンストラクタ内での初期化を避けることができます。

解説

非nullなプロパティはコンストラクタで初期化

まずは非nullなプロパティはコンストラクタで初期化しないといけないことを確認します。

class Class01() {
    var name: String
}

Class01のnameプロパティは非nullであるにもかかわらず初期化されていません。

この場合、下記エラーが出力されます。

Property must be initialized or be abstract

訳: プロパティは初期化するか抽象化してください

抽象化せずにこれを解消するには、このまま初期化するか、コンストラクタで初期化する必要があります。

・このまま初期化

class Class01() {
    var name: String = "Class01"
}

・コンストラクタで初期化

class Class01(className: String) {
    var name: String = className
}

// これでも同じ
// class Class01(var name: String) {}

ちなみにnullならコンストラクタ内で初期化しなくても大丈夫です。

class Class01(var className: String?) {}

依存関係挿入

まずは依存関係挿入をしていないコードを見てみます。

fun main() {
    val car = Car()
    car.start()
}

class Car {
    val engine = Engine()
    
    fun start() {
        engine.start()
    }
}

class Engine {
    fun start() {
        println("Engine starts")
    }
}

これはCarクラス内でEngineクラスをインスタンス化しています。

Carクラスでは1つのEngineクラスを使用しているため、サブクラスや代替クラスを使用することが難しくなっています。

例えばElectricEngineクラスを使いたい場合、容易にはCarクラスに実装できません。

次は依存関係挿入を行うコードです。

fun main() {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

class Car(val engine: Engine) {
    fun start() {
        engine.start()
    }
}

class Engine {
    fun start() {
        println("Engine starts")
    }
}

これはCarクラスではEngineクラスをインスタンス化しておらず、引数で受け取っています。

これにより下記のように他Engineクラスを渡すことが容易になります。

fun main() {
    val engine = Engine()
    val car = Car(engine)
    car.start() // Engine starts
    
    val electricEngine = ElectricEngine()
    val electricCar = Car(electricEngine)
    electricCar.start() // Electric engine starts
}

class Car(val engine: Engine) {
    fun start() {
        engine.start()
    }
}

class ElectricEngine: Engine() {
    override fun start() {
        println("Electric engine starts")
    }
}

open class Engine {
    open fun start() {
        println("Engine starts")
    }
}

※詳しくはAndroidのリファレンス

非nullなプロパティだけどコンストラクタで初期化しない(できない)とき

例えば上のCarクラスのコンストラクタでは初期化せず、lateinitを使用します。

fun main() {
    val car = Car()
    val engine = Engine()
    car.setNewEngine(engine)
    car.start()
}

class Car {
    lateinit var engine: Engine
    
    fun setNewEngine(newEngine: Engine) {
        engine = newEngine
    }
    
    fun start() {
        engine.start()
    }
}

class Engine {
    fun start() {
        println("Engine starts")
    }
}

このようにlateinitを使うことでコンストラクタで初期化をする必要がなくなります。

またlateinitで宣言されたプロパティが初期化済みかどうかは「::プロパティ名.isInitialized」で調べます。

fun main() {
    val car = Car()
    val engine = Engine()
    car.setNewEngine(engine)
    car.setNewEngine(engine)
    car.start()
    
    //  実行結果
    // engine has not initialized yet
    // engine has initialized
    // Engine starts
}

class Car {
    lateinit var engine: Engine
    
    fun setNewEngine(newEngine: Engine) {
        if (::engine.isInitialized) {
            println("engine has initialized")
            return
        }
        println("engine has not initialized yet")
        engine = newEngine
    }
    
    fun start() {
        engine.start()
    }
}

class Engine {
    fun start() {
        println("Engine starts")
    }
}

lateinitが使えるのは、トップレベルのプロパティ、ローカル変数、クラスのボディでvarで宣言されたプロパティにしか使えません。

プライマリコンストラクタの中やカスタムゲッターやセッターを所持してる場合は使えません。

プロパティや変数の型は非nullかつプリミティブ型以外である必要があります。

また、lateinitのプロパティを初期化する前に使うとこのようなエラーがでます。

lateinit property プロパティ名 has not been initialized

訳: lateinitプロパティである「プロパティ名」はまだ初期化されていません。

この場合は、先に「プロパティ名」を初期化しましょう

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です