有没有最佳实践指南,说明何时在Scala中使用case classes(或case objects)与扩展Enumeration?
它们似乎提供了一些相同的好处。
一个重要的区别是,Enumeration
支持从某个 name
字符串进行实例化。例如:
object Currency extends Enumeration {
val GBP = Value("GBP")
val EUR = Value("EUR") //etc.
}
那么你可以这样做:
val ccy = Currency.withName("EUR")
当希望将枚举(例如,存储到数据库中)持久化或从文件中的数据创建枚举时,这很有用。然而,我通常发现在Scala中枚举有些笨拙,并且感觉像是一个尴尬的附加组件,因此我现在倾向于使用case object
。 case object
比枚举更灵活:
sealed trait Currency { def name: String }
case object EUR extends Currency { val name = "EUR" } //etc.
case class UnknownCurrency(name: String) extends Currency
现在我有了一个优势...
trade.ccy match {
case EUR =>
case UnknownCurrency(code) =>
}
正如@chaotic3quilibrium所指出的(进行了一些更正以便更易读):
关于“UnknownCurrency(code)”模式,有其他处理未找到货币代码字符串的方法,而不是“破坏”类型为
Currency
的封闭集性质。将UnknownCurrency
作为Currency
类型可能会溜入API的其他部分。最好将该情况推送到
Enumeration
之外,并使客户端处理一个Option [Currency]
类型,清楚地指示确实存在匹配问题,并“鼓励”API用户自行解决。
继续阅读此处的其他答案,case object
相对于Enumeration
的主要缺点是:
无法迭代“枚举”的所有实例。这当然是事实,但在实践中我发现非常少需要这样做。
从持久化值轻松实例化。这也是正确的,但除了在枚举很大的情况下(例如,所有货币),这并不会产生巨大的开销。
trade.ccy
。 - rloth更新: 已经创建了一种新的基于宏的解决方案,远远优于我在下面概述的解决方案。我强烈建议使用这个新的基于宏的解决方案。而且看起来Dotty的计划将使这种枚举解决方案成为语言的一部分。哇哈哈!
摘要:
尝试在Scala项目中复制Java Enum
有三种基本模式。其中两种模式:直接使用Java Enum
和scala.Enumeration
,无法启用Scala的穷尽模式匹配。第三种模式:“密封的特质+case对象”,可以...但是会导致JVM类/对象初始化问题,从而导致不一致的序数索引生成。
case
objects
与扩展[scala.]Enumeration
”。事实证明,有许多可能的答案,每个答案都取决于具体项目要求的细微差别。答案可以归纳为三种基本模式。
首先,让我们确保我们从相同的基本概念开始工作。让我们主要通过Java 5(1.5)提供的Enum
来定义枚举:
Enum
,很好能够明确利用Scala的模式匹配穷尽性检查枚举 接下来,让我们看一下三种最常见的解决方案模式的简化版本:
A) 实际上直接使用 Java Enum
模式(在混合Scala/Java项目中):
public enum ChessPiece {
KING('K', 0)
, QUEEN('Q', 9)
, BISHOP('B', 3)
, KNIGHT('N', 3)
, ROOK('R', 5)
, PAWN('P', 1)
;
private char character;
private int pointValue;
private ChessPiece(char character, int pointValue) {
this.character = character;
this.pointValue = pointValue;
}
public int getCharacter() {
return character;
}
public int getPointValue() {
return pointValue;
}
}
sealed trait ChessPiece {def character: Char; def pointValue: Int}
object ChessPiece {
case object KING extends ChessPiece {val character = 'K'; val pointValue = 0}
case object QUEEN extends ChessPiece {val character = 'Q'; val pointValue = 9}
case object BISHOP extends ChessPiece {val character = 'B'; val pointValue = 3}
case object KNIGHT extends ChessPiece {val character = 'N'; val pointValue = 3}
case object ROOK extends ChessPiece {val character = 'R'; val pointValue = 5}
case object PAWN extends ChessPiece {val character = 'P'; val pointValue = 1}
}
object ChessPiece extends Enumeration {
val KING = ChessPieceVal('K', 0)
val QUEEN = ChessPieceVal('Q', 9)
val BISHOP = ChessPieceVal('B', 3)
val KNIGHT = ChessPieceVal('N', 3)
val ROOK = ChessPieceVal('R', 5)
val PAWN = ChessPieceVal('P', 1)
protected case class ChessPieceVal(character: Char, pointValue: Int) extends super.Val()
implicit def convert(value: Value) = value.asInstanceOf[ChessPieceVal]
}
Enum
还是scala.Enumeration
解决方案都无法足够扩展以提供第7项。对于我的项目来说,这是使用Scala中的封闭类型的更具吸引力的价值之一。我强烈倾向于在编译时获得警告/错误,以指示代码中存在的差距/问题,而不是在生产运行时异常/故障中推断出它。
在这方面,我开始与case object
路径一起工作,看是否能够产生一个解决方案,涵盖上述所有枚举定义。第一个挑战是推动JVM类/对象初始化问题的核心(在这个StackOverflow帖子中有详细介绍)。最终我终于找到了一个解决方案。
Enumeration
特性超过400行长(有很多注释解释上下文),我放弃将其粘贴到此线程中(这将使它在页面上拉伸)。有关详细信息,请直接跳转到Gist。EnumerationDecorated
中实现,解决方案最终看起来像什么。import scala.reflect.runtime.universe.{TypeTag,typeTag}
import org.public_domain.scala.utils.EnumerationDecorated
object ChessPiecesEnhancedDecorated extends EnumerationDecorated {
case object KING extends Member
case object QUEEN extends Member
case object BISHOP extends Member
case object KNIGHT extends Member
case object ROOK extends Member
case object PAWN extends Member
val decorationOrderedSet: List[Decoration] =
List(
Decoration(KING, 'K', 0)
, Decoration(QUEEN, 'Q', 9)
, Decoration(BISHOP, 'B', 3)
, Decoration(KNIGHT, 'N', 3)
, Decoration(ROOK, 'R', 5)
, Decoration(PAWN, 'P', 1)
)
final case class Decoration private[ChessPiecesEnhancedDecorated] (member: Member, char: Char, pointValue: Int) extends DecorationBase {
val description: String = member.name.toLowerCase.capitalize
}
override def typeTagMember: TypeTag[_] = typeTag[Member]
sealed trait Member extends MemberDecorated
}
decorationOrderedSet
)。虽然我将其最小化为单个重复,但由于两个问题,我无法看到如何使其更少:1. JVM对象/类初始化对于此特定对象/情况对象模型未定义(请参见此Stackoverflow线程)2.从方法getClass.getDeclaredClasses
返回的内容具有未定义的顺序(并且很不可能与源代码中的case object
声明相同)。List
,然后运行时检查它是否真正是一个集合。这不是我想要实现这个目标的方式。val
,例如上面的ChessPiecesEnhancedDecorated
示例,可以添加case object PAWN2 extends Member
,然后忘记将Decoration(PAWN2,'P2', 2)
添加到decorationOrderedSet
中。因此,需要运行时检查以验证列表不仅是一个集合,而且包含所有扩展了sealed trait Member
的案例对象。那是一种特殊的反射/宏地狱来处理。org.scalaolio.util.Enumeration
和org.scalaolio.util.EnumerationDecorated
版本:http://scalaolio.org/ - chaotic3quilibriumCase对象已经在其toString方法中返回其名称,因此单独传递它是不必要的。 这是一个类似于jho的版本(为简洁起见省略了便利方法):
trait Enum[A] {
trait Value { self: A => }
val values: List[A]
}
sealed trait Currency extends Currency.Value
object Currency extends Enum[Currency] {
case object EUR extends Currency
case object GBP extends Currency
val values = List(EUR, GBP)
}
对象是惰性的;而使用val则可以丢弃列表但必须重复名称:
trait Enum[A <: {def name: String}] {
trait Value { self: A =>
_values :+= this
}
private var _values = List.empty[A]
def values = _values
}
sealed abstract class Currency(val name: String) extends Currency.Value
object Currency extends Enum[Currency] {
val EUR = new Currency("EUR") {}
val GBP = new Currency("GBP") {}
}
如果你不介意有些作弊,可以使用反射 API 或类似 Google Reflections 的工具预加载枚举值。非延迟的 case object 给你最干净的语法:
trait Enum[A] {
trait Value { self: A =>
_values :+= this
}
private var _values = List.empty[A]
def values = _values
}
sealed trait Currency extends Currency.Value
object Currency extends Enum[Currency] {
case object EUR extends Currency
case object GBP extends Currency
}
干净利落,具有case类和Java枚举的所有优点。个人而言,我将枚举值定义在对象外部,以更好地匹配Scala习惯用法:
object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency
Currency.values
,我只会得到我之前访问过的值。有没有什么办法可以解决这个问题? - Sasgorilla更新:下面的代码存在一个错误,在这里有描述。下面的测试程序有效,但是如果在使用DayOfWeek本身之前使用DayOfWeek.Mon(例如),则会失败,因为DayOfWeek尚未初始化(内部对象的使用不会导致外部对象被初始化)。如果在您的主类中执行val enums = Seq(DayOfWeek)
之类的操作,则仍然可以使用此代码,以强制初始化您的枚举,或者您可以使用chaotic3quilibrium的修改。期待基于宏的枚举!
如果您想要:
那么以下内容可能会有所帮助。欢迎反馈。
在此实现中,有抽象的Enum和EnumVal基类,您可以扩展这些类。我们一会儿会看到这些类,但首先,这是您如何定义枚举:
object DayOfWeek extends Enum {
sealed abstract class Val extends EnumVal
case object Mon extends Val; Mon()
case object Tue extends Val; Tue()
case object Wed extends Val; Wed()
case object Thu extends Val; Thu()
case object Fri extends Val; Fri()
case object Sat extends Val; Sat()
case object Sun extends Val; Sun()
}
请注意,您必须使用每个枚举值(调用其apply方法)来使其生效。[我希望内部对象不会懒加载,除非我特别要求。这是我的想法。]
当然,如果需要,我们可以向DayOfWeek、Val或各个case对象添加方法/数据。
以下是如何使用此种枚举的示例:
object DayOfWeekTest extends App {
// To get a map from Int id to enum:
println( DayOfWeek.valuesById )
// To get a map from String name to enum:
println( DayOfWeek.valuesByName )
// To iterate through a list of the enum values in definition order,
// which can be made different from ID order, and get their IDs and names:
DayOfWeek.values foreach { v => println( v.id + " = " + v ) }
// To sort by ID or name:
println( DayOfWeek.values.sorted mkString ", " )
println( DayOfWeek.values.sortBy(_.toString) mkString ", " )
// To look up enum values by name:
println( DayOfWeek("Tue") ) // Some[DayOfWeek.Val]
println( DayOfWeek("Xyz") ) // None
// To look up enum values by id:
println( DayOfWeek(3) ) // Some[DayOfWeek.Val]
println( DayOfWeek(9) ) // None
import DayOfWeek._
// To compare enums as ordinals:
println( Tue < Fri )
// Warnings about non-exhaustive pattern matches:
def aufDeutsch( day: DayOfWeek.Val ) = day match {
case Mon => "Montag"
case Tue => "Dienstag"
case Wed => "Mittwoch"
case Thu => "Donnerstag"
case Fri => "Freitag"
// Commenting these out causes compiler warning: "match is not exhaustive!"
// case Sat => "Samstag"
// case Sun => "Sonntag"
}
}
编译后,您将获得以下结果:
DayOfWeekTest.scala:31: warning: match is not exhaustive!
missing combination Sat
missing combination Sun
def aufDeutsch( day: DayOfWeek.Val ) = day match {
^
one warning found
如果您不想要这样的警告,可以将"day match"替换为"( day: @unchecked ) match",或者在结尾处包含一个catch-all case。
运行上面的程序时,您会得到以下输出:
Map(0 -> Mon, 5 -> Sat, 1 -> Tue, 6 -> Sun, 2 -> Wed, 3 -> Thu, 4 -> Fri)
Map(Thu -> Thu, Sat -> Sat, Tue -> Tue, Sun -> Sun, Mon -> Mon, Wed -> Wed, Fri -> Fri)
0 = Mon
1 = Tue
2 = Wed
3 = Thu
4 = Fri
5 = Sat
6 = Sun
Mon, Tue, Wed, Thu, Fri, Sat, Sun
Fri, Mon, Sat, Sun, Thu, Tue, Wed
Some(Tue)
None
Some(Thu)
None
true
请注意,由于列表和映射是不可变的,因此您可以轻松地删除元素以创建子集,而不会破坏枚举本身。
这里是 Enum 类本身(及其中的 EnumVal):
abstract class Enum {
type Val <: EnumVal
protected var nextId: Int = 0
private var values_ = List[Val]()
private var valuesById_ = Map[Int ,Val]()
private var valuesByName_ = Map[String,Val]()
def values = values_
def valuesById = valuesById_
def valuesByName = valuesByName_
def apply( id : Int ) = valuesById .get(id ) // Some|None
def apply( name: String ) = valuesByName.get(name) // Some|None
// Base class for enum values; it registers the value with the Enum.
protected abstract class EnumVal extends Ordered[Val] {
val theVal = this.asInstanceOf[Val] // only extend EnumVal to Val
val id = nextId
def bumpId { nextId += 1 }
def compare( that:Val ) = this.id - that.id
def apply() {
if ( valuesById_.get(id) != None )
throw new Exception( "cannot init " + this + " enum value twice" )
bumpId
values_ ++= List(theVal)
valuesById_ += ( id -> theVal )
valuesByName_ += ( toString -> theVal )
}
}
}
以下是更高级的用法,可控制 ID 并向 Val 抽象和枚举本身添加数据/方法:
object DayOfWeek extends Enum {
sealed abstract class Val( val isWeekday:Boolean = true ) extends EnumVal {
def isWeekend = !isWeekday
val abbrev = toString take 3
}
case object Monday extends Val; Monday()
case object Tuesday extends Val; Tuesday()
case object Wednesday extends Val; Wednesday()
case object Thursday extends Val; Thursday()
case object Friday extends Val; Friday()
nextId = -2
case object Saturday extends Val(false); Saturday()
case object Sunday extends Val(false); Sunday()
val (weekDays,weekendDays) = values partition (_.isWeekday)
}
var
是一种近乎致命的罪过”,我认为这种观点并不被普遍接受。 - Erik Kaplun我这里有一个不错的简单库,它允许你使用密封的 traits/classes 作为枚举值,而无需维护自己的值列表。它依赖于一个简单的宏,不依赖于有错误的knownDirectSubclasses
方法。
更新于2017年3月:正如Anthony Accioly所评论的那样,scala.Enumeration/enum
PR已被关闭。
Dotty(Scala的下一代编译器)将会主导,尽管有dotty issue 1970和Martin Odersky's PR 1958存在。
注意:现在(2016年8月,6年后),有一个提议要移除scala.Enumeration
:PR 5352
弃用
scala.Enumeration
,添加@enum
注解语法为
@enum
class Toggle {
ON
OFF
}
这是一个可能的实现示例,意图是也支持符合某些限制条件(不允许嵌套、递归或变化的构造函数参数)的 ADT,例如:
@enum
sealed trait Toggle
case object ON extends Toggle
case object OFF extends Toggle
废弃了灾难性的
scala.Enumeration
。@enum相对于scala.Enumeration的优点:
- 实际可用
- Java互操作性
- 无消除问题
- 定义枚举时无需学习令人困惑的小型DSL
缺点:无。
这解决了一个问题,即不能有一个支持Scala-JVM、
Scala.js
和Scala-Native的代码库(Java源代码在Scala.js/Scala-Native
上不受支持,Scala源代码不能定义能被现有Scala-JVM API接受的枚举)。
与枚举相比,案例类的另一个劣势是在需要迭代或过滤所有实例时。这是枚举(以及Java枚举)的内置功能,而案例类不会自动支持此功能。
换句话说:“使用案例类没有简单的方法获取枚举值的完整列表。”
如果你认真考虑与其他JVM语言(如Java)保持互操作性,那么最好的选择是编写Java枚举。这些枚举可以在Scala和Java代码中透明地工作,而这不能说是scala.Enumeration
或case对象所能做到的。如果可以避免,就不要为每个新的GitHub业余项目创建一个新的枚举库!
我看到了很多让case class模拟枚举的版本。这是我的版本:
trait CaseEnumValue {
def name:String
}
trait CaseEnum {
type V <: CaseEnumValue
def values:List[V]
def unapply(name:String):Option[String] = {
if (values.exists(_.name == name)) Some(name) else None
}
def unapply(value:V):String = {
return value.name
}
def apply(name:String):Option[V] = {
values.find(_.name == name)
}
}
这使您能够构建类似以下的case类:
abstract class Currency(override name:String) extends CaseEnumValue {
}
object Currency extends CaseEnum {
type V = Site
case object EUR extends Currency("EUR")
case object GBP extends Currency("GBP")
var values = List(EUR, GBP)
}
enum
(截至2020年年中)链接。 - VonC