この記事はKotlin Advent Calendar 2022 の22日目の記事です。
Kotlin Advent Calendarに参加するのは今年で8年連続8回目になりました🎉🐦
毎年22日近辺を書いています。マイルストーンの時から書いている記事もあるので情報が古くなっているものもありますが、
過去にはこんな記事を書いていました。
2018年 22日: Kotlin Multiplatform環境でKotlin SerializationとAndroid ExtensionsのParcelize Annotationを使う
2019年 20日(代打): オレの考えた最強のKotlin Multiplatform Projectアーキテクチャ2020
2021年 22日: さよならfreeze! ついにきたKotlin/Native New Memory Management!!
今年は小ネタですが、Kotlin1.9から導入予定の data object
を使って今までsealed classの痒いなと思っていたところが解消されたので紹介したいと思います。
事前準備
Kotlin1.9はまだEAPなため、data objectを試すにはgradleに languageVersion
を追加する必要があります。🐘
tasks.withType<KotlinCompile> {
kotlinOptions.languageVersion = "1.9"
}
1.7.20から使えるのですが、せっかくなので一昨日リリースされた1.8.0-RC2 を使いましょう。
plugins {
kotlin("jvm") version "1.8.0-RC2"
}
data object
例として、ビデオの状態管理用のsealed classがあるとします。Started
時はVideoのid、Failed
時はエラー原因が含まれている。というよくある感じのsealed classです。
sealed class VideoState {
object Loading : VideoState()
data class Started(val id: String) : VideoState()
object Ended : VideoState()
data class Failed(val reason: Throwable) : VideoState()
}
一見これでも良さそうなのですが、例えばVideoのStateListenerがあったとして送られてきた状態を出力してみます。
videoManager.addStateObserver { state ->
println("VideoState: $state") // VideoState: VideoState$Loading@34ce8af7
}
この場合 data classで定義されているStarted
やFailed
は出力がStarted(id=1234)
, Failed(reason=java.lang.IndexOutOfBoundsException)
のようにいい感じに出力されるのですが、Loading
や Ended
のStateが来た場合には、VideoState$Loading@34ce8af7
のように出力の最後にHashが含まれてしまいます。
これはdata classの場合には自動で toString
が実装されているためです。
じゃあdata classにすればいいじゃんと思いますが、data classは最低でも引数を1つ取る必要があるため、Enumのようにsealed classを使いたい場合はobjectで定義するしかありませんでした。
(classで定義する方法もありますが、State定義に毎回インスタンス生成するのは微妙ですし、toStringを実装しなければHashCodeが出力されるのは同じです。)
そこで登場したのが、data object
です。
objectで定義していたところを全て data object
に書き換えるとこうなります。
sealed class VideoState {
data object Loading : VideoState()
data class Started(val id: String) : VideoState()
data object Ended : VideoState()
data class Failed(val reason: Throwable) : VideoState()
}
既存のobjectの前にdataをつけるだけで使えます。
data objectにすると引数なしのdata classが定義でき、VideoStateを出力してもHashが表示されずに、単純にsealed class内に定義した名前で出力されるようになりました 🎉
val state = VideoState.Loading
println("VideoState: $state") // VideoState: Loading
Java bytecode
data object
の実装がどうなっているのかJavaのコードを見るとこうなっていました。
public static final class Loading extends VideoState {
@NotNull
public static final Loading INSTANCE;
private Loading() {
super((DefaultConstructorMarker)null);
}
static {
Loading var0 = new Loading();
INSTANCE = var0;
}
@NotNull
public String toString() {
return "Loading";
}
public int hashCode() {
return 739009572;
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (!(var1 instanceof Loading)) {
return false;
}
Loading var2 = (Loading)var1;
}
return true;
}
}
data class
とは異なり、copyの実装はありません。
ただのobject
はこうなるので、シンプルにシングルトンのクラスに data class
同様 toString
, hashCode
, equals
を実装したクラスになるようです。
public static final class Loading extends VideoState {
@NotNull
public static final Loading INSTANCE;
private Loading() {
super((DefaultConstructorMarker)null);
}
static {
Loading var0 = new Loading();
INSTANCE = var0;
}
}
とても地味ですが昔から痒いところではあったので、このアップデートはとても嬉しいですね!🎉🐦
おまけ
(個人の感想です)
最近某企業でKotlinを辞める記事がバズっていて、理由の1つに KotlinのMultiplatformへの投資に多くのリソースを割いているというのがありましたが、個人的にはそうでもないのかなと思っています。
確かにKotlin Multiplatform ProjectはKotlin ConfのKeynoteで毎年大々的に取り上げており、Kotlinのここ4, 5年の大きな目標であるようには見えます。直近の1.7, 1.8のリリースもMultiplatform関連以外には今回の記事のような微妙な修正で、大きな機能変更は見当たりませんが、直近はMultiplatformにリソースを割いているよりもK2コンパイラの置き換えにリソースを割いている感じがします。
最終的にはMultiplatform Projectの強化に繋げるためだとは思いますが、コンパイラを1から書き直しているので、この作業が終わればまた言語機能の強化にリソースを割いていくのではないかなと思います。
2023年以降のロードマップ も公開されており、ユーザーアンケート も丁寧にとって公開していますし、JetBrainsはまだまだ改善していく気満々だと思います。
2022/12/20のKotlin Slackのポスト
今年はKMMがついにBetaになり2023年にはStableになる予定です!!!(ついにきた)
2023年もKotlinのさらなる進化に期待しましょう!
Have a nice Kotlin!