言語
日本語
English

Caution

お使いのブラウザはJavaScriptが無効になっております。
当サイトでは検索などの処理にJavaScriptを使用しています。
より快適にご利用頂くため、JavaScriptを有効にしたうえで当サイトを閲覧することをお勧めいたします。

Kotlin辞典

  1. トップページ
  2. Kotlin辞典
  3. sealed class / sealed interface

sealed class / sealed interface

対応: Kotlin 1.0(2016)

Kotlinの『sealed class』は、継承できるサブクラスを同じファイル内(またはネスト)に限定したクラスです。『when』式と組み合わせると網羅性チェックができ、状態管理や結果型の実装に活用されます。

構文

// sealed class の定義
sealed class Result<out T>
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
object Loading : Result<Nothing>()

// when 式で網羅的に処理(else 不要)
fun handle(result: Result<String>) = when (result) {
    is Success -> println("成功: ${result.data}")
    is Error   -> println("エラー: ${result.message}")
    is Loading -> println("読み込み中...")
}

// sealed interface(Kotlin 1.5以降)
sealed interface Shape
data class Circle(val radius: Double) : Shape
data class Rectangle(val w: Double, val h: Double) : Shape

構文一覧

構文概要
sealed class 名前同じコンパイルユニット内にのみサブクラスを持てるクラスです。
sealed interface 名前Kotlin 1.5以降で使える sealed インターフェースです。
is サブクラス名when 式での型チェックです。sealed class なら全ケース網羅で else 不要になります。
object サブクラス名 : 親()状態を持たないサブクラスをシングルトンとして定義します。

サンプルコード

sealed_class.kt
// UI 状態管理のパターン
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String, val code: Int = 0) : UiState<Nothing>()
}

// ネットワーク操作の模擬
fun fetchUser(id: Int): UiState<String> = when (id) {
    0 -> UiState.Loading
    in 1..100 -> UiState.Success("User-$id")
    else -> UiState.Error("ユーザーが見つかりません", 404)
}

// when 式で網羅的に処理(コンパイラが全ケース確認)
fun render(state: UiState<String>): String = when (state) {
    is UiState.Loading -> "読み込み中..."
    is UiState.Success -> "データ: ${state.data}"
    is UiState.Error -> "エラー(${state.code}): ${state.message}"
}

// sealed class を使ったコマンドパターン
sealed class Command
data class Move(val dx: Int, val dy: Int) : Command()
data class Rotate(val degrees: Int) : Command()
object Stop : Command()

fun execute(cmd: Command) {
    when (cmd) {
        is Move -> println("移動: (${cmd.dx}, ${cmd.dy})")
        is Rotate -> println("回転: ${cmd.degrees}度")
        is Stop -> println("停止")
    }
}

fun main() {
    // UiState のデモ
    listOf(0, 1, 999).forEach { id ->
        val state = fetchUser(id)
        println(render(state))
    }

    println()

    // コマンドパターンのデモ
    val commands = listOf(Move(10, 5), Rotate(90), Move(-3, 2), Stop)
    commands.forEach { execute(it) }
}
kotlinc sealed_class.kt -include-runtime -d sealed_class.jar
java -jar sealed_class.jar
読み込み中...
データ: User-1
エラー(404): ユーザーが見つかりません

移動: (10, 5)
回転: 90度
移動: (-3, 2)
停止

when式との連携

sealed classを『when』式(文ではなく式)として使うとコンパイラが全サブクラスの網羅性を検証します。網羅チェックが有効になるのは『when』を値を返す式として使ったときです。文として使うと網羅チェックが働かず、else省略によるバグが混入することがあります。

sample_sealed_when.kt
sealed class AuthState
object Unauthenticated : AuthState()
data class Authenticated(val username: String, val role: String) : AuthState()
data class AuthError(val reason: String) : AuthState()

// when を式として使う(戻り値あり)→ 網羅チェック有効
fun describeState(state: AuthState): String = when (state) {
    is Unauthenticated         -> "ログインしてください"
    is Authenticated           -> "ようこそ、${state.username}さん(${state.role})"
    is AuthError               -> "認証エラー: ${state.reason}"
    // ここで新しいサブクラスを追加すると即コンパイルエラーになります
}

// when を文として使う(戻り値なし)→ 網羅チェックが働かない(注意)
fun logState(state: AuthState) {
    when (state) {
        is Authenticated -> println("ログイン: ${state.username}")
        // 他のケースを書き忘れてもコンパイルエラーにならない
    }
}

fun main() {
    val states = listOf(
        Unauthenticated,
        Authenticated("okabe.rintaro", "admin"),
        AuthError("トークン期限切れ")
    )
    states.forEach { println(describeState(it)) }
}
kotlinc sample_sealed_when.kt -include-runtime -d sample_sealed_when.jar
java -jar sample_sealed_when.jar
ログインしてください
ようこそ、okabe.rintaroさん(admin)
認証エラー: トークン期限切れ

enum class との違い

sealed classとenum classはどちらも「取り得る状態を限定する」ために使われますが、用途が異なります。

比較項目sealed classenum class
インスタンス数サブクラスごとに何個でも作れます各定数は1つだけ(シングルトン)
プロパティサブクラスごとに異なるプロパティを持てます全定数が同じプロパティ構造を持ちます
継承サブクラスはclass/object/data classが使えます継承はできません
ユースケース状態ごとにデータが異なる場合(Success/Errorなど)固定された列挙値(曜日・方角など)
sample_sealed_vs_enum.kt
// enum class: 固定された定数の列挙に適しています
enum class Direction { NORTH, SOUTH, EAST, WEST }

// sealed class: 各状態が異なるデータを持つ場合に適しています
sealed class NetworkResult
data class NetworkSuccess(val body: String, val statusCode: Int) : NetworkResult()
data class NetworkFailure(val error: Throwable, val statusCode: Int) : NetworkResult()
object NetworkLoading : NetworkResult()

fun main() {
    val dir = Direction.NORTH
    println(dir.name)    // NORTH(enumはname/ordinalが使えます)
    println(dir.ordinal) // 0

    val result: NetworkResult = NetworkSuccess("{ \"user\": \"kurisu\" }", 200)
    val message = when (result) {
        is NetworkSuccess -> "成功(${result.statusCode}): ${result.body}"
        is NetworkFailure -> "失敗(${result.statusCode}): ${result.error.message}"
        is NetworkLoading -> "通信中..."
    }
    println(message)
}
kotlinc sample_sealed_vs_enum.kt -include-runtime -d sample_sealed_vs_enum.jar
java -jar sample_sealed_vs_enum.jar
NORTH
0
成功(200): { "user": "kurisu" }

よくあるミス1: whenを文として使う

when を「文」として使うと、網羅チェックが働かず、ケースを忘れてもコンパイルエラーになりません。

sealed class Event
data class Click(val x: Int, val y: Int) : Event()
data class KeyPress(val key: String) : Event()
object Idle : Event()

fun handleBad(event: Event) {
    when (event) {
        is Click -> println("クリック")
        // KeyPress と Idle を忘れてもコンパイルエラーにならない
    }
}

修正: when を「式」として使い、戻り値を持たせると網羅チェックが有効になります。

sample_sealed_mistakes.kt
sealed class Event
data class Click(val x: Int, val y: Int) : Event()
data class KeyPress(val key: String) : Event()
object Idle : Event()

fun handleGood(event: Event): String = when (event) {
    is Click    -> "クリック(${event.x}, ${event.y})"
    is KeyPress -> "キー: ${event.key}"
    is Idle     -> "待機中"
}

fun main() {
    println(handleGood(Click(100, 200)))
    println(handleGood(KeyPress("Enter")))
    println(handleGood(Idle))
}
kotlinc sample_sealed_mistakes.kt -include-runtime -d sample_sealed_mistakes.jar
java -jar sample_sealed_mistakes.jar
クリック(100, 200)
キー: Enter
待機中

よくあるミス2: 別ファイルへの定義・直接インスタンス化

sealed class のサブクラスを別ファイルに定義しようとするとエラーになります(Kotlin 1.5未満では同じファイル内のみ有効。1.5以降は同じコンパイルユニット内ならOK)。また sealed class 自体を直接インスタンス化することはできません(val e = Event() はコンパイルエラー)。

概要

『sealed class』は、取り得る状態を型レベルで制限できるため、「Success/Error/Loading」のような状態機械、「Result型」、「代数的データ型(ADT)」の実装に適しています。

『when』式で全サブクラスを処理した場合、コンパイラが網羅性を検証します。新しいサブクラスを追加したとき、処理漏れがあればコンパイルエラーになるため、保守性が高まります(『when』を式として使うと網羅チェックが有効になります)。

クラスの基本はclass / コンストラクターを、スマートキャストはスマートキャスト / as?を参照してください。

記事の間違いや著作権の侵害等ございましたらお手数ですがまでご連絡頂ければ幸いです。