是否有关于何时使用case classes(或案例对象)与在Scala中扩展枚举的最佳做法指南?
他们似乎提供了一些相同的好处。
答案 0 :(得分:215)
一个很大的区别是Enumeration
支持从某些name
字符串中实例化它们。例如:
object Currency extends Enumeration {
val GBP = Value("GBP")
val EUR = Value("EUR") //etc.
}
然后你可以这样做:
val ccy = Currency.withName("EUR")
当希望持久化枚举(例如,到数据库)或从驻留在文件中的数据创建枚举时,这非常有用。但是,我发现一般来说,Scala中的枚举有点笨拙并且有一种笨拙的附加组件的感觉,所以我现在倾向于使用case object
s。 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
的主要缺点是:
无法遍历“枚举”的所有实例。情况确实如此,但我发现在实践中极少需要这样做。
无法轻易地从持久值中实例化。这也是事实,但除了巨大的枚举(例如,所有货币)之外,这不会产生巨大的开销。
答案 1 :(得分:66)
<强>更新强> 创建了一个新的macro based solution,远远优于我在下面概述的解决方案。我强烈建议您使用这个新的macro based solution。 And it appears plans for Dotty will make this style of enum solution part of the language. Whoohoo!
<强>要点:强>
尝试在Scala项目中重现Java Enum
有三种基本模式。三种模式中的两种;直接使用Java Enum
和scala.Enumeration
,无法启用Scala的详尽模式匹配。第三个; &#34;密封特征+案例对象&#34;,确实......但JVM class/object initialization complications导致序数索引生成不一致。
我创建了一个包含两个类的解决方案;位于此Enumeration的EnumerationDecorated和Gist。我没有将代码发布到此线程中,因为Enumeration的文件非常大(+400行 - 包含许多解释实现上下文的注释)。
的详细说明:强>
你问的问题很普遍; &#34; ...何时使用case
类 objects
与扩展[scala.]Enumeration
&#34;。事实证明,有许多可能的答案,每个答案取决于您具体项目要求的细微之处。答案可以减少到三种基本模式。
首先,让我们确保我们的工作方式与枚举的基本概念相同。我们主要根据Enum
provided as of Java 5 (1.5):
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;
}
}
枚举定义中的以下项目不可用:
对于我目前的项目,我不具备利用Scala / Java混合项目途径承担风险的好处。即使我可以选择进行混合项目,如果/当我添加/删除枚举成员,或者正在编写一些新代码来处理现有的枚举成员时,第7项对于允许我捕获编译时问题至关重要。 />
B)使用&#34; sealed trait
+ case objects
&#34;图案:
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}
}
枚举定义中的以下项目不可用:
它有争议它确实符合枚举定义项目5和6.对于5,声称它有效率是一个延伸。对于6,要扩展以容纳其他相关的单一数据并不容易
C)使用 scala.Enumeration
模式(受this StackOverflow answer启发):
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]
}
枚举定义中的以下项目不可用(恰好与直接使用Java Enum的列表相同):
同样对于我当前的项目,如果/当我添加/删除枚举成员,或者正在编写一些新代码来处理现有的枚举成员时,第7项对于允许我捕获编译时问题至关重要。
因此,鉴于上面的枚举定义,上述三种解决方案都不起作用,因为它们没有提供上面枚举定义中列出的所有内容:
这些解决方案中的每一个都可以最终重新设计/扩展/重构,以尝试覆盖每个缺失的要求中的一些。但是,Java Enum
和scala.Enumeration
解决方案都不能充分扩展以提供第7项。对于我自己的项目,这是在Scala中使用封闭类型的更引人注目的价值之一。我非常喜欢编译时警告/错误,以表明我的代码中存在间隙/问题,而不是必须从生产运行时异常/故障中收集它。
在这方面,我开始着手处理case object
路径,看看我是否可以制定涵盖上述所有枚举定义的解决方案。第一个挑战是推动JVM类/对象初始化问题的核心(在this StackOverflow post中详细介绍)。我终于找到了解决方案。
因为我的解决方案有两个特点; Enumeration和EnumerationDecorated,由于Enumeration
特征超过+400行(许多评论解释上下文),我正在将其粘贴到此帖子中(这将使其延伸有意思的页面)。有关详情,请直接跳至Gist。
这里的解决方案最终看起来像使用与上面相同的数据构思(完全注释版本available here)并在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
}
这是我创建的一对新的枚举特征(位于this Gist)的示例用法,用于实现枚举定义中所需和概述的所有功能。
表达的一个担忧是必须重复枚举成员名称(在上面的示例中为decorationOrderedSet
)。虽然我把它最小化到一次重复,但由于两个问题,我无法看到如何使它更少:
getClass.getDeclaredClasses
返回的内容具有未定义的顺序(并且它与源代码中的case object
声明的顺序不太可能相同)鉴于这两个问题,我不得不放弃尝试生成隐含的排序,并且必须明确要求客户端定义并使用某种有序集合概念声明它。由于Scala集合没有插入有序集实现,我能做的最好是使用List
,然后运行时检查它是否真的是一个集合。这不是我希望如何实现这一目标。
鉴于设计需要第二个列表/集合排序val
,在上面的ChessPiecesEnhancedDecorated
示例中,可以添加case object PAWN2 extends Member
,然后忘记添加Decoration(PAWN2,'P2', 2)
到decorationOrderedSet
。因此,有一个运行时检查来验证列表不仅是一个集合,还包含扩展sealed trait Member
的所有案例对象。这是一种特殊的反思/宏观地狱形式
请在Gist上留下评论和/或反馈。
答案 2 :(得分:62)
Case对象已经为其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)
}
物品是懒惰的;通过使用vals我们可以删除列表但必须重复名称:
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等预先加载枚举值。非惰性案例对象为您提供最清晰的语法:
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
}
很好,干净,具有案例类和Java枚举的所有优点。就个人而言,我在对象之外定义枚举值以更好地匹配惯用的Scala代码:
object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency
答案 3 :(得分:26)
使用案例类优于枚举的优点是:
使用枚举而不是案例类的优点是:
因此,通常,如果您只需要按名称列出简单常量,请使用枚举。否则,如果您需要更复杂的东西,或者希望编译器的额外安全性告诉您是否指定了所有匹配项,请使用案例类。
答案 4 :(得分:15)
更新:下面的代码有一个错误,描述为here。下面的测试程序可以工作,但如果您在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或单个案例对象。
以下是你如何使用这样的枚举:
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)匹配”,在这里你不需要这样的警告,或者只是在最后包含一个包罗万象的案例。
运行上述程序时,会得到以下输出:
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
请注意,由于List和Maps是不可变的,因此您可以轻松删除元素以创建子集,而不会破坏枚举本身。
这是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)
}
答案 5 :(得分:11)
我在这里有一个很好的简单库,允许你使用密封的traits / classes作为枚举值,而不必维护自己的值列表。它依赖于一个不依赖于bug knownDirectSubclasses
的简单宏。
答案 6 :(得分:10)
2017年3月更新:正如Anthony Accioly所述,scala.Enumeration/enum
公关已经关闭。
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(没有嵌套,递归或变化的构造函数参数),例如: G:
@enum
sealed trait Toggle
case object ON extends Toggle
case object OFF extends Toggle
弃用
scala.Enumeration
的未减轻灾难。@enum优于scala.Enumeration:
- 实际工作
- Java interop
- 没有删除问题
- 定义枚举时不要混淆迷你DSL,
缺点:无。
这解决了无法拥有一个代码库的问题 支持Scala-JVM,
Scala.js
和Scala-Native(Scala.js/Scala-Native
不支持Java源代码,Scala源代码无法定义Scala-JVM上现有API接受的枚举)。
答案 7 :(得分:8)
当您需要在所有实例中进行迭代或过滤时,案例类与枚举的另一个缺点。这是Enumeration(以及Java枚举)的内置功能,而案例类不会自动支持此类功能。
换句话说:“没有简单的方法来获取带有案例类的枚举值的总列表”。
答案 8 :(得分:5)
如果您认真考虑维护与其他JVM语言(例如Java)的互操作性,那么最好的选择是编写Java枚举。这些工作透明地来自Scala和Java代码,这可以说是scala.Enumeration
或案例对象。如果可以避免的话,我们就没有为GitHub上的每个新爱好项目都有一个新的枚举库!
答案 9 :(得分:4)
我见过各种版本的案例类模仿枚举。这是我的版本:
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)
}
}
允许您构建如下所示的案例类:
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)
}
也许有人可以提出一个更好的技巧,而不是像我一样简单地将每个案例类添加到列表中。这就是我当时想出来的全部内容。
答案 10 :(得分:2)
在过去几次我需要的时候,我一直在这两个选项上来回走动。直到最近,我的偏好是密封的特征/案例对象选项。
1)Scala枚举声明
object OutboundMarketMakerEntryPointType extends Enumeration {
type OutboundMarketMakerEntryPointType = Value
val Alpha, Beta = Value
}
2)密封特征+案例对象
sealed trait OutboundMarketMakerEntryPointType
case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType
case object BetaEntryPoint extends OutboundMarketMakerEntryPointType
虽然这些都不能满足java枚举给你的所有内容,但下面是优点和缺点:
Scala Enumeration
优点: - 用于实例化选项或直接假设准确的功能(从持久存储加载时更容易) - 支持对所有可能值的迭代
缺点: - 不支持非详尽搜索的编译警告(使模式匹配不太理想)
案例对象/密封特征
优点: - 使用密封的特征,我们可以预先实例化一些值,而其他值可以在创建时注入 -full支持模式匹配(定义了apply / unapply方法)
缺点: - 从持久性商店中实例化 - 您通常必须在此处使用模式匹配或定义您自己的所有可能“枚举值”列表
最终让我改变观点的是以下片段:
object DbInstrumentQueries {
def instrumentExtractor(tableAlias: String = "s")(rs: ResultSet): Instrument = {
val symbol = rs.getString(tableAlias + ".name")
val quoteCurrency = rs.getString(tableAlias + ".quote_currency")
val fixRepresentation = rs.getString(tableAlias + ".fix_representation")
val pointsValue = rs.getInt(tableAlias + ".points_value")
val instrumentType = InstrumentType.fromString(rs.getString(tableAlias +".instrument_type"))
val productType = ProductType.fromString(rs.getString(tableAlias + ".product_type"))
Instrument(symbol, fixRepresentation, quoteCurrency, pointsValue, instrumentType, productType)
}
}
object InstrumentType {
def fromString(instrumentType: String): InstrumentType = Seq(CurrencyPair, Metal, CFD)
.find(_.toString == instrumentType).get
}
object ProductType {
def fromString(productType: String): ProductType = Seq(Commodity, Currency, Index)
.find(_.toString == productType).get
}
.get
调用是可怕的 - 使用枚举而不是我可以简单地在枚举上调用withName方法,如下所示:
object DbInstrumentQueries {
def instrumentExtractor(tableAlias: String = "s")(rs: ResultSet): Instrument = {
val symbol = rs.getString(tableAlias + ".name")
val quoteCurrency = rs.getString(tableAlias + ".quote_currency")
val fixRepresentation = rs.getString(tableAlias + ".fix_representation")
val pointsValue = rs.getInt(tableAlias + ".points_value")
val instrumentType = InstrumentType.withNameString(rs.getString(tableAlias + ".instrument_type"))
val productType = ProductType.withName(rs.getString(tableAlias + ".product_type"))
Instrument(symbol, fixRepresentation, quoteCurrency, pointsValue, instrumentType, productType)
}
}
所以我认为我的偏好是在从存储库访问值时使用Enumerations,否则使用case对象/密封traits。
答案 11 :(得分:2)
我更喜欢case objects
(这是个人偏好的问题)。为了解决该方法固有的问题(解析字符串并迭代所有元素),我添加了一些不完美但有效的行。
我在这里粘贴代码,希望它可能有用,而且其他人可以改进它。
/**
* Enum for Genre. It contains the type, objects, elements set and parse method.
*
* This approach supports:
*
* - Pattern matching
* - Parse from name
* - Get all elements
*/
object Genre {
sealed trait Genre
case object MALE extends Genre
case object FEMALE extends Genre
val elements = Set (MALE, FEMALE) // You have to take care this set matches all objects
def apply (code: String) =
if (MALE.toString == code) MALE
else if (FEMALE.toString == code) FEMALE
else throw new IllegalArgumentException
}
/**
* Enum usage (and tests).
*/
object GenreTest extends App {
import Genre._
val m1 = MALE
val m2 = Genre ("MALE")
assert (m1 == m2)
assert (m1.toString == "MALE")
val f1 = FEMALE
val f2 = Genre ("FEMALE")
assert (f1 == f2)
assert (f1.toString == "FEMALE")
try {
Genre (null)
assert (false)
}
catch {
case e: IllegalArgumentException => assert (true)
}
try {
Genre ("male")
assert (false)
}
catch {
case e: IllegalArgumentException => assert (true)
}
Genre.elements.foreach { println }
}
答案 12 :(得分:0)
对于那些仍在寻找如何获得GatesDa's answer to work的人: 您可以在声明它实例化之后引用案例对象:
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;
EUR //THIS IS ONLY CHANGE
case object GBP extends Currency; GBP //Inline looks better
}
答案 13 :(得分:0)
我认为拥有case classes
胜过enumerations
的最大优势在于,您可以使用 type class pattern ,也称为即席多态性 。不需要匹配以下枚举:
someEnum match {
ENUMA => makeThis()
ENUMB => makeThat()
}
相反,您会得到类似的东西:
def someCode[SomeCaseClass](implicit val maker: Maker[SomeCaseClass]){
maker.make()
}
implicit val makerA = new Maker[CaseClassA]{
def make() = ...
}
implicit val makerB = new Maker[CaseClassB]{
def make() = ...
}