案例对象与Scala中的枚举

Ale*_*ler 225 enumeration scala case-class

是否有关于何时使用案例类(或案例对象)与在Scala中扩展Enumeration的最佳实践指南?

他们似乎提供了一些相同的好处.

oxb*_*kes 218

一个很大的区别是,Enumerations支持从一些nameString 实例化它们.例如:

object Currency extends Enumeration {
   val GBP = Value("GBP")
   val EUR = Value("EUR") //etc.
} 
Run Code Online (Sandbox Code Playgroud)

然后你可以这样做:

val ccy = Currency.withName("EUR")
Run Code Online (Sandbox Code Playgroud)

当希望持久化枚举(例如,到数据库)或从驻留在文件中的数据创建枚举时,这非常有用.但是,我发现一般来说,Scala中的枚举有点笨拙并且有一种笨拙的附加组件的感觉,所以我现在倾向于使用case objects.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
Run Code Online (Sandbox Code Playgroud)

所以现在我有...的优势

trade.ccy match {
  case EUR                   =>
  case UnknownCurrency(code) =>
}
Run Code Online (Sandbox Code Playgroud)

正如@ chaotic3quilibrium指出的那样(有一些修正以便于阅读):

关于"UnknownCurrency(code)"模式,还有其他方法可以处理找不到货币代码字符串而不是"破坏"类型的封闭集性质Currency.UnknownCurrency属于类型Currency现在可以潜入API的其他部分.

建议将该案例推到外面Enumeration并使客户处理一种Option[Currency]明确表明存在匹配问题的类型并"鼓励"API的用户对其进行排序.

为了跟进其他答案,case objects over Enumeration的主要缺点是:

  1. 无法遍历"枚举"的所有实例.情况确实如此,但我发现在实践中这是非常罕见的,这是必需的.

  2. 无法从持久值中轻松实例化.这也是事实,但是,除了大量枚举(例如,所有货币)的情况之外,这不会带来巨大的开销.

  • 另一个区别是Enumeration枚举是开箱即用的,而基于案例对象的枚举则不是 (10认同)
  • @oxbow_lakes关于第1点,特别是这部分"......我发现在实践中这是非常罕见的,这是必需的":显然你很少做很多UI工作.这是一个非常常见的用例; 显示要从中选择的有效枚举成员的(下拉列表)列表. (6认同)

cha*_*ium 69

更新:创建了 一个新的基于宏的解决方案,远远优于我在下面概述的解决方案.我强烈建议使用这种新的基于宏的解决方案.似乎Dotty的计划将使这种enum解决方案成为该语言的一部分.Whoohoo!

简介:
尝试Enum在Scala项目中重现Java有三种基本模式.三种模式中的两种; 直接使用Java Enumscala.Enumeration,不能够使Scala的详尽模式匹配的.第三个; "密封特征+案例对象",确实......但是JVM类/对象初始化并发症导致序数索引生成不一致.

我创建了一个包含两个类的解决方案; EnumerationEnumerationDecorated,位于此Gist中.我没有将代码发布到此线程中,因为Enumeration的文件非常大(+400行 - 包含许多解释实现上下文的注释).

细节:
你问的问题非常普遍; "......何时使用case课程objects与扩展[scala.]Enumeration".事实证明,有许多可能的答案,每个答案取决于您具体项目要求的细微之处.答案可以减少到三种基本模式.

首先,让我们确保我们使用与枚举相同的基本概念.让我们定义一个枚举,主要是根据EnumJava 5(1.5)提供的:

  1. 它包含一组自然排序的已命名成员
    1. 有固定数量的成员
    2. 成员自然被订购并明确索引
      • 而不是基于一些内部成员可查询标准进行排序
    3. 每个成员在所有成员的总集中具有唯一的名称
  2. 可以根据索引轻松迭代所有成员
  3. 可以使用其(区分大小写)名称检索成员
    1. 如果还可以使用不区分大小写的名称检索成员,那将是非常好的
  4. 可以使用其索引检索成员
  5. 成员可以轻松,透明和有效地使用序列化
  6. 可以轻松扩展成员以保存其他相关的单一数据
  7. 超越Java的思考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;
  }
}
Run Code Online (Sandbox Code Playgroud)

枚举定义中的以下项目不可用:

  1. 3.1 - 如果也可以使用不区分大小写的名称检索成员,那将是非常好的
  2. 7 - 超越Java的Enum思考,能够明确地利用Scala的模式匹配穷举检查进行枚举将会很不错

对于我目前的项目,我没有利用围绕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}
}
Run Code Online (Sandbox Code Playgroud)

枚举定义中的以下项目不可用:

  1. 1.2 - 会员自然被下令并明确编入索引
  2. 2 - 可以根据索引轻松迭代所有成员
  3. 3 - 可以使用其(区分大小写)名称检索成员
  4. 3.1 - 如果也可以使用不区分大小写的名称检索成员,那将是非常好的
  5. 4 - 可以使用索引检索成员

有争议的是它确实符合枚举定义项目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]
}
Run Code Online (Sandbox Code Playgroud)

枚举定义中的以下项目不可用(恰好与直接使用Java Enum的列表相同):

  1. 3.1 - 如果也可以使用不区分大小写的名称检索成员,那将是非常好的
  2. 7 - 超越Java的Enum思考,能够明确地利用Scala的模式匹配穷举检查进行枚举将会很不错

同样对于我当前的项目,如果/当我添加/删除枚举成员,或者正在编写一些新代码来处理现有的枚举成员时,第7项对于允许我捕获编译时问题至关重要.


因此,鉴于上面的枚举定义,上述三种解决方案都不起作用,因为它们不提供上面枚举定义中概述的所有内容:

  1. Java Enum直接在混合的Scala/Java项目中
  2. "密封特征+案例对象"
  3. scala.Enumeration

这些解决方案中的每一个都可以最终重新设计/扩展/重构,以尝试覆盖每个缺少的要求中的一些.但是,Java Enumscala.Enumeration解决方案都无法充分扩展以提供第7项.对于我自己的项目,这是在Scala中使用封闭类型的更引人注目的价值之一.我非常喜欢编译时警告/错误,以表明我的代码中存在间隙/问题,而不是必须从生产运行时异常/故障中收集它.


在这方面,我开始着手处理该case object途径,看看我是否能够制定一个涵盖上述所有枚举定义的解决方案.第一个挑战是推动JVM类/对象初始化问题的核心(在此StackOverflow帖子中有详细介绍).我终于找到了解决方案.

因为我的解决方案有两个特点; EnumerationEnumerationDecorated,并且因为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
}
Run Code Online (Sandbox Code Playgroud)

这是我创建的一对新的枚举特征(位于此Gist中)的示例用法,用于实现枚举定义中所需和概述的所有功能.

表达的一个问题是必须重复枚举成员名称(decorationOrderedSet在上面的示例中).虽然我确实将它最小化到一次重复,但由于两个问题,我无法看到如何使它更少:

  1. 此特定对象/案例对象模型的JVM对象/类初始化未定义(请参阅此Stackoverflow线程)
  2. 从该方法返回的内容getClass.getDeclaredClasses具有未定义的顺序(并且它不太可能case object与源代码中的声明的顺序相同)

鉴于这两个问题,我不得不放弃尝试生成隐含的排序,并且必须明确要求客户端定义并使用某种有序集合概念声明它.由于Scala集合没有插入有序集实现,我能做的最好是使用a List然后运行时检查它是否真的是一个集合.这不是我希望如何实现这一点.

而鉴于第二个下拉列表/套订购所需的设计val给出的,ChessPiecesEnhancedDecorated上面的例子中,有可能添加case object PAWN2 extends Member,然后忘记添加Decoration(PAWN2,'P2', 2)decorationOrderedSet.因此,有一个运行时检查来验证列表不仅是一个集合,而且包含所有扩展的列表对象sealed trait Member.这是一种特殊形式的反思/宏观地狱.


请在Gist上留下评论和/或反馈.

  • 这是一个高质量的答案,可以从中获得很多.谢谢 (5认同)

Gat*_*sDA 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)
}
Run Code Online (Sandbox Code Playgroud)

物体是懒惰的; 通过使用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") {}
}
Run Code Online (Sandbox Code Playgroud)

如果您不介意作弊,可以使用反射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
}
Run Code Online (Sandbox Code Playgroud)

很好,干净,具有案例类和Java枚举的所有优点.就个人而言,我定义对象外部的枚举值,以更好地匹配惯用的Scala代码:

object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency
Run Code Online (Sandbox Code Playgroud)

  • 一个问题:最后一个解决方案被称为"非惰性案例对象",但在这种情况下,在我们使用它们之前不会加载对象:为什么你称这个解决方案不是懒惰的? (3认同)
  • @Noel,您需要使用:paste将整个密封的层次结构粘贴到REPL中.如果不这样做,具有密封基类/特征的单行计为单个文件,将立即密封,并且不能在下一行上扩展. (2认同)
  • @GatesDA只有你的第一个代码片段没有错误(因为你明确要求客户端声明和定义值.你的第二个和第三个解决方案都有我在上一个评论中描述的细微错误(如果客户端碰巧访问Currency) .GBP直接和第一,值List将"乱序"..我已经广泛探索了Scala枚举域,并在我对同一个线程的回答中详细介绍了它:http://stackoverflow.com/a/五十零万一千一百十三分之二千五百九十二万三千六百五十一 (2认同)

Aar*_*ron 26

使用案例类优于枚举的优点是:

  • 使用密封案例类时,Scala编译器可以判断匹配是否完全指定,例如,在匹配声明中支持所有可能的匹配.使用枚举,Scala编译器无法分辨.
  • Case类自然支持的字段多于支持名称和ID的基于值的Enumeration.

使用Enumerations而不是case类的优点是:

  • 枚举通常会少写一些代码.
  • 对于Scala新手来说,枚举更容易理解,因为它们在其他语言中很流行

因此,通常,如果您只需要按名称列出简单常量,请使用枚举.否则,如果您需要更复杂的东西,或者希望编译器的额外安全性告诉您是否指定了所有匹配项,请使用案例类.


Ami*_*ico 15

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


如果你想

  • 关于非详尽模式匹配的警告
  • 分配给每个枚举值的Int ID,您可以选择控制它
  • 枚举值的不可变列表,按其定义的顺序排列
  • 从名称到枚举值的不可变映射
  • 从id到enum值的不可变Map
  • 固定所有或特定枚举值的方法/数据,或整个枚举的地方
  • 有序的枚举值(所以你可以测试,例如,是否每天<星期三)
  • 扩展一个枚举以创建其他枚举的能力

那么以下可能是有意义的.欢迎反馈.

在此实现中,您可以扩展抽象的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()
}
Run Code Online (Sandbox Code Playgroud)

请注意,您必须使用每个枚举值(调用其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"
  }

}
Run Code Online (Sandbox Code Playgroud)

以下是编译时的结果:

DayOfWeekTest.scala:31: warning: match is not exhaustive!
missing combination            Sat
missing combination            Sun

  def aufDeutsch( day: DayOfWeek.Val ) = day match {
                                         ^
one warning found
Run Code Online (Sandbox Code Playgroud)

您可以替换"一天的比赛"用"(日:@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
Run Code Online (Sandbox Code Playgroud)

请注意,由于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 )
    }
  }

}
Run Code Online (Sandbox Code Playgroud)

以下是对它的更高级用法,它控制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)
}
Run Code Online (Sandbox Code Playgroud)

  • 我可能无法帮助你.在Scala中编写在内部进行变异但对使用它们不可变的类是很常见的.在上面的例子中,DayOfWeek的用户不能改变枚举; 例如,在事实之后,无法更改星期二的ID或其名称.但如果你想要一个内部没有变异*的实现,那么我什么都没有.不过,我不会感到惊讶的是,在2.11中看到一个基于宏的新的枚举工具; scala-lang正在鼓励他们的想法. (2认同)

llo*_*eta 11

我这里有一个很好的简单库,允许你使用密封的traits/classes作为枚举值,而不必维护自己的值列表.它依赖于一个不依赖于bug的简单宏knownDirectSubclasses.

https://github.com/lloydmeta/enumeratum


Von*_*onC 10

2017年3月更新:由Anthony Accioly评论,scala.Enumeration/enum公关已经关闭.

Dotty(Scala的下一代编译器)将占据主导地位,尽管 1970年有点问题, Martin Odersky的PR 1958也是如此.


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

弃用scala.Enumeration,添加@enum注释

语法

@enum
 class Toggle {
  ON
  OFF
 }
Run Code Online (Sandbox Code Playgroud)

是一个可能的实现示例,意图是也支持符合某些限制的ADT(没有嵌套,递归或变化的构造函数参数),例如:

@enum
sealed trait Toggle
case object ON  extends Toggle
case object OFF extends Toggle
Run Code Online (Sandbox Code Playgroud)

贬低那是一场彻头彻尾的灾难scala.Enumeration.

@enum优于scala.Enumeration的优点:

  • 实际上有效
  • Java互操作
  • 没有擦除问题
  • 在定义枚举时不要混淆迷你DSL

缺点:没有.

这解决了无法拥有一个支持Scala-JVM Scala.js和Scala-Native的代码库的问题(不支持Java源代码Scala.js/Scala-Native,Scala源代码无法定义Scala-JVM上现有API接受的枚举).


小智 8

当您需要在所有实例中进行迭代或过滤时,案例类与枚举的另一个缺点.这是Enumeration(以及Java枚举)的内置功能,而案例类不自动支持此类功能.

换句话说:"没有简单的方法来获取带有案例类的枚举值的总列表".


Con*_*yle 5

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