1. 序章

列挙型は、名前付き定数のグループを参照します。 Scalaは、列挙型を作成および取得するためのEnumerationと呼ばれる抽象クラスを提供します。 

このチュートリアルでは、列挙クラスを拡張してさらにカスタマイズする方法を見てみましょう。

2. 例

手の指を列挙する簡単な例を見てみましょう。

2.1. 列挙の作成

まず、指を表す新しい列挙を作成します。

object Fingers extends Enumeration {
  type Finger = Value

  val Thumb, Index, Middle, Ring, Little = Value
}

Enumerationクラスは、各列挙値を表すValueというタイプを提供します。 また、値を作成および初期化するための同じ名前の気の利いた保護されたメソッドを提供します。

スレッドセーフではないため、構築後に列挙型に値を追加しないことをお勧めします。

2.2. 値の取得

fingerが最短かどうかを確認する簡単な関数を書いてみましょう。

class FingersOperation {
  def isShortest(finger: Finger) = finger == Little
}

単体テストで同じことを確認します。

@Test
def givenAFinger_whenIsShortestCalled_thenCorrectValueReturned() = {
  val operation = new FingersOperation()

  assertTrue(operation.isShortest(Little))
  assertFalse(operation.isShortest(Index))
}

次に、最も長い2本の指を返す関数を作成してみましょう。

これを行うには、列挙値を反復処理する方法が必要です。 列挙クラスのValuesメソッドが役に立ちます。

def twoLongest() =
  Fingers.values.toList.filter(finger => finger == Middle || finger == Index)

Values メソッドは、順序付けられた列挙値のセットを返します。

twoLongest関数もテストしてみましょう。

@Test
def givenFingers_whenTwoLongestCalled_thenCorrectValuesReturned() = {
  val operation = new FingersOperation()

  assertEquals(List(Index, Middle), operation.twoLongest())
}

2.3. 識別子と名前を上書きする

各列挙値には、識別子と名前があります。 デフォルトでは、各値には0から始まる識別子と、値自体と同じ名前が割り当てられます

簡単なテストでデフォルトを検証できます。

@Test
def givenAFinger_whenIdAndtoStringCalled_thenCorrectValueReturned() = {
  assertEquals(0, Thumb.id)
  assertEquals("Little", Little.toString())
}

列挙クラスのValueメソッドを使用すると、デフォルトを変更できます。

val Thumb = Value(1, "Thumb Finger")
val Index = Value(2, "Pointing Finger")
val Middle = Value(3, "The Middle Finger")
val Ring = Value(4, "Finger With The Ring")
val Little = Value(5, "Shorty Finger")

テストを変更して、変更を確認しましょう。

assertEquals(1, Thumb.id)
assertEquals("Shorty Finger", Little.toString())

2.4. 列挙値の逆シリアル化

有効な名前から列挙値を取得する場合は、withNameメソッドが便利です

assertEquals(Middle, Fingers.withName("The Middle Finger"))

ただし、存在しない値を逆シリアル化しようとすると、java.util.NoSuchElementException。が発生します。

2.5. 順序の変更

EnumerationクラスのValuesメソッドは、識別子でソートされた値のセットを返します。 指の列挙のデフォルトの順序を確認しましょう。

assertEquals(List(Thumb, Index, Middle, Ring, Little), Fingers.values.toList)

次に、Thumbが列挙の最後に来るように順序を変更しましょう。 サムの識別子を最大にするだけです。

val Thumb = Value(6, "Thumb Finger")
// Other enumeration values defined previously

新しい順序を主張しましょう:

assertEquals(List(Index, Middle, Ring, Little, Thumb), Fingers.values.toList)

3. 列挙への属性の追加

isShortestおよびtwoLongest関数は、比較のために列挙のハードコードされた値を使用します。 代わりに、指の高さを使用した方がよいでしょう。 それでは、この新しい属性を Fingers enumerationにカプセル化してみましょう。

列挙クラスは、 Val、という名前の内部クラスを提供します。これを拡張して、属性を追加できます。

protected case class FingerDetails(i: Int, name: String, height: Double)
  extends super.Val(i, name) {
  def heightInCms(): Double = height * 2.54
}

heightおよびheightInCmsを使用するには、FingerDetailsクラスの暗黙的な型変換も提供する必要があります。

import scala.language.implicitConversions

implicit def valueToFingerDetails(x: Value): FingerDetails =
  x.asInstanceOf[FingerDetails]

これで、各指の高さを設定する準備が整いました。

val Thumb = FingerDetails(6, "Thumb Finger", 1)
val Index = FingerDetails(2, "Pointing Finger", 4)
val Middle = FingerDetails(3, "The Middle Finger", 4.1)
val Ring = FingerDetails(4, "Finger With The Ring", 3.2)
val Little = FingerDetails(5, "Shorty Finger", 0.5)

最後に、isShortest関数とtwoLongest関数を変更して、新しい属性を使用し、結果を計算してみましょう。

def isShortest(finger: Finger) =
  Fingers.values.toList.sortBy(_.height).head == finger

def twoLongest() =
  Fingers.values.toList.sortBy(_.heightInCms()).takeRight(2)

4. 問題と代替案

4.1. 列挙の問題

列挙クラスの使用にはいくつかの大きな問題があります。

まず、すべての列挙は消去後に同じタイプになります。 したがって、引数として異なる列挙を使用しても、メソッドをオーバーロードすることはできません

object Operation extends Enumeration { 
  type Operation = Value 
  val Plus, Minus = Value 
} 

object Conflicts { 

  // compile error 
  def getValue(f: Fingers.Finger) = f.toString 
  def getValue(o: Operation.Operation) = o.toString 
}

第二に、 Scalaコンパイラーは、大文字と小文字が一致するかどうかの徹底的なチェックを行いません。 たとえば、次の関数のコンパイルエラーは発生しません。

def checkIfIndex(finger: Finger) = {
  finger match {
    case Index => true
  }
}

上記の関数は、 Index を引数として渡す限り、正常に機能します。

val operation = new FingersOperation()

assertTrue(operation.checkIfIndex(Index))

ただし、大文字と小文字の一致でカバーされていない他の引数を送信すると、scala.MatchErrorが発生します。

4.2. 封印された特性との比較

列挙を作成するより良い代替手段は、ケース一致のコンパイル時の安全性を提供するため、(ケースオブジェクトとともに)シールド特性を使用することです。 ただし、問題には独自の手荷物があります。

  • 封印された特性は、すべての列挙値をリストするためのすぐに使えるソリューションを提供しません
  • 列挙名からケースオブジェクトを逆シリアル化する簡単な方法はありません
  • ケースオブジェクトには、識別子に基づくデフォルトの順序がありません。封印された特性に属性として識別子を手動で含め、順序付け機能を提供する必要があります

5. 結論

この記事では、列挙を作成、取得、およびカスタマイズする方法について説明しました。

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