如何建模类型安全枚举类型?

Scala不像Java那样有类型安全的__abc。给定一组相关的常量,在Scala中如何最好地表示这些常量?

107596 次浏览

http://www.scala-lang.org/docu/files/api/scala/Enumeration.html

示例使用

  object Main extends App {


object WeekDay extends Enumeration {
type WeekDay = Value
val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
}
import WeekDay._


def isWorkingDay(d: WeekDay) = ! (d == Sat || d == Sun)


WeekDay.values filter isWorkingDay foreach println
}

我必须说,上面的例子从Scala文档中复制出来的 by skaffman在实践中用处有限(你也可以使用case objects)。

为了得到最接近Java Enum的东西(即使用合理的toStringvalueOf方法——也许你要将枚举值持久化到数据库中),你需要稍微修改它。如果你使用了skaffman的代码:

WeekDay.valueOf("Sun") //returns None
WeekDay.Tue.toString   //returns Weekday(2)

鉴于使用以下声明:

object WeekDay extends Enumeration {
type WeekDay = Value
val Mon = Value("Mon")
val Tue = Value("Tue")
... etc
}

你会得到更合理的结果:

WeekDay.valueOf("Sun") //returns Some(Sun)
WeekDay.Tue.toString   //returns Tue

声明命名枚举的一种稍微不那么冗长的方式:

object WeekDay extends Enumeration("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") {
type WeekDay = Value
val Sun, Mon, Tue, Wed, Thu, Fri, Sat = Value
}


WeekDay.valueOf("Wed") // returns Some(Wed)
WeekDay.Fri.toString   // returns Fri

当然,这里的问题是,您需要保持name和val的顺序同步,如果name和val声明在同一行上,这将更容易做到。

有很多方法可以做。

1)使用符号。但是,除了在需要符号的地方不接受非符号之外,它不会为您提供任何类型安全性。我在这里只是为了完整起见才提到它。下面是一个用法示例:

def update(what: Symbol, where: Int, newValue: Array[Int]): MatrixInt =
what match {
case 'row => replaceRow(where, newValue)
case 'col | 'column => replaceCol(where, newValue)
case _ => throw new IllegalArgumentException
}


// At REPL:
scala> val a = unitMatrixInt(3)
a: teste7.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 0 1 /


scala> a('row, 1) = a.row(0)
res41: teste7.MatrixInt =
/ 1 0 0 \
| 1 0 0 |
\ 0 0 1 /


scala> a('column, 2) = a.row(0)
res42: teste7.MatrixInt =
/ 1 0 1 \
| 0 1 0 |
\ 0 0 0 /

2)使用类Enumeration:

object Dimension extends Enumeration {
type Dimension = Value
val Row, Column = Value
}

或者,如果你需要序列化或显示它:

object Dimension extends Enumeration("Row", "Column") {
type Dimension = Value
val Row, Column = Value
}

可以这样使用:

def update(what: Dimension, where: Int, newValue: Array[Int]): MatrixInt =
what match {
case Row => replaceRow(where, newValue)
case Column => replaceCol(where, newValue)
}


// At REPL:
scala> a(Row, 2) = a.row(1)
<console>:13: error: not found: value Row
a(Row, 2) = a.row(1)
^


scala> a(Dimension.Row, 2) = a.row(1)
res1: teste.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 1 0 /


scala> import Dimension._
import Dimension._


scala> a(Row, 2) = a.row(1)
res2: teste.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 1 0 /

不幸的是,它不能确保所有匹配都被考虑在内。如果我忘记在匹配中放入Row或Column, Scala编译器不会警告我。所以它给了我一些类型安全性,但并没有得到那么多。

3)案例对象:

sealed abstract class Dimension
case object Row extends Dimension
case object Column extends Dimension

现在,如果我省略了match的大小写,编译器会警告我:

MatrixInt.scala:70: warning: match is not exhaustive!
missing combination         Column


what match {
^
one warning found

它的使用方式几乎相同,甚至不需要import:

scala> val a = unitMatrixInt(3)
a: teste3.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 0 1 /


scala> a(Row,2) = a.row(0)
res15: teste3.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 1 0 0 /

那么,您可能想知道为什么要使用Enumeration而不是case对象。事实上,case对象在很多时候都有优势,比如这里。Enumeration类有很多Collection方法,比如elements (Scala 2.8上的迭代器),它返回一个iterator、map、flatMap、filter等。

这个答案本质上是从我的博客中的这篇文章中选择的一部分。

你可以使用一个密封的抽象类来代替枚举,例如:

sealed abstract class Constraint(val name: String, val verifier: Int => Boolean)


case object NotTooBig extends Constraint("NotTooBig", (_ < 1000))
case object NonZero extends Constraint("NonZero", (_ != 0))
case class NotEquals(x: Int) extends Constraint("NotEquals " + x, (_ != x))


object Main {


def eval(ctrs: Seq[Constraint])(x: Int): Boolean =
(true /: ctrs){ case (accum, ctr) => accum && ctr.verifier(x) }


def main(args: Array[String]) {
val ctrs = NotTooBig :: NotEquals(5) :: Nil
val evaluate = eval(ctrs) _


println(evaluate(3000))
println(evaluate(3))
println(evaluate(5))
}


}

在Scala中,使用https://github.com/lloydmeta/enumeratum非常舒服

项目是非常好的例子和文档

他们文档中的这个例子应该会让你感兴趣

import enumeratum._


sealed trait Greeting extends EnumEntry


object Greeting extends Enum[Greeting] {


/*
`findValues` is a protected method that invokes a macro to find all `Greeting` object declarations inside an `Enum`


You use it to implement the `val values` member
*/
val values = findValues


case object Hello   extends Greeting
case object GoodBye extends Greeting
case object Hi      extends Greeting
case object Bye     extends Greeting


}


// Object Greeting has a `withName(name: String)` method
Greeting.withName("Hello")
// => res0: Greeting = Hello


Greeting.withName("Haro")
// => java.lang.IllegalArgumentException: Haro is not a member of Enum (Hello, GoodBye, Hi, Bye)


// A safer alternative would be to use `withNameOption(name: String)` method which returns an Option[Greeting]
Greeting.withNameOption("Hello")
// => res1: Option[Greeting] = Some(Hello)


Greeting.withNameOption("Haro")
// => res2: Option[Greeting] = None


// It is also possible to use strings case insensitively
Greeting.withNameInsensitive("HeLLo")
// => res3: Greeting = Hello


Greeting.withNameInsensitiveOption("HeLLo")
// => res4: Option[Greeting] = Some(Hello)


// Uppercase-only strings may also be used
Greeting.withNameUppercaseOnly("HELLO")
// => res5: Greeting = Hello


Greeting.withNameUppercaseOnlyOption("HeLLo")
// => res6: Option[Greeting] = None


// Similarly, lowercase-only strings may also be used
Greeting.withNameLowercaseOnly("hello")
// => res7: Greeting = Hello


Greeting.withNameLowercaseOnlyOption("hello")
// => res8: Option[Greeting] = Some(Hello)

从Scala 3开始,现在有enum关键字,它可以表示一组常量(和其他用例)

enum Color:
case Red, Green, Blue


scala> val red = Color.Red
val red: Color = Red
scala> red.ordinal
val res0: Int = 0