1. 概要

Scalaのfinal修飾子は、クラスまたはトレイトを拡張機能で使用できないようにします。 したがって、それはかなり制限的です。 一方、クラスをパブリックにすると、他のクラスをそのクラスから拡張できます。

これら2つの間に何かが必要な場合はどうなりますか? Scalaのsealed修飾子が助けになります!

このチュートリアルでは、 Sealed キーワードがScalaで何であるか、その使用法、および適切に使用する方法を学習します。

2. 封印されたキーワードとは何ですか?

Sealed キーワードは、クラスと特性の拡張を制御するために使用されます。 クラスまたはトレイトを封印されたものとして宣言すると、そのサブクラスを定義できる場所が制限されます。同じソースファイルでそれらを定義する必要があります

次のセクションでは、さまざまなシナリオで Sealed を使用するときに発生するコンパイラの警告や例外など、Sealed修飾子の特性と動作を確認します。

3. シールの特徴

ソースファイルSealedClassExample.scalasealedを使用した複数選択オプションクラスの実装を見てみましょう。

sealed abstract class MultipleChoice

case class OptionA() extends MultipleChoice
case class OptionB() extends MultipleChoice
case class OptionC() extends MultipleChoice

封印されたトレイトを親クラスファイルの外部に拡張しようとすると、コンパイラは次のメッセージを含む例外をスローします。

illegal inheritance from sealed class.

SelaedExtendedDifferentFile.scala という名前の別のソースファイルを作成して、いくつかの実験を試してみましょう。

case class OptionD() extends MultipleChoice
Error:(5, 32) illegal inheritance from sealed class MultipleChoice
case class OptionD() extends MultipleChoice

ただし、封印されたクラスのサブクラスから拡張することに制限はありません。

case class OptionY() extends OptionX  //This is valid

このセカンダリサブクラスはパターンマッチングで使用でき、一致ケースに含めない場合、コンパイラは警告を表示しません。

case class OptionY() extends OptionX

def selectOption(option: MultipleChoice): String = option match {
  case optionY: OptionY => "Option-Y Selected"
  case optionX: OptionX => "Option-X Selected"
}

println(selectOption(OptionY()))

OptionYMultipleChoiceのサブクラスであり、別のソースファイルで定義した場合でも、printステートメントは例外なく実行されます。

Option-Y Selected

case-classを使用することで、この状況を防ぐことができます。これは、からの継承を許可しません。

Scalaでは、トレイト、クラス、抽象クラスにSealed修飾子を使用できます。 Sealed のコンテキストでは、クラス、トレイト、または抽象クラスは、クラスとトレイトの通常の違いを除いて同じように機能します。たとえば、抽象クラスはパラメーターを受け取ることができますが、トレイトは受け取ることができません。

4. 密閉型を使用する利点

Sealed クラスを使用すると、ファイルで定義されたサブクラスのみが存在することを保証できます。 これは、コンパイラが封印されたクラスのすべてのサブクラスを知るのに役立ちます。 したがって、この動作はパターンマッチングなどのシナリオで役立ちます。

MatchError 例外を防ぐために、一致ケースが完全でない場合、コンパイラーは警告を発行できます。 パターンマッチからOptionCを省略して、コンパイラの動作を観察してみましょう。

def selectOption(option: MultipleChoice): String = option match {
  case optionA: OptionA => "Option-A Selected"
  case optionB: OptionB => "Option-B Selected"
}

完全に一致しない場合があるため、コンパイラから適切な警告メッセージが表示されます。

Warning:(11, 54) match may not be exhaustive.
It would fail on the following input: OptionC()
  def selectOption(option: MultipleChoice): String = option match {

5. 列挙型の代替としてsealedを使用する

Scala の列挙には、列挙の動作を拡張できないなどの特定の欠点があります。 消去後の列挙は同じタイプになります。 コンパイル時に徹底的な一致チェックはありません。 密封されたケースオブジェクトのセットを使用して、この問題を克服できます。 Sealed を使用すると、シンプルに保つか、必要に応じて機能を追加する柔軟性が得られます。

シールドクラスを使用してDaysOfTheWeek列挙を実装する方法を見てみましょう。

sealed abstract class DayOfTheWeek(val name: String, val isWeekEnd: Boolean)

case object Monday extends DayOfTheWeek("Monday", false)
case object Tuesday extends DayOfTheWeek("Tuesday",  false)
case object Wednesday extends DayOfTheWeek("Wednesday", false)
case object Thursday extends DayOfTheWeek("Thursday", false)
case object Friday extends DayOfTheWeek("Friday", false)
case object Saturday extends DayOfTheWeek("Saturday", true)
case object Sunday extends DayOfTheWeek("Sunday", true)

ただし、Scalaの列挙クラスと比較した場合、このアプローチにはいくつかの欠点があることに注意してください。 たとえば、シリアル化/逆シリアル化、値の順序付け、またはすべての値のリストの取得に使用できるデフォルトのメソッドはありません。

したがって、ユースケースをより適切に評価し、状況に最も適したアプローチを選択する必要があります。

6. 代数的データ型(ADT)として封印

代数的データ型は、標準インターフェースを実装する可能な値の固定セットによって形成される一種の複合型です。 ADTの主な特徴は、違法な状態を存在させないようにする機能です。 ADTには、製品タイプと合計タイプの2つがあります。 合計タイプは、コンテナー内のタイプのすべての可能な値をリストします。

封印されたトレイトを使用して、合計タイプのADTとしてコーヒーのタイプを実装するコーヒーショップの例を見てみましょう。

sealed trait Coffee

case object Cappuccino extends Coffee
case object Americano extends Coffee
case object Espresso extends Coffee

7. 結論

このチュートリアルでは、SealedキーワードがScalaで何であるかを学びました。 パターンマッチングにおけるその特性と利点、および列挙型およびADTとしてのアプリケーションを見てきました。 また、Scalaアプリケーションでそれを適切に使用する場所と方法を学びました。

いつものように、例の完全なソースコードは、GitHubから入手できます。