Scala 枚舉的使用和探索(譯)

Scala 枚舉示例和特性

枚舉(Enumerations)是一種語言特性,對於建模有限的實體集來講特別有用。一個經典的例子是將工做日建模爲一個枚舉:每一個七天都有一個值。Scala和許多其餘語言同樣,提供了一種表示枚舉的方法:java

object Weekday extends Enumeration {
  val Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday = Value
}

如今咱們能夠準確清晰地表示工做日,而無需使用StringInt等基本類型。Scala枚舉還提供了一組有用的特性:git

  • 序列化和反序列化方法(Serialize and Deserialize methods),這些方法也會拋出異常:(:
scala> Weekday.Monday.toString
res0: String = Monday

scala> Weekday.withName("Monday")
res1: Weekday.Value = Monday

scala> Weekday.withName("Mondai")
java.util.NoSuchElementException: No value found for 'Mondai'
  at scala.Enumeration.withName(Enumeration.scala:124)
  ... 32 elided
  • 提供可讀性(human-readable value)的值:
object Weekday extends Enumeration {
    val Monday = Value("Mo.")
    val Tuesday = Value("Tu.")
    val Wednesday = Value("We.")
    val Thursday = Value("Th.")
    val Friday = Value("Fr.")
    val Saturday = Value("Sa.")
    val Sunday = Value("Su.")
  }

scala> Weekday.Monday.toString
res0: String = Mo.
  • 列出全部可能的值:
scala> Weekday.values
res0: Weekday.ValueSet = Weekday.ValueSet(Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday)
  • 排序(Ordering)。默認狀況下,枚舉值是按照聲明的順序排序的,排序順序能夠經過覆蓋(overridden)原來枚舉值的方式改變:
object Weekday extends Enumeration {
  val Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday = Value
}

// 按照枚舉值聲明的順序排序
scala> Weekday.values.toList.sorted
res0: List[Weekday.Value] = List(Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday)
object Weekday extends Enumeration {
  val Monday = Value(1)
  val Tuesday = Value(2)
  val Wednesday = Value(3)
  val Thursday = Value(4)
  val Friday = Value(5)
  val Saturday = Value(6)
  val Sunday = Value(0)
}

// 按照枚舉對應的數字值排序
scala> Weekday.values.toList.sorted
res1: List[Weekday.Value] = List(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)

scala.Enumeration的問題

然而,這種方法有一些問題。主要有兩個缺點:github

  • 擦除(erasure)後枚舉具備相同的類型:
object Weekday extends Enumeration {
    val Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday = Value
}

object OtherEnum extends Enumeration {
	val A, B, C = Value
}

def test(enum: Weekday.Value) = {
    println(s"enum: $enum")
}

def test(enum: OtherEnum.Value) = {
    println(s"enum: $enum")
}

<console>:25: error: double definition:
def test(enum: Weekday.Value): Unit at line 21 and
def test(enum: OtherEnum.Value): Unit at line 25
have same type after erasure: (enum: Enumeration#Value)Unit
         def test(enum: OtherEnum.Value) = {
             ^
  • 在編譯期間沒有詳盡的匹配檢查(matching check)。下面的示例將在沒有任何警告的狀況下編譯,可是在對週一和週日之外的工做日匹配時會拋出scala.MatchError異常:
def nonExhaustive(weekday: Weekday.Value) {
  weekday match {
    case Monday => println("I hate Mondays")
    case Sunday => println("The weekend is already over? :( ")
  }
}

Scala中,咱們嚴重依賴於編譯器強大的類型系統,使用這種方法,編譯器不能找到非窮盡模式匹配子句,也不能對不一樣的枚舉使用重載方法。安全

爲了不這種問題,咱們能夠其餘辦法實現枚舉:app

  • 使用密封盒對象(sealed case objects)
  • 分項(itemized)
  • enumeratum

Sealed case objects

若是您決定使用sealed case objectsScala編譯器能夠解決Scala枚舉中存在的兩個問題。編譯器既能夠檢測非窮盡模式匹配,也能夠避免類型擦除問題。框架

sealed trait Weekday

case object Monday extends Weekday
case object Tuesday extends Weekday
case object Wednesday extends Weekday
case object Thursday extends Weekday
case object Friday extends Weekday
case object Saturday extends Weekday
case object Sunday extends Weekday

def test(weekday: Weekday) = {
    weekday match {
      case Monday => println("I hate Mondays")
      case Sunday => println("The weekend is already over? :( ")
    }
}

<console>:15: warning: match may not be exhaustive.
It would fail on the following inputs: Friday, Saturday, Thursday, Tuesday, Wednesday
           weekday match {
           ^
test: (weekday: Weekday)Unit

另外一個很是好的特性是,能夠在枚舉值中包含更多字段(Scala enumerations only provides an index and a name),僅僅使用sealed abstract class而不是sealed traitide

sealed abstract class Weekday( val name: String,
                               val abbreviation: String,
                               val isWorkDay: Boolean)

case object Monday extends Weekday("Monday", "Mo.", true)
case object Tuesday extends Weekday("Tuesday", "Tu.", true)
case object Wednesday extends Weekday("Wednesday", "We.", true)
case object Thursday extends Weekday("Thursday", "Th.", true)
case object Friday extends Weekday("Friday", "Fr.", true)
case object Saturday extends Weekday("Saturday", "Sa.", false)
case object Sunday extends Weekday("Sunday", "Su.", false)

sealed case objects的問題

可是這種方式也有它本身的問題:學習

  • 沒有檢索全部枚舉值的簡單方法
  • 沒有默認的序列化/反序列化方法
  • 枚舉值之間沒有默認的排序——這能夠經過包含一些關於值的信息來手動實現,示例以下:
sealed abstract class Weekday( val name: String,
                               val abbreviation: String,
                               val isWeekDay: Boolean,
                               val order: Int) extends Ordered[Weekday] {

  def compare(that: Weekday) = this.order - that.order
}

case object Monday extends Weekday("Monday", "Mo.", true, 2)
case object Tuesday extends Weekday("Tuesday", "Tu.", true, 3)
case object Wednesday extends Weekday("Wednesday", "We.", true, 4)
case object Thursday extends Weekday("Thursday", "Th.", true, 5)
case object Friday extends Weekday("Friday", "Fr.", true, 6)
case object Saturday extends Weekday("Saturday", "Sa.", false, 7)
case object Sunday extends Weekday("Sunday", "Su.", false, 1)

scala> Monday < Tuesday
res0: Boolean = true

分項(itemized)

itemized是一個OSS lib,它是rbrick的一部分,rbricks是一種可組合的、佔用空間小的Scala庫的集合。this

itemized爲枚舉提供了密封特質層次結構(sealed trait hierarchies)的宏和類型類,回到咱們以前的例子:scala

import io.rbricks.itemized.annotation.enum

@enum trait Weekday {
  object Monday
  object Tuesday
  object Wednesday
  object Thursday
  object Friday
  object Saturday
  object Sunday
}

除上面的之外,itemized還有其餘的一些特性:

  • 列出全部枚舉值
  • 默認的序列化/反序列化方法
scala> import io.rbricks.itemized.ItemizedCodec

scala> ItemizedCodec[Weekday].fromRep("Monday")
res0: Option[Weekday] = Some(Monday)

scala> val weekday: Weekday = Planet.Monday

scala> import io.rbricks.itemized.ItemizedCodec.ops._

scala> weekday.toRep
res1: String = Earth

itemized的問題

儘管itemized可讓咱們用註解方式建立類型安全的枚舉,可是它也有一些不足:

enumeratum

Enumeratum是一個類型安全且功能強大的Scala枚舉實現,它提供了詳盡的模式匹配警告。

import enumeratum._

sealed trait Weekday extends EnumEntry
object Weekday extends Enum[Weekday] {
  val values = findValues // mandatory due to Enum extension

  case object Monday extends Weekday
  case object Tuesday extends Weekday
  case object Wednesday extends Weekday
  case object Thursday extends Weekday
  case object Friday extends Weekday
  case object Saturday extends Weekday
  case object Sunday extends Weekday
}
def test(weekday: Weekday) = {
    weekday match {
      case Weekday.Monday => println("I hate Mondays")
      case Weekday.Sunday => println("The weekend is already over? :( ")
    }
  }

<console>:18: warning: match may not be exhaustive.
It would fail on the following inputs: Friday, Saturday, Thursday, Tuesday, Wednesday
           weekday match {
           ^
test: (weekday: Weekday)Unit

除了非詳盡的模式匹配警告,enumeratum還提供:

  • 列出可能的值(由於這些值須要在Enum繼承上實現)
  • 默認的序列化/反序列化方法(有和沒有異常拋出)
scala> Weekday.withName("Monday")
res0: Weekday = Monday

scala> Weekday.withName("Momday")
java.util.NoSuchElementException: Momday is not a member of Enum (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday)
  at enumeratum.Enum$$anonfun$withName$1.apply(Enum.scala:82)
  at enumeratum.Enum$$anonfun$withName$1.apply(Enum.scala:82)
  at scala.Option.getOrElse(Option.scala:121)
  at enumeratum.Enum$class.withName(Enum.scala:81)
  at Weekday$.withName(<console>:13)
  ... 43 elided

scala> Weekday.withNameOption("Monday")
res2: Option[Weekday] = Some(Monday)

scala> Weekday.withNameOption("Momday")
res3: Option[Weekday] = None
  • 向枚舉添加額外的值。它很是相似於咱們給簡單的密封盒對象添加額外的值
sealed abstract class Weekday( val name: String,
                               val abbreviation: String,
                               val isWorkDay: Boolean) extends EnumEntry

case object Weekday extends Enum[Weekday] {
  val values = findValues
  case object Monday extends Weekday("Monday", "Mo.", true)
  case object Tuesday extends Weekday("Tuesday", "Tu.", true)
  case object Wednesday extends Weekday("Wednesday", "We.", true)
  case object Thursday extends Weekday("Thursday", "Th.", true)
  case object Friday extends Weekday("Friday", "Fr.", true)
  case object Saturday extends Weekday("Saturday", "Sa.", false)
  case object Sunday extends Weekday("Sunday", "Su.", false)
}
  • 排序能夠經過與封閉層次(sealed hierarchies)結構相同的方式實現。只需與有序[]特質(trait)混合,並實現比較方法。
sealed abstract class Weekday(val order: Int) extends EnumEntry with Ordered[Weekday] {
   def compare(that: Weekday) = this.order - that.order
 }

 object Weekday extends Enum[Weekday] {
   val values = findValues

   case object Monday extends Weekday(2)
   case object Tuesday extends Weekday(3)
   case object Wednesday extends Weekday(4)
   case object Thursday extends Weekday(5)
   case object Friday extends Weekday(6)
   case object Saturday extends Weekday(7)
   case object Sunday extends Weekday(1)
 }

總結

若是您剛剛開始學習Scala,我建議使用scala.Enumeration的方式實現枚舉。當您以爲使用更多Scala特性更舒服時,以及開始享受編譯器安全性時,能夠試試其餘方式實現枚舉。個人兩個建議是:

  • 若是您不想依賴於外部庫,就使用sealed hierarchies
  • 使用enumeratum,由於它提供了這裏提到的全部特性

枚舉特性總結

  • 詳盡的模式匹配
  • 沒有類型擦除
  • 安全的序列化/反序列化的默認方法
  • 列出全部可能的值
  • 在枚舉值上添加額外的字段
  • 排序

若是您想看到更多的替代方法,請查看Scala枚舉的後續內容—Scala Enumerations - Return of the (Java) Jedi

編譯自:Scala Enumerations

相關文章
相關標籤/搜索