1. 概要

この記事では、Kotlinでプリミティブ型に後で初期化されたプロパティと変数を使用できない理由を学習します。

この制限の背後にある理論的根拠をよりよく理解するために、まず、一歩下がって、Kotlinがプリミティブ型のnullabilityをどのように処理するかを確認します。 次に、lateinitプロパティの動機について理解します。 結局、2つを混合することが意味をなさない理由を理解するのははるかに簡単になります。

2. Kotlinとプリミティブタイプ

Kotlinがプリミティブ型をコンパイルする方法をよりよく理解するために、例を考えてみましょう。

class LateInit {
    private val nonNullable: Int = 12
    private val nullable: Int? = null

    // omitted
}

上記の例では、Intタイプの1つのnull許容変数と1つの非null許容変数があります。 それでは、 kotlinc を使用してこのファイルをコンパイルし、javapツールを使用して生成されたバイトコードを見てみましょう。

>> kotlinc LateInit.kt
>> javap -c -p com.baeldung.lateinit.LateInit
public final class com.baeldung.lateinit.LateInit {
  private final int nonNullable;
  private final java.lang.Integer nullable;
  // omitted
}

当然、 Kotlinは、null許容でないプリミティブ型をJavaの対応するプリミティブ型(この例ではint)にコンパイルします。 また、null許容プリミティブ型をJavaの対応するボックス型(この例では整数)にコンパイルします。 Kotlinの他のプリミティブタイプについても同じことが言えます。

3. lateinitリフレッシャー

Kotlinでは、プロパティ初期化子を使用して、コンストラクターで常にnull以外のプロパティを初期化する必要があります。 残念ながら、それが不可能または便利でない場合があります。 たとえば、フレームワークのテストで特定の依存性注入アプローチまたはセットアップメソッドを使用している場合がこれに該当します。

@Autowired
private val userService: UserService // won't work

上記の例は、null許容でないプロパティを初期化していないため、コンパイルすらしません。 これを修正する1つのアプローチは、null許容型を使用することです。

@Autowired
private val userService: UserService? = null

// on the call site
userService?.createUser(user)

これは確かに機能します。 ただし、変数を使用するたびに、null許容型(nullセーフ演算子など)の厄介さに対処する必要があります。

この制限を克服するために、Kotlinは lateinit 変数を提供します。これは、最初は初期化しないままにしておくことができます。

@Autowired
private lateinit var userService: UserService

基本的に、ここで we は、この変数をすぐに初期化していないことをコンパイラーに通知していますが、できるだけ早く初期化することを約束します。 If we fail to initialize the property, the runtime will throw an exception once we try to read its value.

このようにして、この変数がnull許容でないかのように動作できます。これはすばらしいことです。 興味深いことに、Kotlinは内部で、通常のnull許容型のようにlateinit変数をコンパイルします。 たとえば、別の例を考えてみましょう。

private lateinit var lateinit: String

上記の例のバイトコードは次のようになります。

private java.lang.String lateinit;

通常のvarとは対照的に、 lateinit 変数にアクセスするたびに、Kotlinはそれらを初期化したかどうかをチェックします。

6: ifnonnull     16
9: ldc           #22     // String lateinit
11: invokestatic  #28    // Method Intrinsics.throwUninitializedPropertyAccessException:(LString;)V
14: aconst_null
15: athrow

上に示したように、 lateinit変数の値がnull(インデックス6)の場合、Kotlinは実行時に例外をスローします(インデックス11、14、および15)

具体的には、変数が初期化されているかどうかを確認するために、Kotlinは throwUninitializedPropertyAccessException メソッドを呼び出し、UninitializedPropertyAccessException例外のインスタンスをスローします。

基本的に、追加のメソッド呼び出しは、このシンタックスシュガーを使用するために支払う価格です。

4. プリミティブ型とlateinit

つまり、 Kotlinは、lateinitプロパティが初期化されているかどうかを判断するための特別な値としてnullを使用しています。 つまり、 lateinit var を別の場所に再割り当てできても、 null に明示的に設定することはできません。これは、特別な意味があり、そのために予約されているためです。 。

それでは、次の宣言の何が問題になっているのかを見てみましょう。

private lateinit var x: Int

これまでに学んだことに基づいて、Kotlinはxintとして内部でコンパイルします。 また、これは lateinit 変数であるため、Kotlinは初期化されていないケースを表す特別な値としてnullを使用する必要があります。 nullをintやその他のプリミティブ型に格納できないため、Kotlinではこの宣言は無効です。

上記の制限を解決するためにnull許容型を使用することを提案するかもしれません:

private lateinit var x: Int?

明らかに、これは2つの理由で意味がありません。

  1. Int?のようなnull許容型の場合、nullを含むすべての値が受け入れられます。 したがって、nullを初期化されていないケースの特別なホルダーとして使用することはできません。 このため、null許容型でlateinit変数を使用することもできません。
  2. null許容型でlateinitプリミティブ変数を実装することが可能であったとしても、そうすることは意味がありません。 ご存知のように、null許容型の厄介さを回避するためにlateinit変数を使用しています。 したがって、プリミティブにnull許容型を使用すると、lateinitを使用するという目的全体が無効になります。

したがって、要約すると、 Kotlin のプリミティブ(Intやブールなど)またはnull許容型にlateinit変数を使用することはできません。

5. 結論

この記事では、Kotlinがプリミティブ型とnull許容型にlateinit変数を使用できない理由を学びました。 より傾倒した読者は、 Project Valhalla の進捗状況を追跡することもできます。これは、近い将来、この制限に対する優れた解決策を提供する可能性があります。

いつものように、すべての例はGitHubから入手できます。