Kotlinの sealed class
に親しんでいきます。
sealed class
は、継承に制約を課す言語機能です。Kotlinのdocsによれば、 sealed class
として定義されたクラスのサブクラスは、すべてコンパイル時に定義されている必要があります。
Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance. All direct subclasses of a sealed class are known at compile time. No other subclasses may appear after a module with the sealed class is compiled. For example, third-party clients can’t extend your sealed class in their code. Thus, each instance of a sealed class has a type from a limited set that is known when this class is compiled. Sealed classes | Kotlin
sealed class
sealed class
の簡単なサンプルコードを以下に示します。親の sealed class
としてSealedClassというクラスを定義しています。また、そのサブクラス( object
)として、A、B、Cを定義しています。 sealed class
の付けられたクラスはabstractになります。そのため、SealedClass自身をインスタンス化することはできず、インスタンスとして存在し得るのは、この場合、そのサブクラスであるA、B、Cのみです。
sealed class SealedClass
object A : SealedClass()
object B : SealedClass()
object C : SealedClass()
sealed class
のサブクラスは、コンパイル時に定義されている必要があるのでした。この性質は、 when
によるパターンマッチングと相性がよく、サブクラスの型に応じた処理の分岐を容易に行うことができます。コンパイル時に全てのサブクラスがわかっているので、 when
のブランチを包括的(exhaustive)に定義することができます。IntelliJ IDEAやAndroid Studioでは、 when
のブランチがexhaustiveでない場合に警告が表示されます1。
fun main() {
val sealedClass: SealedClass = A
when (sealedClass) {
is A -> {
// do something here
}
is B -> {
// do something here
}
is C -> {
// do something here
}
}
}
なお、Kotlin 1.7以降は、exhaustiveでない when
がコンパイルエラーになることが予定されています。
また、Kotlinスタートブック(長澤(2016))などには、 sealed class
のサブクラスは sealed class
にネストされたサブクラスしか定義することができない、という記述がありました。下のサンプルコードのように、 sealed class
本体にサブクラスを定義するということです。
sealed class SealedClass {
object A : SealedClass()
object B : SealedClass()
object C : SealedClass()
}
この制限については、Kotlin 1.1 Betaにて変更が入り、必ずしもサブクラスをネストして定義する必要はなくなったようです(調べてくれた @tkhs0604
さん、ありがとうございました)。
サブクラスをネストして定義する場合は、通常、以下のサンプルコードのように、 Parent.Sub
というシンタックスでサブクラスを使います。
fun main() {
val sealedClass: SealedClass = SealedClass.A
when (sealedClass) {
is SealedClass.A -> {
// do something here
}
is SealedClass.B -> {
// do something here
}
is SealedClass.C -> {
// do something here
}
}
}
サブクラスをネストして定義した場合の方がタイプ数は多くなってしまうのですが、こちらの方がクラスの継承に規律ができて、自分は好みです。
sealed class
に親しむここからが本題です。Kotlinの sealed class
で遊びながら親しんでいきます。
まずは sealed class
のコンストラクタを作ってみます。サブクラスごとに特定の値を持つことができます。
sealed class SealedClass(val awesomeValue: Int)
object A : SealedClass(awesomeValue = 42)
object B : SealedClass(awesomeValue = 43)
object C : SealedClass(awesomeValue = 44)
val sealedClass: SealedClass = SealedClass.A
when (sealedClass) {
is A -> {
println("value is ${sealedClass.awesomeValue}") // 42
}
is B -> {
println("value is ${sealedClass.awesomeValue}") // 43
}
is C -> {
println("value is ${sealedClass.awesomeValue}") // 44
}
}
data class
を用いるここまでのサンプルコードでは、 sealed class
のサブクラスは、すべてシングルトンになっていました。お気付きのとおり、これらのサンプルコードは enum class
でもほぼ等価なコードを実装することができます。
Kotlinの sealed class
では、サブクラスにシングルトンでない通常のクラスを定義することもできるので、 enum class
と使い分けることが可能です。
以下のサンプルコードでは、 sealed class
のサブクラスとして、 data class
を定義しています。この場合は、サブクラスごとに異なるプロパティを持たせることができます。
sealed class SealedClass
data class A(val a: Int) : SealedClass()
data class B(val b: Int) : SealedClass()
val sealedClass: SealedClass = A(42)
when (sealedClass) {
is A -> {
println("value is ${sealedClass.a}")
}
is B -> {
println("value is ${sealedClass.b}")
}
}
class
を用いるdata class
でない通常の class
を sealed class
のサブクラスにすることも可能ですが、Android StudioやIntelliJ IDEAなどのIDEから、 equals
と hashCode
をoverrideしなさい、という警告が表示されるかと思います。個人的には、 sealed class
を使いたい場合のサブクラスは object
と data class
で事足りる場合が多い気がしています。
以下のoverrideは、IDEが自動生成したコードです。
class C : SealedClass() {
override fun equals(other: Any?): Boolean {
return this === other
}
override fun hashCode(): Int {
return System.identityHashCode(this)
}
}
abstract
なプロパティを定義するsealed class
に abstract
なプロパティを定義することで、サブクラスが特定のプロパティを持っていることを保証できます。以下のサンプルコードでは、 sealed class
のサブクラスが必ずawesomeValueというInt型のプロパティを持ちます。
sealed class SealedClass {
abstract val awesomeValue: Int
}
data class A(override val awesomeValue: Int) : SealedClass()
data class B(override val awesomeValue: Int) : SealedClass()
val sealedClass: SealedClass = A(42)
println("value is ${sealedClass.awesomeValue}")
sealed class
に拡張関数を定義することもできます。
fun SealedClass.awesomeHello() = println("hello awesome world!")
sealed class
(のサブクラス)のインスタンスに対して、拡張関数を呼び出します。
val sealedClass: SealedClass = A(42)
sealedClass.awesomeHello()
Kotlin 1.6から、exhaustiveでない when
をオプションでエラーにすることができます。Kotlin 1.7からは標準でエラーになります(参考文献2を参照)。 ↩︎