案例对象与Scala中的枚举

bsint 发布于 2019-11-10 enumeration 最后更新 2019-11-10 12:11 77 浏览

是否有关于什么时候使用case classes(或case对象)和在Scala中扩展Enumeration的最佳实践指南? 他们似乎提供了一些相同的好处。

已邀请:

uvero

赞同来自:

我在这里有一个很好的简单库,它允许你使用密封的特征/类作为枚举值,而不必维护自己的值列表。它依赖于一个不依赖于有缺陷的knownDirectSubclasses的简单宏。 https://github.com/lloydmeta/enumeratum

riure

赞同来自:

内容太长未翻译

et_est

赞同来自:

更新:下面的代码有一个错误,描述为here。下面的测试程序可以工作,但是如果您在DayOfWeek本身之前使用DayOfWeek.Mon(例如),它将失败,因为DayOfWeek尚未初始化(使用内部对象不会导致外部对象被初始化)。如果您在主类中执行类似val enums = Seq( DayOfWeek )的操作,强制初始化枚举,或者您可以使用chaotic3quilibrium的修改,您仍然可以使用此代码。期待以宏观为基础的枚举!


如果你想
  • 关于非详尽模式匹配的警告
  • 分配给每个枚举值的Int ID,您可以选择控制
  • 枚举值的不可变列表,按照定义的顺序
  • 从名称到枚举值的不可变Map
  • 从id到枚举值的不可变Map
  • 用于粘贴所有或特定枚举值的方法/数据,或整个枚举的地方
  • 订购的枚举值(例如,您可以测试是否日期< Wednesday)
  • 扩展一个枚举以创建其他人的能力
那么以下可能是有意义的。欢迎反馈。 在此实现中,您可以扩展抽象的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)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
请注意,由于List和Maps是不可变的,因此您可以轻松删除元素以创建子集,而不会破坏枚举本身。 这是Enum类本身(以及其中的EnumVal):
abstract class Enum {
type Val <: EnumVal
protected var nextId: Int = 0
private var values_       =       ListVal
  private var valuesById_   = MapInt   ,Val
  private var valuesByName_ = MapString,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)
}

verror

赞同来自:

如果您认真考虑维护与其他JVM语言(例如Java)的互操作性,那么最好的选择是编写Java枚举。这些工作透明地来自Scala和Java代码,这对于scala.Enumeration或案例对象来说更是如此。如果可以避免的话,我们就没有为GitHub上的每个新业余爱好项目创建一个新的枚举库!

dquis

赞同来自:

我最近几次需要它们时,我一直在这两个选项上来回走动。直到最近,我的偏好是密封的特征/案例对象选项。 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枚举 优点: - 用于实例化选项或直接假设准确的功能(从持久性存储加载时更容易) - 支持对所有可能值的迭代 缺点: - 不支持非穷举搜索的编译警告(使模式匹配不太理想) 案例对象/密封特征 优点: - 使用密封的特征,我们可以预先实例化一些值,而其他值可以在创建时注入 -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。

nodit

赞同来自:

我已经看过各种版本的案例类模仿枚举。这是我的版本:

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)
}
也许有人可以提出一个更好的技巧,而不是像我一样简单地将每个案例类添加到列表中。这就是我当时能想到的。

tut

赞同来自:

2017年3月更新:由Anthony Accioly评论,scala.Enumeration/enum PR已关闭。 Dotty(Scala的下一代编译器)将占据主导地位,尽管dotty issue 1970Martin Odersky's PR 1958


注意:现在(2016年8月,6年以后)有关删除scala.Enumeration的提案:PR 5352

Deprecate scala.Enumeration, add @enum annotation

The syntax
@enum
 class Toggle {
  ON
  OFF
 }
is a possible implementation example, intention is to also support ADTs that conform to certain restrictions (no nesting, recursion or varying constructor parameters), e. g.:
@enum
sealed trait Toggle
case object ON  extends Toggle
case object OFF extends Toggle
Deprecates the unmitigated disaster that is scala.Enumeration.

Advantages of @enum over scala.Enumeration:

  • Actually works
  • Java interop
  • No erasure issues
  • No confusing mini-DSL to learn when defining enumerations

Disadvantages: None.

This addresses the issue of not being able to have one codebase that supports Scala-JVM, Scala.js and Scala-Native (Java source code not supported on Scala.js/Scala-Native, Scala source code not able to define enums that are accepted by existing APIs on Scala-JVM).

pdolor

赞同来自:

当您需要在所有实例中进行迭代或过滤时,案例类与枚举的另一个缺点。这是Enumeration(以及Java枚举)的内置功能,而案例类不自动支持此类功能。 换句话说:“没有简单的方法来获取包含案例类的枚举值的总列表”。

iest

赞同来自:

一个很大的区别是Enumerations支持从某些name字符串中实例化它们。例如:

object Currency extends Enumeration {
   val GBP = Value("GBP")
   val EUR = Value("EUR") //etc.
} 
然后你可以这样做:
val ccy = Currency.withName("EUR")
当希望持久化枚举(例如,到数据库)或从驻留在文件中的数据创建枚举时,这非常有用。但是,我发现一般来说,Scala中的枚举有点笨拙并且有一种笨拙的附加组件的感觉,所以我现在倾向于使用case objects。 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指出的那样(通过一些修正来简化阅读):
Regarding "UnknownCurrency(code)" pattern, there are other ways to handle not finding a currency code string than "breaking" the closed set nature of the Currency type. UnknownCurrency being of type Currency can now sneak into other parts of an API. It's advisable to push that case outside Enumeration and make the client deal with an Option[Currency] type that would clearly indicate there is really a matching problem and "encourage" the user of the API to sort it out him/herself.
要在此处跟进其他答案,case objects优于Enumerations的主要缺点是:
  1. 无法迭代“枚举”的所有实例。事实确实如此,但我发现在实践中极少需要这样做。
  2. 无法从持久值中轻松实例化。这也是事实,但除了巨大的枚举(例如,所有货币)之外,这不会带来巨大的开销。

jneque

赞同来自:

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

et_et

赞同来自:

我更喜欢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 }
}

kex

赞同来自:

对于那些仍在寻找如何获得GatesDa's answer to work的人: 您可以在声明它实例化之后引用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
}

xeos

赞同来自:

使用案例类而不是Enumerations的优点是:

  • 使用密封案例类时,Scala编译器可以判断匹配是否已完全指定,例如当匹配声明中包含所有可能的匹配时。使用枚举,Scala编译器无法分辨。
  • 案例类自然支持的字段多于支持名称和ID的基于值的枚举。
使用Enumerations而不是case类的优点是:
  • 枚举通常会少一些代码。
  • 对于Scala新手来说,枚举更容易理解,因为它们在其他语言中很流行
因此,通常,如果您只需要按名称列出简单常量,请使用枚举。否则,如果您需要更复杂的东西,或者希望编译器的额外安全性告诉您是否指定了所有匹配项,请使用案例类。