Scala 枚举介绍及深刻应用

本文详细地总结了Scala枚举的几种实现方式,对咱们更好地进行函数式编程有很好地指导和帮助。java

Scala 枚举示例和特性

枚举(Enumerations)是一种语言特性,对于建模有限的实体集来讲特别有用。一个经典的例子是将工做日建模为一个枚举:每一个七天都有一个值。Scala和许多其余语言同样,提供了一种表示枚举的方法:git

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

如今咱们能够准确清晰地表示工做日,而无需使用StringInt等基本类型。Scala枚举还提供了一组有用的特性:github

  • 序列化和反序列化方法(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的问题

然而,这种方法有一些问题。主要有两个缺点:ajax

  • 擦除(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中,咱们严重依赖于编译器强大的类型系统,使用这种方法,编译器不能找到非穷尽模式匹配子句,也不能对不一样的枚举使用重载方法。编程

为了不这种问题,咱们能够其余办法实现枚举:安全

  • 使用密封盒对象(sealed case objects)
  • 分项(itemized)
  • enumeratum

Sealed case objects

若是您决定使用sealed case objectsScala编译器能够解决Scala枚举中存在的两个问题。编译器既能够检测非穷尽模式匹配,也能够避免类型擦除问题。app

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 trait框架

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的问题

可是这种方式也有它本身的问题:ide

  • 没有检索全部枚举值的简单方法
  • 没有默认的序列化/反序列化方法
  • 枚举值之间没有默认的排序——这能够经过包含一些关于值的信息来手动实现,示例以下:
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库的集合。函数式编程

itemized为枚举提供了密封特质层次结构(sealed trait hierarchies)的宏和类型类,回到咱们以前的例子:

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

相关文章
相关标签/搜索