在Scala中,何时使用案例类(或案例对象)与扩展枚举有什么最佳实践指南吗?
它们似乎提供了一些相同的好处。
在Scala中,何时使用案例类(或案例对象)与扩展枚举有什么最佳实践指南吗?
它们似乎提供了一些相同的好处。
当前回答
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或谷歌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
其他回答
一个很大的区别是枚举支持从某个名称String实例化它们。例如:
object Currency extends Enumeration {
val GBP = Value("GBP")
val EUR = Value("EUR") //etc.
}
然后你可以这样做:
val ccy = Currency.withName("EUR")
这在希望持久化枚举(例如,到数据库)或从驻留在文件中的数据创建枚举时非常有用。然而,我发现在Scala中枚举通常有点笨拙,给人一种笨拙的附加组件的感觉,所以我现在倾向于使用case对象。case对象比enum更灵活:
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) =>
}
正如@ chaotic3equilibrium所指出的(为了便于阅读,做了一些更正):
关于“UnknownCurrency(code)”模式,除了“破坏”currency类型的封闭集性质外,还有其他方法可以处理找不到货币代码字符串的问题。类型为Currency的UnknownCurrency现在可以潜入API的其他部分。 建议将这种情况推到枚举之外,并让客户端处理Option[Currency]类型,这将清楚地表明确实存在匹配问题,并“鼓励”API的用户自己进行排序。
为了跟进这里的其他答案,case对象相对于Enumerations的主要缺点是:
不能遍历“枚举”的所有实例。这当然是事实,但我发现在实践中很少需要这样做。 不容易从持久化值实例化。这也是正确的,但是,除了在大量枚举的情况下(例如,所有货币),这并不会带来巨大的开销。
更新: 一个新的基于宏的解决方案已经创建,它远远优于我下面概述的解决方案。我强烈推荐使用这种新的基于宏的解决方案。Dotty的计划似乎将使这种枚举解决方案成为语言的一部分。Whoohoo !
简介: 尝试在Scala项目中重现Java Enum有三种基本模式。三种模式中的两种;直接使用Java Enum和scala。枚举,不能启用Scala的穷举模式匹配。第三个;“密封特质+格对象”,是否…但是有JVM类/对象初始化的复杂性,导致不一致的序号索引生成。
I have created a solution with two classes; Enumeration and EnumerationDecorated, located in this Gist. I didn't post the code into this thread as the file for Enumeration was quite large (+400 lines - contains lots of comments explaining implementation context). Details: The question you're asking is pretty general; "...when to use caseclassesobjects vs extending [scala.]Enumeration". And it turns out there are MANY possible answers, each answer depending on the subtleties of the specific project requirements you have. The answer can be reduced down to three basic patterns.
首先,让我们确保使用的是与枚举相同的基本概念。让我们主要根据Java 5(1.5)提供的Enum定义一个枚举:
It contains a naturally ordered closed set of named members There is a fixed number of members Members are naturally ordered and explicitly indexed As opposed to being sorted based on some inate member queriable criteria Each member has a unique name within the total set of all members All members can easily be iterated through based on their indexes A member can be retrieved with its (case sensitive) name It would be quite nice if a member could also be retrieved with its case insensitive name A member can be retrieved with its index Members may easily, transparently and efficiently use serialization Members may be easily extended to hold additional associated singleton-ness data Thinking beyond Java's Enum, it would be nice to be able to explicitly leverage Scala's pattern matching exhaustiveness checking for an enumeration
接下来,让我们来看看三种最常见的解决方案模式: 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;
}
}
枚举定义中的以下项不可用:
如果一个成员也可以用它不区分大小写的名字来检索,那就太好了 7 -超越Java的Enum,如果能够显式地利用Scala的模式匹配耗尽性检查枚举就好了
对于我目前的项目,我没有在Scala/Java混合项目路径上冒险的好处。即使我可以选择做一个混合项目,如果/当我添加/删除枚举成员或编写一些新代码来处理现有的枚举成员时,第7项对于允许我捕获编译时问题也是至关重要的。 B)使用“密封特征+格对象”模式:
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}
}
枚举定义中的以下项不可用:
1.2 -成员自然有序并显式建立索引 2 -所有成员都可以根据它们的索引进行迭代 成员可以通过其名称(区分大小写)进行检索 如果一个成员也可以用它不区分大小写的名字来检索,那就太好了 成员可以用它的索引来检索
它确实符合枚举定义第5和6项,这是有争议的。对于5人来说,说它是有效的有点牵强。对于6来说,扩展以保存额外的关联单例数据并不容易。 C)使用scala。枚举模式(受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的模式匹配耗尽性检查枚举就好了
同样,对于我当前的项目,第7项对于允许我在添加/删除枚举成员或编写一些新代码来处理现有枚举成员时捕获编译时问题至关重要。
因此,给定上面的枚举定义,以上三个解决方案都不能工作,因为它们不能提供上面枚举定义中概述的所有内容:
Java Enum直接在混合Scala/Java项目 “密封trait + case对象” scala。枚举
Each of these solutions can be eventually reworked/expanded/refactored to attempt to cover some of each one's missing requirements. However, neither the Java Enum nor the scala.Enumeration solutions can be sufficiently expanded to provide item 7. And for my own projects, this is one of the more compelling values of using a closed type within Scala. I strongly prefer compile time warnings/errors to indicate I have a gap/issue in my code as opposed to having to glean it out of a production runtime exception/failure.
在这方面,我开始使用case对象路径,看看是否可以生成一个涵盖上述所有枚举定义的解决方案。第一个挑战是解决JVM类/对象初始化的核心问题(在这篇StackOverflow文章中有详细介绍)。我终于找到了解决办法。
我的解决方案有两个特点;Enumeration和enumerationented,并且由于Enumeration trait超过+400行长(许多注释解释上下文),我放弃将其粘贴到这个线程(这将使它延伸到页面相当大)。详情请直接跳到主旨。
下面是使用与上面相同的数据思想(此处提供完整的注释版本)并在enumerationdecoration中实现的解决方案。
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)。虽然我确实把它减少到一次重复,但由于两个问题,我不知道如何让它更少:
这个特定对象/case对象模型的JVM对象/类初始化是未定义的(参见这个Stackoverflow线程) 方法getClass返回的内容。getDeclaredClasses有一个未定义的顺序(它不太可能与源代码中的case对象声明的顺序相同)
考虑到这两个问题,我不得不放弃尝试生成隐含的排序,而必须显式地要求客户端用某种有序集概念定义和声明它。由于Scala集合没有插入有序集实现,所以我能做的最好的事情就是使用List,然后运行时检查它是否真的是一个集合。这不是我想要的实现方式。
And given the design required this second list/set ordering val, given the ChessPiecesEnhancedDecorated example above, it was possible to add case object PAWN2 extends Member and then forget to add Decoration(PAWN2,'P2', 2) to decorationOrderedSet. So, there is a runtime check to verify that the list is not only a set, but contains ALL of the case objects which extend the sealed trait Member. That was a special form of reflection/macro hell to work through. Please leave comments and/or feedback on the Gist.
对于那些仍在寻找盖茨的答案的人来说: 你可以在声明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类模拟枚举的各种版本。以下是我的看法:
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)
}
也许有人可以想出一个更好的技巧,而不是像我这样简单地向列表中添加一个each case类。这是我当时所能想到的。
在过去几次我需要这两个选择的时候,我一直在反复考虑。直到最近,我的偏好一直是密封的trait/case对象选项。
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枚举
优点: -使用选项实例化或直接假设准确的函数(从持久存储加载时更容易) -支持迭代所有可能的值
缺点: 不支持非穷举搜索的编译警告(使模式匹配不理想)
Case对象/密封特征
优点: 使用密封特征,我们可以预实例化一些值,而其他值可以在创建时注入 完全支持模式匹配(应用/取消应用定义的方法)
缺点: -从持久存储实例化-你经常需要在这里使用模式匹配或者定义你自己的所有可能的“enum值”列表
最终让我改变观点的是以下片段:
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)
}
}
所以我认为我的首选是使用枚举当值打算从存储库访问和case对象/密封特征。