はじめに
本連載は、Kotlinの変更点を紹介する連載です。
Kotlinは、JVM言語のひとつとして開発され、最初の安定版であるバージョン1がリリースされたのが2016年2月です。その後、Android開発における第1言語の座を占めるようになったのを機に、多くの人に利用されるようになってきています。もちろん、Android開発以外でも利用されており、筆頭JVM言語としての地位を獲得しています。
そのようなKotlin言語のバージョン2.0が、2024年5月にリリースされました。バージョン1から2までの8年間、順当にアップデートを重ねてきており、さまざまな新機能が追加されています。本連載では、この8年間にバージョン1.1以降で追加されたさまざまな変更点を整理し、バージョン横断で各テーマごとに紹介していきます。具体的には、以下のテーマを予定しています。
- 関数に関する新機能
- クラスとインターフェースに関する新機能
- コルーチン
第1回目である今回は、上記テーマに含まれていないものの、重要な変更点を取り上げます。これらは、ひとつのテーマでは連載1回分の内容には少ないものばかりを、詰め合わせセットのようにして紹介していきます。
whenにおける新しい使い方
本連載の最初に紹介するアップデートは、whenにおける新しい使い方です。
whenの確認
when構文とは、場合分けに使える構文であり、他の言語でのswitch構文と同じものです。例えば、リスト1のような構文です。この例では、1〜10のどれかの整数が格納された変数randの値によって場合分けしています。
val rand = getRandInt() when(rand) { in 8..10 -> println("${rand}点で優です。") 7 -> println("${rand}点で良です。") 6 -> println("${rand}点で可です。") else -> println("${rand}点で不可です。") }
Kotlinでは、このwhen構文が文だけではなく式としても使えます。その場合は、リスト2のようなコードとなります。この例では、randの値によって場合分けされた文字列が、変数msgに格納されます。
val rand = getRandInt() val msg = when(rand) { in 8..10 -> "${rand}点で優です。" 7 -> "${rand}点で良です。" 6 -> "${rand}点で可です。" else -> "${rand}点で不可です。" }
()内には変数定義式が可能
when構文において、()に場合分けの元となる変数などを記述します(公式ドキュメントでは、これをsubjectと呼んでいます)。この()内に変数定義が記述できるように、バージョン1.3でアップデートされています。例えば、リスト3のようなコードです。
when(val rand = getRandInt()) { in 8..10 -> println("${rand}点で優です。") : }
リスト3のように()内で変数を定義するメリットは、その変数(リスト3ならばrand)のスコープがwhenブロック内に限定され、スコープ外、すなわち、whenブロック外から利用される心配がないことです。
lateinitの新しい使い方
次に紹介するのは、lateinitに関するものです。
lateinitプロパティ
lateinitは、クラスのプロパティ宣言で利用されてきています。例えば、リスト4のようなコードです。
class TimeGenerator { private lateinit var _time: LocalTime // (1) // private lateinit var _rand: Int // (2) fun getTimeStr(): String { if(!this::_time.isInitialized) { // (3) _time = LocalTime.now() } return _time.toString() // (4) } }
リスト4の(1)のLocalTime型のプロパティ_timeにはlateinitが付与されています。本来ノンナル(Non-Null)のプロパティは、コンストラクタで初期化処理を行う必要があります。もし初期化処理を記述しないならば、ナラブル(Nullable)なプロパティとして定義する必要があります。一方で、コンストラクタ段階、すなわち、インスタンスの生成時には初期化処理が定義できないプロパティもあり、その場合にこのlateinitは非常に便利です。Android開発のアクティビティに定義するプロパティなどで頻出です。
ただし、lateinitプロパティのデータ型として、プリミティブ型は利用できない点には注意しておいてください。リスト4の(2)のコメントアウトを元に戻すと、コンパイルエラーとなります。
初期化済みかどうかをチェックするisInitialized
このようなlateinitプロパティは、コンストラクタで初期化する必要がない代わりに、どこか別のメソッド内で初期化処理をしておく必要があります。もちろん、初期化されていない状態でアクセスしようとすると、UninitializedPropertyAccessExceptionが発生してしまいます。
そのような場合に便利な仕組みが、バージョン1.2で導入されています。それが、リスト4の(3)のisInitializedです。isInitializedは、初期化処理が済んでいるかチェックするプロパティであり、初期化処理が済んでいればtrueとなります。リスト4の(3)では、lateinitプロパティである_timeのtoString()メソッドにアクセスする(4)の前に、isInitializedで初期化済みかのチェックを行い、初期化されていなければLocalTime.now()メソッドで初期化処理を行っています。
ただし、このisInitializedを利用する場合は、単に_time.isInitializedとはできず、プロパティそのものを参照する「::」を利用し、「this::_time.isInitialized」とする必要がある点には注意しておいてください。
トップレベルプロパティとローカル変数でもlateinitが可能
isInitializedが導入されたバージョン1.2において、同じく導入されたのが、lateinitの拡張です。これまで、クラスのプロパティでのみ利用できていたlateinitをトップレベルプロパティとローカル変数にも利用できるようになりました。例えば、トップレベルプロパティに導入すると、リスト5のようなコードをファイル直下に記述した場合となります。
lateinit var topLevelTime: LocalTime // (1) fun getTopLevelTimeStr(): String { if(!::topLevelTime.isInitialized) { // (2) topLevelTime = LocalTime.now() } return topLevelTime.toString() }
リスト5の(1)のように、クラス内のプロパティと同様に、トップレベルプロパティ宣言にlateinitを付与できます。そして(2)のように、isInitializedを利用して初期化済みかのチェックも行うことができます。ただし制約があります。まず、ローカル変数にはisInitializedは利用できません。また、トップレベルプロパティで利用する場合は、そのプロパティが宣言された同一ファイル内で利用する必要があり、その場合は、(2)の「::topLevelTime.isInitialized」のように、「::」から始める記述とします。