Kotlin Sealedクラス

Sealedクラス(Sealed classes)

sealedクラスとインターフェースは、継承をより効率的に管理できる制限されたクラス階層を表す。Sealedは封印という意味で、Enumクラスの拡張形として、Sealedクラスは複数のクラスをまとめたクラスだと言える。Sealedクラスのすべての直接サブクラスはコンパイル時に認識される。Sealedクラスを含むモジュールがコンパイルされた後は、他のサブクラスは現れない。たとえば、サードパーティのクライアントコードではSealedクラスを拡張できない。したがって、Sealedクラスの各インスタンスは、そのクラスがコンパイルされるときに認識される制限された集合の型を持つ。

sealedインターフェースの実装についても同じように動作する。Sealedインターフェースを持つモジュールがコンパイルされた後は、新しい実装はできない。

ある意味で、sealedクラスは列挙型クラスに似ている。列挙型の値集合も制限されるが、各列挙型定数は単一インスタンスとしてのみ存在する。一方、Sealedクラスのサブクラスは、それぞれ固有の状態を持つ複数のインスタンスを持つことができる。

たとえば、ライブラリのAPIについて考えてみる。ライブラリ利用者が投げられる例外を処理できるように、エラークラスが含まれている場合がある。このような例外クラス階層に、公開APIに表現されるインターフェースまたは抽象クラスが含まれていると、クライアントコードでそれを実装または拡張することを防げない。しかし、ライブラリはライブラリ外部で宣言されたエラーを認識しないため、自身のクラスでエラーを一貫して処理できない。エラークラスの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クラスはそれ自体が抽象的であり、直接インスタンス化できず、抽象メンバーを持つことができる。

sealedクラスのコンストラクタは、基本的にprotectedまたはprivateの2つの可視性のいずれかを持つことができる。

sealed class IOError {
    constructor() { /*...*/ } // protected by default
    private constructor(description: String): this() { /*...*/ } // private is OK
    // public constructor(code: Int): this() {} // Error: public and internal are not allowed
}

直接サブクラスの場所(Location of direct subclasses)

sealedクラスとインターフェースの直接サブクラスは、同じパッケージに宣言されなければならない。トップレベルでもよく、別の名前付きクラス、名前付きインターフェース、名前付きオブジェクトの中にネストすることもできる。サブクラスはKotlinの通常の継承規則と互換性がある限り、任意の可視性を持てる。

sealedクラスのサブクラスには、適切な修飾名が必要である。ローカルオブジェクトにも匿名オブジェクトにもできない。

enumクラスはsealedクラスやその他のクラスを拡張できないが、sealedインターフェースは実装できる。

これらの制限は間接サブクラスには適用されない。sealedクラスの直接サブクラスがsealedとしてマークされていない場合、修飾子で許可される方法で拡張できる。

sealed interface Error // has implementations only in same package and module

sealed class IOError(): Error // extended only in same package and module
open class CustomError(): Error // can be extended wherever it's visible

マルチプラットフォームプロジェクトの継承(Inheritance in multiplatform projects)

マルチプラットフォームプロジェクトには、継承制限がもう1つある。sealedクラスの直接サブクラスは、同じソースセットに存在しなければならない。これはexpect修飾子とactualで修飾されていないsealedクラスに適用される。

sealedクラスが共通ソースセットでexpectとして宣言され、プラットフォームソースセットにactual実装がある場合、expect版とactual版の両方で、それぞれのソースセットにサブクラスを含めることができる。また、階層構造を使用する場合、expect宣言とactual宣言の間の任意のソースセットにサブクラスを作成できる。

マルチプラットフォームプロジェクトの階層構造について詳しく確認してほしい。

sealedクラスとwhen式(Sealed classes and when expression)

sealedクラスを使用する主な利点は、whenを使用するときにある。文がすべての場合を含むことを確認できる場合、文にelse節を追加する必要がない。ただし、これはwhenを文ではなく式(結果を使用するもの)として使用する場合にのみ動作する。

fun log(e: Error) = when(e) {
    is FileReadError -> { println("Error while reading file ${e.file}") }
    is DatabaseError -> { println("Error while reading from database ${e.source}") }
    RuntimeError ->  { println("Runtime error") }
    // the `else` clause is not required because all the cases are covered
}

マルチプラットフォームプロジェクトの共通コードでは、期待されるsealedクラスに対する式であってもelse分岐が必要になる場合がある。これは、実際のプラットフォーム実装のサブクラスが共通コードでは認識されないために発生する。

参考