ザクっと理解するKotlin 密閉クラス(Sealed classes)と密閉インターフェース(Sealed interface)
密閉クラスと密閉インターフェースとは
密閉クラスと密閉インターフェースは継承において強い力を持っています。
密閉クラスの直接のサブクラスは、同じパッケージまたはモジュール内で宣言する必要があります。
他のパッケージやモジュールからは、その密閉クラスのサブクラスを継承することはできません。
例えば、サードパーティのコードの密閉クラスを拡張することはできません。
密閉インターフェースも同様に、密閉インターフェースがあるモジュール以外ではサブクラスを宣言できません。
密閉クラスとenumクラスは似ている点があります。
列挙型の値も制限されますが、列挙定数は個々に単一のインスタンスとして存在します。
一方、密閉クラスのサブクラスは複数のインスタンスを持つことができ、それぞれが独自の状態を持つことができます。
例としてライブラリのAPIを考えてみます。
ライブラリは投げる可能性のあるエラーを処理するためのエラークラスを含むことがあります。
もしエラークラスの階層に、パブリックなAPIで可視なインターフェースや抽象クラスが含まれている場合、ユーザーが実装または拡張することが可能です。
しかし、ライブラリは外部で宣言されたエラーを知ることはできないため、自身のクラスと一貫して処理することはできません。
密閉クラスを使用することで、ライブラリの作者はすべての可能なエラータイプを把握しており、後から他のタイプが現れることはありません。
密閉クラスとインターフェースの宣言
sealedキーワードを名前の前につけることで宣言できます。
sealed interface Error
sealed class IOError(): Error
class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()
object RuntimeError : Error
密閉クラスは抽象クラスであり、抽象メンバーを持つことはできますが、直接インスタンス化することはできません。
コンストラクタの可視性はprotected(デフォルト)かprivateのみしか設定できません。
直接のサブクラスの宣言場所
密閉クラスやインターフェースとその直接のサブクラスは同じパッケージ内で宣言される必要があります。
サブクラスは、Kotlinの継承のルールに則っていれば、どの可視性を持っていても問題ありません。
enumクラスは密閉クラスを継承できませんが、密閉インターフェースは可能です。
これらの制限は間接的なサブクラスには当てはまりません。
密閉クラスの直接のサブクラスにsealedキーワードがついていなければ、これらの制限はありません。
sealed interface Error // 同パッケージ、モジュール内でのみこれを継承可能
sealed class IOError(): Error // 同パッケージ、モジュール内のため継承可能
open class CustomError(): Error // 参照できるのならどこからでも継承可能
密閉クラスとwhenキーワード
密閉クラスを使用するメリットはwhenキーワードを使用する際に現れます。
条件が全てのケースをカバーしている場合、elseキーワードが不要になります。
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}") }
is RuntimeError -> { println("Runtime error") }
// 全てのケースを網羅しているためelseキーワードが不要
}