sealedに関する変更点
本連載は、Kotlinのバージョン1.1から2.0までのアップデート内容を、テーマごとにバージョン横断で紹介する連載です。今回と次回は、クラスやインターフェース、オブジェクトに関する変更点を紹介していきます。そのうち、最初に紹介するのは、sealedに関する変更点です。
sealedクラス定義
sealed(シールド)クラスとは、その継承関係をコントロールできる仕組みです。例えば、リスト1のようなコードです。
sealed class GinBaseCocktail { // (1) fun getBaseName(): String { // (2) return "ジン" } abstract fun getGinVolume(): Int // (3) }
sealedクラスは、リスト1の(1)のように、クラス宣言にsealedキーワードを記述するだけで、その他は通常のクラスと同じように(2)のようなメソッド定義やプロパティ定義が可能です。
ただし、sealedクラスとした時点で、抽象クラスとなります。そのため(3)のように抽象メソッドを定義でき、その際のクラス宣言にabstractは不要です。
sealedクラスの特徴
sealedクラスが抽象クラスということは、そのままではインスタンスは生成できず、継承して利用されることを前提としています。そこまではabstractなクラスと同じですが、sealedクラス固有の特徴は「直接のサブクラスが、コンパイル時に全て把握されている」点です。
例えば、リスト1のGinBaseCocktailを継承したサブクラスとして、WhiteLadyとMississippiMuleの2個が定義されているとします。すると、GinBaseCocktailのサブクラスはこの2個のみである、ということがコンパイル段階に確定し、保証されます。
まさに、「sealed=封印された」の意味の如く、無制限にサブクラスが作成されるのではなく、サブクラスが封印されているという状態を実現する仕組みといえます。
サブクラスが全て把握されているということは、例えば、リスト2のwhichCocktail()関数を定義できるようになります。
fun whichCocktail(cocktail: GinBaseCocktail) { when(cocktail) { is WhiteLady -> { println("ホワイトレディです。") } is MississippiMule -> { println("ミシシッピミュールです。") } } }
この関数では、引数で受け取ったGinBaseCocktailインスタンスが、どのサブクラスかに応じて分岐処理が定義されています。大きな特徴は、when分岐内にelseブロックが不要である点です。
これは、GinBaseCocktailがsealedクラスであり、そのサブクラスがWhiteLadyかMississippiMuleのどちらかであることが保証されているからこそ可能な仕組みです。
ただし、サブクラスを作成する際に注意点があります。それは、そのサブクラスをopen、すなわち、継承可能なクラスとして作成してしまうと、そこから間接的なサブクラス(孫クラスやひ孫クラスなど)が作成可能となり、それらは把握されないサブクラスとして存在することになります。これではsealedの意味がなくなるので、注意してください。
sealedクラスのサブクラス定義の制約がなくなってきている
また、さらなる制約として「sealedクラスとそのサブクラスは、同一パッケージ内に定義しなければならない」という条件があります。この点にも注意してください。
これまでの例で言えば、sealedクラスであるGinBaseCocktail、そのサブクラスであるWhiteLadyとMississippiMuleは同一パッケージで宣言する必要があります。
実は、Kotlinが正式リリースされた直後は、sealedクラスのサブクラスは、ネストクラスとして定義する必要がありました。それがバージョン1.1で制約が緩くなり、同一ファイルに定義する限りは、そのサブクラスをトップレベルクラスとして定義できるように変更されました。さらにバージョン1.5で、ファイルの分割が可能となりました。
そのため、GinBaseCocktail、WhiteLady、MississippiMuleの3クラスは、それぞれ別ファイルに定義でき、1クラス1ファイルとして管理できるようになっています。ただし先述の通り、パッケージは同一である、という制約は残っているので注意してください。
sealedインターフェースの導入
さらに同じく1.5で導入されたのが、sealedインターフェースです。これは、例えばリスト3のようなコードです。
sealed interface Cocktail { fun getName(): String }
仕組みはsealedクラスと同じで、sealedキーワードを利用してインターフェースを宣言します。
例えば、リスト1のGinBaseCocktailを、リスト4のように、このインターフェースを実装したクラスとしたとします。ただし、sealedクラス同様に、sealedインターフェースも同一パッケージで宣言する必要があります。
sealed class GinBaseCocktail : Cocktail { : }
そのサブクラスであるWhiteLadyは、リスト5のように、Cocktailインターフェースに定義されたgetName()とGinBaseCocktailクラスに定義された抽象メソッドであるgetGinVolume()の両方をオーバーライドする必要があります。MississippiMuleも同様です。
class WhiteLady : GinBaseCocktail() { override fun getName(): String { return "ホワイトレディ" } override fun getGinVolume(): Int { return 30 } }
さらに、図1のように、Cocktailインターフェースを頂点とする継承構造が、コンパイル段階で確定していることが保証されます。
