シールド(sealed)とは「封印された」や「閉ざされた」という意味の単語です。shield(盾)とは全く違う単語なので注意しましょう。ちなみに下の画像は「sealed letter」です。
Kotlinのシールドもまさにこの意味で使われていますが、「何を」「どこに」封印しているのでしょう? そしてその利点は何でしょうか。
今回はEnumクラスやwhenを使いながら、
- sealed classを使うと便利な場面
- sealed classの宣言方法
- sealed classの制限
といったことを中心にお話したいと思います。
列挙型(Enumクラス)の問題点
TV画面のON/OFFを表現する
sealed classを語る上でよく引き合いに出されるのがEnumクラスです。まずはEnumクラスと関数を使って、TV画面の状態と、それを切り替えるスイッチを表現してみましょう。
//画面の状態を表すEnum型
enum class ScreenStatus{
ON,
OFF
}
//電源のON/OFFを実装する関数
fun pushSwitch(status: ScreenStatus): ScreenStatus{
return when(status){
ScreenStatus.OFF->{
println("スイッチをONにします")
ScreenStatus.ON
}
ScreenStatus.ON->{
println("スイッチをOFFにします")
ScreenStatus.OFF
}
}
}
pushSwitch
は「ON」か「OFF」どちらかを引数として取り、引数が「OFF」ならスイッチをONに、「ON」ならOFFにして、それを返す関数です。
これでTV画面と、そのON/OFFを切り替えるスイッチが出来上がりました。
fun main() {
//画面の初期状態をOFFに設定
var tv = ScreenStatus.OFF
tv = pushSwitch(tv) //スイッチをONにします
println(tv) //ON
}
画面の状態はONかOFF、2つに1つです。そのため関数内のwhen式にはelseが不要であり、2つの分岐があれば事足ります。
チャンネルを実装するには?
さてここからが問題です。画面をONにした後、チャンネルを選択するにはどうすればいいでしょう?
「どのチャンネルが映っているか」という状態は列挙型では表現できません。なぜなら列挙型はあくまでシングルトンであり、「画面が点いていれば、それは何チャンネルか?」といったことを表現するには、複数のインスタンスが必要になるからです。
sealed classを定義する
定義構文
そこで登場するのがシールドクラスです。Enumクラスをシールドクラスに置き換えてみましょう。
//「sealed class」で始める
sealed class ScreenStatus{
//ONの場合は通常のクラス。channelプロパティはprivateに設定
class ON(private var channel: Int): ScreenStatus(){
//現在のチャンネルを表示するメソッド
override fun printChannel() = println("現在のチャンネルは${channel}です")
//チャンネルを変更するメソッド
override fun changeChannel(num: Int) {
channel = num
}
}
//OFFの場合はオブジェクト宣言によるシングルトン
object OFF: ScreenStatus(){
override fun printChannel(){}
override fun changeChannel(num: Int){}
}
//シールドクラスには抽象メンバを含めることができる
abstract fun printChannel()
abstract fun changeChannel(num: Int)
}
ScreenStatusクラスは2つのクラスをネストしています。1つはコンストラクタを持つ通常のクラス、もう1つはオブジェクト宣言によるシングルトンです。
そしてネストされた2つのクラスは、シールドクラスを継承しているという点にも注目です。Enumクラスと同じように、クラス内部で継承関係が発生していることが分かります。
関数のカスタマイズ
関数側もこの仕様に合わせ、電源をONにしたときにONのインスタンスを生成するように変更しましょう。
fun pushSwitch(status: ScreenStatus): ScreenStatus{
return when(status){
is ScreenStatus.OFF->{
println("スイッチをONにします")
ScreenStatus.ON(4) //コンストラクタを呼び出す
}
is ScreenStatus.ON->{
println("スイッチをOFFにします")
ScreenStatus.OFF
}
}
}
新しい関数のwhen式では「is」を使って、受け取ったScreenStatus型の中身が、どちらのクラスであるかを判定材料としています。
5行目にも注目しましょう。ONを返す場合にそのチャンネルを指定し、ONクラスのコンストラクタを呼び出してインスタンスを生成するというのが、大きな変更点。
つまり「スイッチがOFFであればONにして、4チャンネルを映し出す」のが新しい関数の処理です。
実行結果とポイント
この状態でコードを走らせましょう。
fun main() {
var tv: ScreenStatus = ScreenStatus.OFF
tv = pushSwitch(tv) //スイッチをONにします
tv.printChannel() //現在のチャンネルは4です
tv.changeChannel(8)
tv.printChannel() //現在のチャンネルは8です
tv = pushSwitch(tv) //スイッチをOFFにします
tv.printChannel() //
}
TV画面のON/OFFに加え、画面に映し出されている「チャンネルが何であるか」を表現できました。
- Enumのようなクラスを使うことで、when分岐にelseが必要無くなる
- 列挙型はシングルトンであるため、各インスタンスの状態を変化させることができない
- sealed classはクラスやオブジェクト宣言を内包できるEnumのようなクラスである
sealed classが制限するもの
ここまで見てみるとシールドクラスはただの拡張版Enumクラスのように見えますが、より本質に近い部分は「継承の制限」です。
シールドクラスを継承できるクラスは、シールドクラスと同じファイルで宣言されたクラスに限られます。
シールドクラスを別ファイルに移動する
main関数が含まれるファイルとは別に、新しく「Sub.kt」を作成し、そこにシールドクラスを移動させてみましょう。
//Sub.kt
sealed class ScreenStatus {
abstract fun printChannel()
abstract fun changeChannel(num: Int)
}
class ON(private var channel: Int): ScreenStatus(){...}
object OFF: ScreenStatus(){...}
シールドクラスはサブクラスをネストする必要は無く、こんな風に別々に定義することができます。
なおこの場合(当然ですが)、ONクラスとOFFクラスのインスタンス生成にScreenStatus.OFF
のようにスーパークラスを表記する必要はありません。
別ファイルからの継承は不可
では移動させたシールドクラスを、main関数側のファイルから継承してみるとどうなるでしょうか?
コンストラクタにアクセスできないため、このクラスは使用できません。シールドクラスは自分が置かれた「ファイル」に封をして、その外から継承されることを禁じています。
これによってシールドクラスは、他のファイルのどこからも継承されていないと保証されます。
どれだけプログラムが大規模になったとしても、シールドクラスであるScreenStatus型に由来するエラーがあったとすれば、確認スべき場所は限られます。シールドクラスの封によって、この型に関連する定義全ては1ファイルに収められているからです。