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 class | enum 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?を参照してください。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。