是否有关于何时使用案例类(或案例对象)与在Scala中扩展枚举的最佳实践指南?
他们似乎提供了一些相同的好处。
enum
(适用于2020年中)。
是否有关于何时使用案例类(或案例对象)与在Scala中扩展枚举的最佳实践指南?
他们似乎提供了一些相同的好处。
enum
(适用于2020年中)。
Answers:
最大的不同是Enumeration
s支持从某些name
String 实例化它们。例如:
object Currency extends Enumeration {
val GBP = Value("GBP")
val EUR = Value("EUR") //etc.
}
然后,您可以执行以下操作:
val ccy = Currency.withName("EUR")
当希望保留枚举(例如,到数据库)或从文件中的数据创建枚举时,此功能很有用。但是,我发现总体上来说,枚举在Scala中有点笨拙,并且具有笨拙的附加组件的感觉,因此我现在倾向于使用case object
s。A 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
s over 的主要缺点Enumeration
s是:
无法遍历“枚举”的所有实例。确实是这种情况,但是在实践中我发现极少数情况是必须这样做的。
无法从持久值中轻松实例化。这也是正确的,但是,除非有大量的枚举(例如,所有货币),否则这不会带来巨大的开销。
trade.ccy
密封特质示例中要匹配的项目的类型。
case
object
产生比(?4x)大的代码足迹Enumeration
吗?有用的区别,特别是对于scala.js
需要占用空间小的项目。
更新:已经创建了 一个新的基于宏的解决方案,它远远优于我在下面概述的解决方案。我强烈建议使用这种基于宏的新解决方案。看来Dotty的计划将使这种样式的枚举解决方案成为该语言的一部分。hoo!
简介:
有三种用于Enum
在Scala项目中尝试重现Java的基本模式。三种模式中的两种;直接使用Java Enum
和scala.Enumeration
不能启用Scala的详尽模式匹配。还有第三个 “密封特征+案例对象”确实可以,但是具有JVM类/对象初始化的复杂性,导致序数索引生成不一致。
我创建了一个包含两个类的解决方案;Enumeration和EnumerationDecorated,位于此要点。我没有将代码发布到此线程中,因为用于枚举的文件非常大(+400行-包含许多解释实现上下文的注释)。
详细信息:
您要问的问题很笼统。“ ...何时使用case
类objects
与扩展[scala.]Enumeration
”。事实证明,这里有许多可能的答案,每个答案取决于您对特定项目要求的微妙之处。答案可以简化为三种基本模式。
首先,请确保我们使用的是枚举的相同基本思想。让我们主要从Enum
Java 5(1.5)开始定义一个枚举:
Enum
,能够明确利用Scala的模式匹配穷举性检查进行枚举会很好。 接下来,让我们看一下三个最常见的解决方案模式的简化版本:
A)实际上直接使用JavaEnum
模式(在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)使用“ sealed trait
+case objects
”模式:
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
模式(受此StackOverflow答案的启发):
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类/对象初始化问题的核心(在此StackOverflow帖子中有详细介绍)。我终于找到了解决方案。
我的解决方案有两个特点:Enumeration和EnumerationDecorated,并且由于Enumeration
特征超过+400行长(很多注释解释了上下文),我放弃将其粘贴到此线程中(这会使它充分扩展页面)。有关详细信息,请直接跳至要点。
这就是该解决方案最终看起来像使用与上述相同的数据概念(此处提供了完整注释的版本)并在中实现的结果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
}
这是我创建的一对新的枚举特征(位于此Gist中)的示例用法,以实现枚举定义中期望和概述的所有功能。
表示的一个担忧是枚举成员名称必须重复(decorationOrderedSet
在上面的示例中)。虽然我确实将其最小化为单个重复,但是由于两个问题,我无法看到如何使它更小:
getClass.getDeclaredClasses
具有不确定的顺序(并且不太可能case object
与源代码中的声明顺序相同)考虑到这两个问题,我不得不放弃尝试生成隐式排序,并且必须明确要求客户端使用某种有序集合概念进行定义和声明。由于Scala集合没有插入排序的集合实现,因此我能做的最好的事情是使用a List
,然后在运行时检查它是否确实是集合。这不是我希望实现这一目标的方式。
鉴于设计需要第二个列表/集合排序val
,给出ChessPiecesEnhancedDecorated
上面的示例,可以添加case object PAWN2 extends Member
然后忘记添加Decoration(PAWN2,'P2', 2)
到decorationOrderedSet
。因此,需要进行运行时检查,以验证该列表不仅是一个集合,而且还包含所有扩展了的case对象sealed trait Member
。那是一种特殊的反思/宏观地狱形式。
请在要点上发表评论和/或反馈。
org.scalaolio.util.Enumeration
org.scalaolio.util.EnumerationDecorated
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)
}
对象是懒惰的;通过使用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的方法预先加载枚举值。非惰性案例对象为您提供了最简洁的语法:
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
,我只会取回以前访问的值。有什么办法解决吗?
更新:下面的代码有一个错误,在此进行描述。下面的测试程序可以工作,但是,如果要在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”,以免出现此类警告,或者在结尾处添加一个万能的案例。
当您运行上述程序时,将获得以下输出:
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
]在FP世界中是致命的致命罪恶” –我认为这种观点没有被普遍接受。
我这里有一个很好的简单库,它使您可以使用密封的特征/类作为枚举值,而不必维护自己的值列表。它依赖于一个不依赖越野车的简单宏knownDirectSubclasses
。
2017年3月更新:根据Anthony Accioly的评论,scala.Enumeration/enum
PR已关闭。
Dotty(Scala的下一代编译器)将发挥主导作用,尽管 1970年发行了dotty,而 Martin Odersky 发行了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接受的枚举)。
我已经看到了使case类模仿枚举的各种版本。这是我的版本:
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)
}
也许有人会想出一个更好的技巧,而不是像我一样简单地将每个案例类添加到列表中。那是我当时能想到的。
最近几次我一直在来回选择这两个选项。直到最近,我更喜欢使用密封的特征/案例对象选项。
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枚举
优点:-使用选项进行实例化或直接假定为准确的函数(从持久性存储加载时更容易)-支持对所有可能值的迭代
缺点:-不支持非详尽搜索的编译警告(使模式匹配不太理想)
案例对象/密封特征
优点:-使用密封特征,我们可以预先实例化一些值,而其他值可以在创建时注入-完全支持模式匹配(已定义应用/未应用方法)
缺点:-从持久性存储实例化-您通常必须在此处使用模式匹配或定义所有“枚举值”的自己的列表
最终使我改变看法的是以下片段:
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对象/密封特征。
我更喜欢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 }
}
对于仍在寻找如何使GatesDa起作用的答案的人:您可以在声明要实例化的case对象之后引用它:
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
}
我认为case classes
over 的最大优点enumerations
是您可以使用类型类模式(又名即席多态)。不需要匹配以下枚举:
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() = ...
}