覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Kotlin - シールドクラス

明示していない場合も少なくないですが、現時点で私が書いているKotlinのエントリは、ほぼ 「Kotlinイン・アクション」 を読みながらの写経です。

Kotlinイン・アクション

Kotlinイン・アクション

  • 作者: Dmitry Jemerov,Svetlana Isakova,長澤太郎,藤原聖,山本純平,yy_yank
  • 出版社/メーカー: マイナビ出版
  • 発売日: 2017/10/31
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (2件) を見る


式を表現する Expr インターフェース とそれを実装した数字を表現する Num クラス, 値の和を表す Sum クラスを定義し、
when 式によって、サブクラスごとをの処理を分岐させて、計算結果を導き出す eval 関数を定義すると以下のようになります。

import java.lang.IllegalArgumentException

interface Expr

class Num(val value: Int) : Expr

class Sum(val left: Expr, val right: Expr) : Expr


fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        else -> 
            throw IllegalArgumentException("Unknown expression")
    }


実行するコードは以下のようになります。

fun main() {
    // 10 + 10
    val answer1 = eval(Sum(Num(10), Num(10)))
    println(answer1)  // -> 20

    // 1 + 20 + 30
    val answer2 = eval(Sum(Num(1), Sum(Num(20), Num(30))))
    println(answer2)  // -> 51
}


when で分岐にマッチしなかった場合の処理を指定するための else分岐 を用意する必要があります。
Kotlinのコンパイラでは、 when構文を使って式を評価するときに、デフォルトの選択肢のチェックが行われます。

上記の例では、Num と Sum 以外の場合意味のある値を返却する事ができないため、例外をスローしています。

常にデオフォルトの分岐が必要になるのは不便であり、かつ、新しい実装クラスを増やした場合にwhenの分岐にそれが加わっているかどうかはコンパイラはチェックしてくれません。

このような問題を解決する策として シールドクラス が存在しています。
スーパークラスに sealed修飾子を設定すると生成可能なサブクラスを制限する事ができます。

シールドクラスを継承したサブクラスとして宣言するには、シールドクラスでネストされたクラスか、同じファイル内で宣言されたクラスとしてのみです。

先ほどの例はシールドクラスを利用する事で以下のように記述できます

sealed class Expr

class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()


fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
    }

when 式内でシールドクラスの全てのサブクラスを処理するのであれば、デフォルトの分岐を追加する必要がありません。


シールドクラスを継承したサブクラスを追加した場合は、when 式の分岐の中にも追記して、全てのサブクラスを網羅する必要があります。
以下の例では、 Subtract というクラスを追加しています

sealed class Expr

class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
class Subtract(val left: Expr, val right: Expr) : Expr()  // 新しく増やしたサブクラス

fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        is Subtract -> eval(e.left) - eval(e.right)  // この行を追加しないとコンパイルエラーになる
    }


まとめ

簡単な例しか取り扱っていないため、シールドクラスの便利さを表現しきれていませんが、
今回のエントリでいうと when 式での分岐において余計なデフォルト処理の記述が不要となり、
かつ、シールドクラスの全てのサブクラスに対するケースの網羅をコンパイラが強制してくれる事がわかりました。