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


311

Scala没有enumJava所具有的类型安全的。给定一组相关的常数,Scala中代表这些常数的最佳方法是什么?


2
为什么不只使用Java枚举?这是我仍然更喜欢使用纯Java的少数几件事之一。
Max

1
我已经撰写了有关scala枚举和替代方法的简短概述,您可能会发现它很有用:pedrorijo.com/blog/scala-enums/
pedrorijo91 2016年

Answers:


187

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
  }

2
认真地,不应使用应用程序。它没有固定;引入了一个新的App类,它没有Schildmeijer提到的问题。因此,“对象foo扩展App {...}”也是如此,您可以通过args变量立即访问命令行参数。
AmigoNico

scala.Enumeration(这是您在上面的“对象WeekDay”代码示例中使用的内容)不提供详尽的模式匹配。我研究了Scala当前使用的所有不同枚举模式,并在此StackOverflow答案中给出并概述了它们(包括一个同时兼具scala.Enumeration和“密封特征+案例对象”模式的新模式:stackoverflow。 com / a / 25923651/501113
chaotic3quilibrium 2014年

377

我必须说,上面的skaffman 从Scala文档中复制的示例在实践中用途有限(您最好使用s)。case object

为了得到一些东西最接近类似的一个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

7
顺便说一句。valueOf方法现在已死:-(
greenoldman

36
@macias valueOf的替换项是withName,它不返回Option,如果不匹配则抛出NSE。什么!
Bluu 2012年

6
@Bluu您可以自己添加valueOf:def valueOf(name:String)= WeekDay.values.find(_。toString == name)可以有一个选择
2014年

@centr当我尝试创建一个Map[Weekday.Weekday, Long]并添加一个值Mon时,编译器将引发无效的类型错误。预期工作日。工作日发现了价值?为什么会这样?
Sohaib 2015年

@Sohaib应该是Map [Weekday.Value,Long]。
2015年

98

有很多方法可以做。

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类具有许多Collection方法,例如元素(Scala 2.8上的迭代器),该方法返回Iterator,map,flatMap,filter等。

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


“ ...在需要符号的地方不接受非符号”>我猜您的意思是Symbol实例不能有空格或特殊字符。大多数人初次Symbol上课时可能会这样想,但实际上是不正确的。Symbol("foo !% bar -* baz")编译并运行得很好。换句话说,您可以完美地创建Symbol包装任何字符串的实例(您无法使用“单昏迷”语法糖来做到这一点)。唯一Symbol可以保证的是任何给定符号的唯一性,这使得比较和匹配的速度略快。
雷吉斯让吉尔斯

@RégisJean-Gilles不,我的意思是String,例如,您不能将a 作为参数传递给Symbol参数。
Daniel C. Sobral

是的,我理解了这一部分,但是如果您替换String为另一个类,这实际上是一个围绕字符串的包装器,并且可以在两个方向上自由转换(例如),这是一个很重要的问题Symbol。我想这就是您在说“它不会为您提供任何类型安全性”时的意思,鉴于OP明确要求提供类型安全解决方案,这并不是很清楚。我不知道,如果在写这篇文章时,你知道,它不仅是不是类型安全的,因为这些都不是枚举所有,但 Symbol说自己是即使保证传递的参数不会有特殊字符。
雷吉斯让吉尔斯

1
详细地说,当您说“不接受期望符号的非符号”时,可以理解为“不接受不是符号实例的值”(显然是真的)或“不接受不是符号实例的值”像标识符之类的简单字符串,又名“符号”(这是不正确的,这是一个误解,因为几乎没有人第一次遇到scala符号,原因是第一次遇到虽然是特殊的'foo符号,但确实会非标识符字符串)。这是我想消除的任何未来读者的误解。
雷吉斯让吉尔斯

@RégisJean-Gilles我的意思是前者,那显然是正确的。我的意思是,对于习惯于静态键入的任何人来说,这显然都是正确的。当时有很多的静态和“动态”类型的优缺点的讨论,很多有志于斯卡拉的人来自一个动态类型的背景,所以我认为它没有不言而喻。如今,我什至不愿发表这样的言论。就我个人而言,我认为Scala的Symbol是丑陋且多余的,并且从不使用它。我赞成你的最后评论,因为这很重要。
Daniel C. Sobral

52

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

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

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


11
乍一看看起来比较干净,但是有一个缺点,要求维护者保持两个列表的同步。对于星期几的示例,似乎不太可能。但是通常,可以插入一个新值,或者删除一个新值,并且两个列表可能不同步,在这种情况下,可能会引入一些细微的错误。
布伦特浮士德

1
根据先前的评论,风险在于两个不同的列表可能会默默地不同步。尽管对于您当前的小示例而言,这不是问题,但如果有更多的成员(例如数十到数百个成员),则两个列表静默不同步的几率会更高。同样,scala.Enumeration不能从Scala的编译时穷举模式匹配警告/错误中受益。我创建了一个StackOverflow答案,其中包含执行运行时检查以确保两个列表保持同步的解决方案:stackoverflow.com/a/25923651/501113
chaotic3quilibrium 2014年

17

您可以使用密封的抽象类而不是枚举,例如:

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))
  }

}

带有案例对象的密封特征也是可能的。
2013年

2
“密封特征+案例对象”模式存在一些问题,我将在StackOverflow答案中进行详细介绍。但是,我确实弄清楚了如何解决与该模式相关的所有问题,该模式也包含在线程中:stackoverflow.com/a/25923651/501113
chaotic3quilibrium 2014年


2

在对Scala中有关“枚举”的所有选项进行了广泛研究之后,我在另一个StackOverflow线程上发布了有关此域的更完整的概述。它包括针对“密封特征+案例对象”模式的解决方案,其中我已经解决了JVM类/对象初始化排序问题。



1

在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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.