Kotlin1.5でsealed interfaceがstableになりました。

参考 : https://kotlinlang.org/docs/sealed-classes.html

参考 : https://blog.jetbrains.com/ja/kotlin/2021/05/kotlin-1-5-0-released/

今までsealed classを使っていたのですが、それがinterfaceとして使えるようになったイメージです。

sealed classを使う

例えば自分が所持しているPCを整理するプログラムを作るとします。

sealed class MyPC(val maker: String) {
    object Desktop : MyPC("dell")
    object Notebook : MyPC("apple")
}

fun main() {
    val myPc: MyPC = MyPC.Notebook
    
    when (myPc) {
        is MyPC.Desktop -> println("my desktop is made by ${myPc.maker}")
        is MyPC.Notebook -> println("my notebook is made by ${myPc.maker}")
    }
}

https://pl.kotl.in/13ZTfPWGt

ここまでは問題ないですが、将来「OSの情報も載せたいなぁ」とか「常設しているサーバとクライアントPCで分けたいなぁ」とか色々な仕様変更をしたくなります。

OSの情報を載せたい場合はMyPCクラスにval os: Stringを追加します。

sealed class MyPC(val maker: String, val os: String) {
    object Desktop : MyPC("dell", "windows")
    object Notebook : MyPC("apple", "mac")
}

fun main() {
    val myPc: MyPC = MyPC.Notebook
    
    when (myPc) {
        is MyPC.Desktop -> println("my desktop -> made by ${myPc.maker}, os : ${myPc.os}")
        is MyPC.Notebook -> println("my notebook -> made by ${myPc.maker}, os : ${myPc.os}")
    }
}

https://pl.kotl.in/6aogGrOFb

変更に耐えることができました。今度は「動かしっぱなしのサーバ」と「使うときだけ電源を入れるPC」毎に処理を分けたい場面が来ました。

sealed class MyPC(val maker: String, val os: String) {
    object Desktop : MyPC("dell", "windows")
    object Notebook : MyPC("apple", "mac")
}

sealed class MyServer(val maker: String, val os: String) {
    object Rack : MyServer("hp", "ubuntu")
    object Nas : MyServer("qnap", "qts")
}

fun main() {
    val myPc: MyPC = MyPC.Notebook
    
    when (myPc) {
        is MyPC.Desktop -> println("my desktop -> made by ${myPc.maker}, os : ${myPc.os}")
        is MyPC.Notebook -> println("my notebook -> made by ${myPc.maker}, os : ${myPc.os}")
    }
    
    val myServer: MyServer = MyServer.Rack
    
    when (myServer) {
        is MyServer.Rack -> println("my rack server -> made by ${myServer.maker}, os : ${myServer.os}")
        is MyServer.Nas -> println("my nas server -> made by ${myServer.maker}, os : ${myServer.os}")
    }
    
}

https://pl.kotl.in/SbkT1lw44

makerとosがデータとしては同じ意味のデータなので、同じ定義にしたいですが、sealed classのままだと定義を共通にできません。ここでsealed interfaceが出てきます。sealed interfaceでmakerとosの情報をまとめます。

sealed interfaceを使う

interface Maker { val maker: String }

interface Os { val os: String }

sealed interface MyPC : Maker, Os

sealed interface MyServer : Maker, Os

object Desktop : MyPC {
    override val maker = "dell"
    override val os = "windows"
}

object Notebook : MyPC {
    override val maker = "apple"
    override val os = "mack"
}

object Rack : MyServer {
    override val maker = "hp"
    override val os = "ubuntu"
}

object Nas : MyServer {
    override val maker = "qnap"
    override val os = "qts"
}

fun main() {
    val myPc: MyPC = Notebook
    
    when (myPc) {
        is Desktop -> println("my desktop -> made by ${myPc.maker}, os : ${myPc.os}")
        is Notebook -> println("my notebook -> made by ${myPc.maker}, os : ${myPc.os}")
    }
    
    val myServer: MyServer = Rack
    
    when (myServer) {
        is Rack -> println("my rack server -> made by ${myServer.maker}, os : ${myServer.os}")
        is Nas -> println("my nas server -> made by ${myServer.maker}, os : ${myServer.os}")
    }
}

https://pl.kotl.in/PFCWp97ZS

このようにするとOSとMakerという同じ表現をMyPCとMyServerで共通にできますし、他の表現を追加したくなった場合もinterfaceを定義し、継承することで可能です。

ちゃんとsealed classのメリットは生かされているので

when (myPc) {
    is Desktop -> println("my desktop -> made by ${myPc.maker}, os : ${myPc.os}")
    is Notebook -> println("my notebook -> made by ${myPc.maker}, os : ${myPc.os}")
    else -> Exception("error")
}

このようにコードを変更すると「’when’ is exhaustive so ‘else’ is redundant here」という警告がでるようになるので、else不要のメリットは生かされます。

但し型の定義には注意してください。

val myPc = Notebook
    
when (myPc) {
    is Desktop -> println("my desktop -> made by ${myPc.maker}, os : ${myPc.os}")
    is Notebook -> println("my notebook -> made by ${myPc.maker}, os : ${myPc.os}")
}

これはコンパイルが通らなくなります。val myPc = NotebookというコードによってmyPcはNotebook型になり、MyPC型ではなく、Desktop型を取り得ないためwhen(myPc) { }の記述でコンパイルエラーになります。必ずval myPc: MyPC = Notebookとしなければなりません。(interfaceの比較ではなくsealed classの比較にすること)