Kotlin sealed class パターンマッチの挙動

sealed class を when でパターンマッチした際に、想定外の挙動をしていたのでいろいろ検証してみました。

網羅チェック

網羅チェックが動きそうで動かなかったケース

  • ビルドエラーになりません。
sealed class Event {
    class Insert: Event()
    class Update: Event()
    class Delete: Event()
}

fun hoge(event: Event) {
    
    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Update -> { print("Update") }
//        is Event.Delete -> { print("Delete") }
    }
    
}

網羅チェックが動くパターン1

  • ビルドエラーになります。
sealed class Event {
    class Insert: Event()
    class Update: Event()
    class Delete: Event()
}

fun hoge(event: Event) {

    val x = when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Update -> { print("Update") }
//        is Event.Delete -> { print("Delete") }
    }

}

網羅チェックが動くパターン2

  • ビルドエラーになります。
sealed class Event {
    class Insert: Event()
    class Update: Event()
    class Delete: Event()
}

fun hoge(event: Event) {

    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Update -> { print("Update") }
//        is Event.Delete -> { print("Delete") }
    }.let {  }

}

参考にしたページ

ネストしたクラス

下記のようなページも有ったので検証してみました。

  • ビルドエラーになります。
sealed class Event {
    class Insert: Event() {
        class Single: Event()
        class Multiple: Event()
    }
    class Update: Event()
    class Delete: Event()
}

fun hoge(event: Event) {

    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Insert.Multiple -> { print("Multiple Insert") }
//        is Event.Insert.Single -> { print("Single Insert") }
        is Event.Update -> { print("Update") }
        is Event.Delete -> { print("Delete") }
    }.let {  }

}

複雑なクラス継承

継承した時の挙動が気になったので検証してみました。

sealed class の inner class を外で継承

  • ForceDeleteをwhenの条件に加えなくてもビルドエラーになりません。
  • ForceDelete の場合 Delete にマッチします。
sealed class Event {
    class Insert: Event()
    class Update: Event()
    open class Delete: Event() // 継承できるように open を指定する。
}

class ForceDelete: Event.Delete()

fun hoge(event: Event) {

    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Update -> { print("Update") }
        is Event.Delete -> { print("Delete") }
    }.let {  }

}
hoge(ForceDelete()) // Delete

sealed class の inner class を sealed class 内で継承

  • Deleteをwhenの条件に加えなくてもビルドエラーになりません。
  • hoge(ForceDelete()) では Delete が実行されます。
  • 1つ前の外で継承したケースと同じ挙動です。
sealed class Event {
    class Insert: Event()
    class Update: Event()
    open class Delete: Event()
    class ForceDelete: Event.Delete()
}


fun hoge(event: Event) {

    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Update -> { print("Update") }
        is Event.Delete -> { print("Delete") }
    }.let {  }

}
hoge(ForceDelete()) // Delete

sealed class の inner class を継承した場合の when の評価を検証

  • 次のコードでは Delete は Update にマッチします。
sealed class Event {
    class Insert: Event()
    open class Update: Event()
    class Delete: Event.Update()
}


fun hoge(event: Event) {

    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Update -> { print("Update") }
        is Event.Delete -> { print("Delete") }
    }.let {  }

}

hoge(Event.Delete()) // Update
  • 次のコードでは Delete は Delete にマッチします。
sealed class Event {
    class Insert: Event()
    open class Update: Event()
    class Delete: Event.Update()
}


fun hoge(event: Event) {

    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Delete -> { print("Delete") }
        is Event.Update -> { print("Update") }
    }.let {  }

}
    

hoge(Event.Delete()) // Delete

この通り、When-isの順番により処理が変わってしまうため、 sealed class の inner class を open にして継承することは避けるべきです。

最後に

いくつか微妙だなと感じる仕様があったので以下の仕様変更があればなと思います。

  • whenで評価した値を使わなくてもsealed classの網羅チェックが働くようにする。
  • sealed class の inner class は open にできないようにする。

こういうのどこでリクエストすればいいのでしょうか。