Edit Page

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クラスの直接のサブクラスは同じソースセットに存在していないといけません。 これはexpectactual 修飾子の無いsealed クラスに適用されるルールです。

もしsealedクラスがcommonソースセットの方でexpectと定義されてプラットフォーム側のソースセットにactualとして実装されていたら、 expect側もactual側もそれぞれのソースセットでサブクラスを持ち得ます。 さらに、hierarchical structureを用いると、 expectactualの宣言の間のどのソースセットでもサブクラスを作る事が出来ます。

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実装を知らないからです。