sealedクラスとsealedインターフェース
sealedクラスとsealedインターフェースは、 制限されたクラスヒエラルキーを表現し、継承のさらなる制御の方法を提供します。 sealedクラスの直接のサブクラスは、全てコンパイル時にしられている必要があります。 それ以外のサブクラスがsealedクラスの定義されているパッケージやモジュールの外にあらわれてはいけません。 例えば、サードパーティーのクライアントはあなたのsealedクラスを彼らのコードの中で継承する事は出来ません。 かくして、sealedクラスの各インスタンスは、 このクラスがコンパイルされた時点で知られている限られた種類だけからなる型を持ち得ます。
同様の事はsealedインターフェースとその実装についても言えます: ひとたびsealdインターフェースのあるモジュールがコンパイルされれば、 それ以後新たな実装が現れる事はありえません。
ある意味では、enum
クラスに似ています。
enum型の値の種類も同じく制限されています。
けれど、それぞれのenum定数はシングルインスタンスとしてのみ存在するのに対し、
sealedクラスのサブクラスは複数のインスタンスをもつことができ、それぞれが独自の状態を保持できます。
例として、ライブラリのAPIを考えてみましょう。 ライブラリにはライブラリのユーザーがハンドルしたりthrow出来るエラーを含む場合があります。 そのようなエラーの継承ツリーにインターフェースや抽象クラスが含まれていてパブリックなAPIとして可視であるなら、 クライアント側でそのエラーを実装したり継承したりする事を防ぐ方法はありません。 ですがライブラリ側は外側で定義されたそんなエラーを知らないので、 自身のエラーの時と一貫した形で扱う事は出来ません。 sealedな継承階層のエラークラスなら、 ライブラリの作者は可能なすべてのエラーの型を確実に知る事が出来て、 後からそれ以外のものが追加されない事を確信出来ます。
sealedクラスやsealedインターフェースを宣言するには、
sealed
修飾子を名前の前に置きます:
sealed interface Error
sealed class IOError(): Error
class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()
object RuntimeError : Error
sealedクラスはそれ自身はabstractで、直接インスタンシエートは出来ません。
また、abstract
なメンバを持つ事が可能です。
sealedクラスのコンストラクタの可視性は二つに一つです:
protected
(こちらがデフォルト) または private
です:
sealed class IOError {
constructor() { /*...*/ } // デフォルトなのでprotected
private constructor(description: String): this() { /*...*/ } // privateはOK
// public constructor(code: Int): this() {} // Error: publicとinternalは許可されない
}
直接のサブクラスの置き場所
sealedクラスやsealedインターフェースの直接のサブクラスは、同じパッケージに宣言されなくてはいけません。 それらはトップレベルでも他のクラスやインターフェースやオブジェクトにネストした中でも構いません。 サブクラスは通常のKotlinの継承ルールに従っている範囲の、 いかなる可視性も持ち得ます。
sealedクラスのサブクラスはちゃんとした名前を持っていなくてはなりません。 ローカルオブジェクトや無名オブジェクトではいけません。
enum
クラスはsealedクラスを継承出来ません(sealedクラス以外のクラスと同様)が、sealedインターフェースを実装する事は出来ます。
sealedクラスの間接的なサブクラスにはこれらの制約は適用されません。 もしも直接のサブクラスがsealedとマークされていなければ、 そのサブクラスは通常の修飾子が許すいかなるようにも継承出来ます。
sealed interface Error // 同じパッケージとモジュールにだけ実装を持つ
sealed class IOError(): Error // 同じパッケージとモジュールでだけ継承可能
open class CustomError(): Error // このクラスが見える所ならどこでも継承可能
マルチプラットフォームプロジェクトでの継承
マルチプラットフォームプロジェクトの場合はさらにもう一つ継承に関わる制約があります:
sealedクラスの直接のサブクラスは同じソースセットに存在していないといけません。
これはexpect
と actual
修飾子の無いsealed クラスに適用されるルールです。
もしsealedクラスがcommonソースセットの方でexpect
と定義されてプラットフォーム側のソースセットにactual
として実装されていたら、
expect
側もactual
側もそれぞれのソースセットでサブクラスを持ち得ます。
さらに、hierarchical structureを用いると、
expect
とactual
の宣言の間のどのソースセットでもサブクラスを作る事が出来ます。
multiplatform projectsのhierarchical structureについてもっと学ぶ
Sealedクラスとwhen式
sealedクラスの主な利点は when
式 の中で使用されたときに発揮されます。
もし文が全てのケースをカバーすることを確認できれば、 else
句を追加する必要はありません。
fun log(e: Error) = when(e) {
is FileReadError -> { println("ファイルを読んでいる時にエラー: ${e.file}") }
is DatabaseError -> { println("データベースを読んでいる時にエラー: ${e.source}") }
is RuntimeError -> { println("実行時エラー") }
// 全てのケースをカバーした為、`else` 句は不要
}
マルチプラットフォームプロジェクトのcommonコードの中で、
expect
sealedクラスに対してwhen式を使う時は、else
節は必要です。これはcommonコード側としてはプラットフォーム側のサブクラスとなるactual
実装を知らないからです。