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関数の呼び出しをチェックする必要があった。 詳細は以下の記事を参照のこと。
Kotlin 2.0.20での改善
新しいバージョンでは、data classのコンストラクタの可視性がそのままcopy関数の可視性となる。先の例では、User
クラスのコンストラクタがprivate
なので、copy関数も同様にprivate
になる。
この挙動を有効にするには、次の2つの方法がある。
- コンパイラオプション
-Xconsistent-data-class-copy-visibility
を使用してプロジェクト全体に適用する。 - 対象のデータクラスに
@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で充分だと考えるため、今回のアップデートの内容に非常に満足している。