December 22, 2022

Kotlin 1.9から導入予定のdata objectでsealed classをちょっといい感じにする

この記事はKotlin Advent Calendar 2022 の22日目の記事です。

Kotlin Advent Calendarに参加するのは今年で8年連続8回目になりました🎉🐦
毎年22日近辺を書いています。マイルストーンの時から書いている記事もあるので情報が古くなっているものもありますが、 過去にはこんな記事を書いていました。

今年は小ネタですが、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で定義されているStartedFailedは出力がStarted(id=1234), Failed(reason=java.lang.IndexOutOfBoundsException) のようにいい感じに出力されるのですが、LoadingEnded の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!

© AAkira 2023