Edited at
KotlinDay 12

Kotlinのsealed classを使いこなす

Kotlinが書きやすすぎて、仕事のAndroid開発だけでなく趣味のデスクトップアプリもKotlinで書いています。

Kotlinには便利な言語機能がたくさんあります。

その中でも個人的には sealed class が好きなので、この魅力をご紹介したいと思います。


sealed class とは

日本を代表するKotlinエバンジェリストのたろうさんのブログによると、


一言で言うと、クラスの継承を制限するための修飾子です。 sealedが付いたクラスを継承するにはある条件を満たす必要があるということです。


kotlin 1.2.1現在、sealed classを継承できるのは、sealed classでネストされたクラスと、同じファイル内で宣言されたクラスのみです。

// in Base.kt

sealed class Base {
object SubOne: Base() // ok
object SubTwo: Base() // ok
}

class SubThree: Base() // ok

// in Other.kt
object SubFour: Base() // ng

なるほど、わかりました。

しかしこれ、どう使えば良いのでしょう?


sealed classの使い所

Kotlinのリファレンス によるとこんな感じの用途で使うようです。


Sealed classは、限定された範囲の型のうちの一つの値を持ちたい場合、クラス階層の範囲を限定したいときに使用されます。

enum classの拡張版のようなもので、enumの定数は単一インスタンスとして存在しますが、sealed classの子クラスは複数のインスタンスであり、状態を持つことができます。

Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type. They are, in a sense, an extension of enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances which can contain state.


なるほどわからん。

私の訳が下手なだけかもしれません。

しかし便利な使い方があるのです。

百聞は一見にしかず。具体的に見ていきましょう。

順を追って見て行きましょう。


規定値を選ばせたいが、カスタム値も指定できるようにしたい

「拡張版enumみたいなもの」とリファレンスに書いてあるので、そのイメージで使ってみます。


例:色

例えば、何かしらの色で塗りつぶしをしたいとしましょう。

fun fill(color: Color) {

...
}

Color は使いやすいように、デフォルトとして定義済みの色をいくつか用意しておいて、場合によっては任意の値を受け取れるようにしたいとします。

さて、どう宣言したら良いでしょうか。

定義済みのものから何かを選ぶ場合、真っ先に思いつくのはenumですね! まずenumで宣言してみましょう。


enum class Color {
RED,
GREEN,
BLUE
}

カスタム値はどうしましょう? enum定数に宣言後に値を持たせることはできません。

CMYKでも色を指定したい、というオーダーが出てきたら? ちょっと頭を抱えてしまいそうですね…

そこで使えるのがsealed classです。

sealed class Color {

// objectは単一インスタンスのみもつクラスを作るキーワード。
// つまりenum定数を作るのとあまり変わらない。
object Red: Color()
object Green: Color()
object Blue: Color()

// 任意のclassがsealed classの子クラスになれるので
// data classでも良い。
data class Rgb(val red: Int, val green, val blue): Color()
data class Cmyk(val cyan: Int, val magenta: Int, val yellow: Int, val keyPlate: Int): Color()
}

なんと、定数だけでなくカスタム値も表現できます。

便利!! :muscle:


値の重ね合わせを表現したい

少し突っ込んで考えてみましょう。

上の Color クラスというのは、 複数の状態が重ね合わさったもの として捉えることはできないでしょうか?

特に data class の部分だけを取り出して見てみましょう。

sealed class Color {

data class Rgb(val red: Int, val green, val blue): Color()
data class Cmyk(val cyan: Int, val magenta: Int, val yellow: Int, val keyPlate: Int): Color()
}

これは、「Color とは(表現方法はわからないけれど)色であり、色の表現方法としては Rgb もしくは Cymk がある」と読むこともできます。

この 「もしくは」 を型で表現できるのが代数的データ型と言われるものです。

そう捉えると、表現できるものの範囲が広がるのではと思いませんか?


例:成功、もしくは失敗

何かしらの処理を行った結果を呼び出し元に返したい場合、通常はreturnを使います。


fun doSomeProc(): SomeData {

...

// 結果を返す
return someValue
}

常に成功する場合はそれで良いでしょう。

では失敗する場合は? 戻り値が Int などなら -1 を異常値とすることもありますが、Javaの世界だと例外を投げるのが通例ですね。


fun doSomeProc(): SomeData {

...

// 失敗した場合は例外を投げて値を返さない
if (failed) throw FailureException()

return someValue
}

しかしKotlinには検査例外がありません。

もしうっかり try-catch を忘れてしまうと実行時例外でアプリがクラッシュすることもあり得ます。

それでは、値で成功と失敗を表現してみたらどうでしょう?

sealed class Result<V>

data class Success<V>(val value: V): Result<V>()
data class Failure<V>(val reason: Exception): Result<V>()

fun doSomeProc(): Result<SomeData> {

...

return if (failed) Failure(FailureException())
else Success(someValue)
}

val result = doSomeProc()
when (result) {
// smart castしてます。便利。
is Success -> println("data is ${result.value}")
is Failure -> println("process failed because ${result.reason}")
}

これで成功時と失敗時の挙動を明確に書くことができました! :tada:

is による型チェックがないと中の値を取り出すことができないので、成功したのか、失敗したのかの検査を強制することができます。

複数人開発のときに便利ですね!

はい、お気づきかと思いますがこれはいわゆる Either型 です。

ちゃんとしたEitherの実装には map などがあっていちいち when に入れなくても良いようになっていたりするので、実用する際は有志の方が公開されているライブラリを使うと良いでしょう。

(2019/04/02 追記)

そういえばKotlin 1.3から、kotlin-stdlibにResult型が公式に採用されています!

便利な専用拡張関数なども定義されているので、じゃんじゃん使っていきましょう!


例:状態管理

成功と失敗、2つを重ね合わせることができるなら、複数の状態を重ね合わせることだってできます。

アプリの画面で困るのは、複数の非同期処理が走ってしまうことだったりします。

例えば、データの読み込み中にまた読み込みのイベントが発生してしまったら…? AtomicBoolean か何かで読み込み中フラグを用意しますか? では読み込み中の状態の表示テストをしたい場合はどうしましょう? いつフラグをセットしますか?

そうしたことを考え始めると、頭がパンクしてしまいそうです。

そこでsealed class!

アプリの画面状態の管理にも使えてしまうのです!

私が特に便利に使っていて、sealed classが好きな理由でもあります。

// 何かのデータを読み込んで、それを表示する画面の状態管理クラス

class SomeScreenModel(private val repo: Repository, initState: State = State.NoData) {
sealed class State {
// まだ読み込みしていない状態
object NoData: State()
// 読み込み中
object Loading: State()
// 読み込み正常終了
data class Loaded(val hoge: String, val fuga: Int): State()
// 読み込み失敗
data class LoadFailure(val reason: Throwable): State()
}

// RxJavaのObservableや、JavaFXのObservableValueなどで変更を通知できるようにしておくと更に便利

private val subject = BehaviorSubject.createDefault(initState)

val stateChangeEvent: Observable<State>
get() = subject

var currentState: State
get() = subject.value

// 読み込む
fun load() {
// メンバのままだと値が変わる可能性がありsmart castが働かないので、一度取り出しておく
val currentState = currentState
// 読み込み中だったら無視
if (currentState is State.Loading) return

subject.onNext(State.Loading)

repo
.load()
.subscribe({ value ->
subject.onNext(State.Loaded(value.hoge, value.fuga))
}, { e ->
// 例えば失敗を告げるダイアログだけ表示させてから
subject.onNext(State.LoadFailure(e))
// 未読み込み状態に戻るとか
subject.onNext(State.NoData)
})
}
}

たったこれだけで以下のことが実現できています。


  • 画面が取りうる状態を網羅している

  • 状態に付随するパラメータも保持できる

  • 画面が現在表示しているべき状態がわかる

  • 画面の状態を変えるべき時にイベントが飛んでくる

  • 任意の状態から画面表示を開始することができる

  • 特定の状態のときには実行できない処理を無視できる

あとは、このクラスが抱える「画面状態」を見て、その状態を画面表示に反映するクラスを作れば、画面が未定義の状態に陥る事はほぼ無いでしょう。

画面のデバッグがすこぶる簡単になります!

Androidでのこうした状態管理の方法は以前の勉強会でお話したことがありますので、具体的な実装方法などはこちらのスライドを御覧ください。


おわりに

こうした言語機能以外にも、まだexperimentalなKotlin Android Extensionsの機能やKotlin/Nativeなど、Kotlinの魅力はまだまだ増えてゆきそうです。

来年も楽しいKotlinライフを送れますように! Have a nice kotlin!!