/var/log/jsoizo

メモ帳 技術とか趣味とか

Kotlin 2.0.20で追加されたdata classのcopyメソッドの可視性に関するコンパイラオプション

Kotlin 2.0.20がリリースされた。

その中で注目すべき機能の一つがこれ。

Data class copy function to have the same visibility as constructor

この待望の機能について詳しく見ていく。

従来の問題点

まず、以下のようなdata classを考えてみる。

data class User private constructor(val name: String, val age: Int) {
    companion object {
        fun create(name: String, age: Int): Result<User> {
            if (name.isBlank()) {
                return Result.failure(IllegalArgumentException("Name cannot be blank"))
            }
            if (age < 0) {
                return Result.failure(IllegalArgumentException("Age cannot be negative"))
            }
            return Result.success(User(name, age))
        }
    }
}

このサンプルでは User.createメソッドを通じてインスタンス生成を強制し、バリデーションを確実に行うことができる。しかし、data class(およびvalue class)に対してコンパイル時に自動生成されるcopy関数には、このような制約が適用されなかった。

そのため、以下のようにバリデーションを回避することができてしまっていた。

val user = User.create("Jun", 30).getOrThrow() // 例示のため強制的にgetしている
val copiedUser = user.copy("", -1)
println(copiedUser)
// User(name=, age=-1)

この問題を防ぐには、ArchUnit等のツールを使用して、不適切なcopy関数の呼び出しをチェックする必要があった。 詳細は以下の記事を参照のこと。

zenn.dev

Kotlin 2.0.20での改善

新しいバージョンでは、data classのコンストラクタの可視性がそのままcopy関数の可視性となる。先の例では、Userクラスのコンストラクタがprivateなので、copy関数も同様にprivateになる。

この挙動を有効にするには、次の2つの方法がある。

  1. コンパイラオプション -Xconsistent-data-class-copy-visibility を使用してプロジェクト全体に適用する。
  2. 対象のデータクラスに @ConsistentCopyVisibility アノテーションを付ける。

コンパイラオプションを付ける場合の例:

$ kotlin -version
Kotlin version 2.0.20-release-360 (JRE 17.0.5+8-LTS)

$ kotlin -Xconsistent-data-class-copy-visibility ./example.kts
example.kts:17:27: error: cannot access 'fun copy(name: String = ..., age: Int = ...): User': it is private in '/User'.
    val copiedUser = user.copy("", 0)

アノテーションを使用する例:

@ConsistentCopyVisibility
data class User private constructor(val name: String, val age: Int) {
   // 中身は省略
}

結果はどちらも同じとなった。

$ kotlin ./example_annotation.kts
example_annotation.kts:18:27: error: cannot access 'fun copy(name: String = ..., age: Int = ...): User': it is private in '/User'.
    val copiedUser = user.copy("", 0)

なお、コンパイラオプション consistent-data-class-copy-visibility が設定されていても、@ExposedCopyVisibility アノテーションを使用すれば従来の動作を維持できる。

@ExposedCopyVisibility
data class User private constructor(val name: String, val age: Int) {
   // 中身は省略
}

この場合、copyが可能になる。

$ kotlin -Xconsistent-data-class-copy-visibility ./example_can_copy.kts
User(name=, age=-1)

まとめ

この新機能により、不適切なcopy関数の呼び出しを防ぐことができ、コードの安全性が向上する。ただし、copy関数の呼び出し元を完全には禁止せず部分的に許可したいような場合は、引き続きArchUnit等のツールを使用してテスト時にチェックする必要がある。

個人的には、コンストラクタを隠蔽している時点でcopy関数はprivateで充分だと考えるため、今回のアップデートの内容に非常に満足している。