Kotlin sealed classについて

採用はこちら

Kotlinのsealed classは、継承できる型を制限できる特別なクラスです。

通常のclassabstract classでは、設計によってはあとから自由にサブクラスを増やせますが、sealed classを使うと、あらかじめ決めた範囲内でだけ派生を許可する形にできます。

そのため、sealed classは「状態の種類を限定したい」「取りうるパターンを明確にしたい」ときにとても役立ちます。

特に、画面の状態管理やAPIの処理結果、エラーの種類分けなどでよく使われます。

目次

sealed classの基本的な考え方

sealed classの大きな特徴は、直接のサブクラスを制限できることです。

これによって、親クラスに対して「この型は、このいくつかのパターンしか持たない」という設計を表現しやすくなります。

たとえば、通信状態を「成功」「失敗」「読み込み中」の3種類に限定したい場合、次のように書けます。

sealed class ApiResult {
    data class Success(val data: String) : ApiResult()
    data class Error(val message: String) : ApiResult()
    data object Loading : ApiResult()
}

このように書くと、ApiResultSuccessErrorLoading のいずれかとして扱えます。

状態の種類がはっきり決まっているため、コードの見通しがよくなります。

sealed classを使うメリット

sealed classの大きなメリットは、when式との相性が非常によいことです。

fun render(result: ApiResult): String {
    return when (result) {
        is ApiResult.Success -> "成功: ${result.data}"
        is ApiResult.Error -> "失敗: ${result.message}"
        ApiResult.Loading -> "読み込み中"
    }
}

このように、想定されるパターンをすべて書けていれば、elseを書かずに済むことがあります。

つまり、分岐漏れに気づきやすいのが大きな利点です。

ただし、どんな場合でも必ずelseが不要になるわけではありません。

型の扱い方や書き方によっては、補助的にelseが必要になるケースもあります。

そのため、sealed classは「whenの網羅性チェックに強い」と理解しておくのが正確です。

sealed classのルール

sealed classには、いくつか押さえておきたいルールがあります。

まず、sealed class暗黙的にabstractです。

そのため、sealed classそのものを直接インスタンス化することはできません。

sealed class Example

このExampleをそのままExample()のように生成することはできず、必ず子クラスを通して利用します。

また、sealed class直接のサブクラスには定義場所の制約があります。

基本的には、同じpackage内で定義する必要があります。

さらに仕様上は、同じmodule内であることも条件です。

そのため、どこからでも自由に継承できるわけではありません。

この制約があるからこそ、型のバリエーションを管理しやすくなります。

なお、ローカルクラスや無名オブジェクトは、sealed class直接のサブクラスにはできません。

昔のKotlinとの違い

sealed classについて調べると、「子クラスは同じファイルに書かなければならない」と説明している古い記事を見かけることがあります。

これは、以前のKotlinでは強く意識する必要があった制約です。

ただし、現在のKotlinでは、以前より柔軟になっており、同じpackage内であれば複数ファイルに分けて定義できるようになっています。

そのため、最近のKotlinを前提にするなら、「必ず同一ファイルでなければならない」という理解は古い情報です。

abstract classとの違い

sealed classは抽象的な親クラスとして使われるため、abstract classと似て見えることがあります。

しかし、両者にははっきりした違いがあります。

abstract classは、共通の性質や振る舞いを持つ親クラスを作るための仕組みです。

一方で、sealed classはそれに加えて、派生パターンを制限する役割を持っています。

つまり、

  • abstract classは「共通化したい」ときに使う
  • sealed classは「共通化しつつ、種類も限定したい」ときに使う

という違いがあります。

enum classとの違い

sealed classとよく比較されるのがenum classです。

enum classは、決まった定数を列挙したいときに向いています。

たとえば、曜日や信号の色のように、単純な選択肢を表すにはenumが適しています。

一方、sealed classは、ケースごとに異なるデータを持たせたい場合に向いています。

たとえば、次のような場合です。

  • 成功時だけデータを持つ
  • 失敗時だけエラーメッセージを持つ
  • 読み込み中はデータを持たない

このように、状態ごとに構造が違うなら、enum classよりsealed classのほうが自然です。

使い分けの目安

  • 単純な選択肢の列挙ならenum class
  • ケースごとに持つデータや構造が違うならsealed class

このように考えると整理しやすいです。

data classとの関係

ここは混同しやすいポイントです。

sealed class自体をdata classにすることはできません。

つまり、sealed data classのような書き方はできません。

ただし、sealed classの子クラスをdata classにすることは可能です。

実際にはこの使い方が非常によく使われます。

sealed class UiEvent {
    data class Click(val id: Int) : UiEvent()
    data class Submit(val text: String) : UiEvent()
    data object Cancel : UiEvent()
}

このように、親をsealed classにし、データを持つ子をdata classにすると、とても整理しやすくなります。

sealed interfaceとの違い

Kotlinにはsealed interfaceもあります。

考え方はsealed classとほぼ同じで、実装できる型を制限したいインターフェースです。

sealed classはクラス継承で表現したいときに向いています。

一方で、既存の継承構造と組み合わせたい場合や、より柔軟に設計したい場合は、sealed interfaceのほうが使いやすいこともあります。

そのため、

  • 親としてクラスを使いたいならsealed class
  • 親としてインターフェースを使いたいならsealed interface

という理解で問題ありません。

sealed classが向いている場面

sealed classは、次のようなケースで特に力を発揮します。

APIの結果を表現するとき

sealed class Result<out T> {
    data class Success<T>(val value: T) : Result<T>()
    data class Failure(val error: Throwable) : Result<Nothing>()
    data object Loading : Result<Nothing>()
}

このように書くと、「成功」「失敗」「読み込み中」という状態を1つの型として安全に扱えます。

UIの状態管理をしたいとき

sealed class ScreenState {
    data object Initial : ScreenState()
    data object Loading : ScreenState()
    data class Content(val items: List<String>) : ScreenState()
    data class Error(val message: String) : ScreenState()
}

複数のBooleannullで状態を管理するよりも、こちらのほうが状態の意味がはっきりします。

イベントやエラー種別を整理したいとき

  • ボタンクリック
  • 送信
  • キャンセル
  • 認証エラー
  • 通信エラー
  • 入力エラー

このような「種類が決まっているもの」をまとめるのにも向いています。

sealed classを使うときの注意点

sealed classは便利ですが、どんな場面でも使えばよいわけではありません。

外部から自由に拡張されることを前提にした設計では、sealed classは向かない場合があります。

たとえば、プラグイン形式の仕組みや、利用側が独自に実装を増やすことを想定しているAPIでは、通常のinterfaceabstract classのほうが適していることがあります。

sealed classは、あくまで閉じたパターンを表現したいときの仕組みです。

そのため、「あとから自由に増やしたいか」「最初から限定したいか」を基準に使い分けることが大切です。

よくある誤解

子クラスは必ず同じファイルに書く必要がある?

現在のKotlinでは、必ずしもそうではありません。

古い情報では同一ファイル制約が強く説明されていることがありますが、今は同じpackage内で複数ファイルに分けられるようになっています。

sealed classなら必ずelseが不要?

必ずではありません。

ただし、whenの網羅性チェックに強く、適切に全ケースをカバーできていればelseなしで書けることがあります。

enum classで代用できる?

単純な列挙なら可能です。

しかし、ケースごとに持つデータや構造が違う場合は、sealed classのほうが表現しやすいです。

まとめ

Kotlinのsealed classは、直接のサブクラスを制限することで、型のパターンを安全に扱いやすくする仕組みです。

特に次のような特徴があります。

  • 継承できる範囲を制限できる
  • whenで網羅的に分岐しやすい
  • 状態管理や結果型、イベント表現に向いている
  • enum classより柔軟にデータを持たせられる

そのため、sealed classは、「この型の種類はあらかじめ決まっている」という場面で非常に強力です。

APIの結果、UI状態、イベント、エラー種別などをわかりやすく安全に表現したいなら、sealed classはとても有力な選択肢になります。

以上、Kotlin sealed classについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次