Scala中的案例对象与枚举


231

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

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


2
我已经撰写了有关Scala枚举和替代方法的简短概述,您可能会发现它很有用:pedrorijo.com/blog/scala-enums/
pedrorijo91 2016年

1
另请参阅基于Dotty的Scala 3enum(适用于2020年中)。
VonC

Answers:


223

最大的不同是Enumerations支持从某些nameString 实例化它们。例如:

object Currency extends Enumeration {
   val GBP = Value("GBP")
   val EUR = Value("EUR") //etc.
} 

然后,您可以执行以下操作:

val ccy = Currency.withName("EUR")

当希望保留枚举(例如,到数据库)或从文件中的数据创建枚举时,此功能很有用。但是,我发现总体上来说,枚举在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

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

trade.ccy match {
  case EUR                   =>
  case UnknownCurrency(code) =>
}

正如@ chaotic3quilibrium指出的(为了便于阅读而进行了一些更正):

关于“ UnknownCurrency(code)”模式,除了“破坏”货币对的封闭集性质外,还有其他方法可以解决找不到货币代码字符串的问题。 Currency类型。UnknownCurrency类型Currency现在可以潜入API的其他部分。

最好把那个盒子推到外面 Enumeration,让客户处理Option[Currency]明显表明确实存在匹配问题的类型,并“鼓励” API用户自行解决。

在这里继续跟进其他答案时,case objects over 的主要缺点Enumeration s是:

  1. 无法遍历“枚举”的所有实例。确实是这种情况,但是在实践中我发现极少数情况是必须这样做的。

  2. 无法从持久值中轻松实例化。这也是正确的,但是,除非有大量的枚举(例如,所有货币),否则这不会带来巨大的开销。


10
另一个区别是,枚举枚举是开箱即用的,而基于案例对象的枚举则不是很明显
om-nom-nom

1
案例对象的另一点是您是否关心Java互操作性。Enumeration会将值返回为Enumeration.Value,因此1)需要scala库,2)丢失实际的类型信息。
juanmirocks 2012年

7
@oxbow_lakes关于第1点,特别是这一部分“ ...在实践中,我发现它是极为罕见的”。显然,您很少进行很多UI工作。这是一个非常普通的用例。显示从中选择的有效枚举成员的(下拉)列表。
chaotic3quilibrium 2014年

我不了解trade.ccy密封特质示例中要匹配的项目的类型。
rloth

并且不会case object产生比(?4x)大的代码足迹Enumeration吗?有用的区别,特别是对于scala.js需要占用空间小的项目。
ecoe

69

更新:已经创建了 一个新的基于宏的解决方案,它远远优于我在下面概述的解决方案。我强烈建议使用这种基于宏的解决方案看来Dotty的计划将使这种样式的枚举解决方案成为该语言的一部分。hoo!

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

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

详细信息:
您要问的问题很笼统。“ ...何时使用caseobjects与扩展[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;
  }
}

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

  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}
}

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

  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]
}

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

  1. 3.1-如果还可以使用不区分大小写的名称来检索成员,那就太好了
  2. 7-超越Java的Enum思维,能够显式利用Scala的模式匹配穷举性检查进行枚举将是很好的选择

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


因此,考虑到上面的枚举定义,以上三个解决方案均无法正常工作,因为它们不能提供上面的枚举定义中概述的所有内容:

  1. 直接在混合Scala / Java项目中的Java枚举
  2. “密封特征+案例对象”
  3. 标量枚举

这些解决方案中的每一个最终都可以进行重新设计/扩展/重构,以尝试满足每个人遗漏的需求。但是,Java Enumscala.Enumeration解决方案都无法充分扩展以提供第7项。对于我自己的项目,这是在Scala中使用封闭类型更引人注目的价值之一。我强烈希望使用编译时警告/错误来指示我的代码中存在间隔/问题,而不是必须将其从生产运行时异常/故障中清除出去。


在这方面,我着手研究该case object途径,以查看是否可以产生一个涵盖以上所有枚举定义的解决方案。第一个挑战是解决JVM类/对象初始化问题的核心(在此StackOverflow帖子中有详细介绍)。我终于找到了解决方案。

我的解决方案有两个特点:EnumerationEnumerationDecorated,并且由于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在上面的示例中)。虽然我确实将其最小化为单个重复,但是由于两个问题,我无法看到如何使它更小:

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

考虑到这两个问题,我不得不放弃尝试生成隐式排序,并且必须明确要求客户端使用某种有序集合概念进行定义和声明。由于Scala集合没有插入排序的集合实现,因此我能做的最好的事情是使用a List,然后在运行时检查它是否确实是集合。这不是我希望实现这一目标的方式。

鉴于设计需要第二个列表/集合排序val,给出ChessPiecesEnhancedDecorated上面的示例,可以添加case object PAWN2 extends Member然后忘记添加Decoration(PAWN2,'P2', 2)decorationOrderedSet。因此,需要进行运行时检查,以验证该列表不仅是一个集合,而且还包含所有扩展了的case对象sealed trait Member。那是一种特殊的反思/宏观地狱形式。


请在要点上发表评论和/或反馈。


我现在已经发布了ScalaOlio库(GPLv3)的第一个版本,其中包含以下两个版本的最新版本:scalaolio.orgorg.scalaolio.util.Enumerationorg.scalaolio.util.EnumerationDecorated
chaotic3quilibrium,2016年

并直接跳至Github上的ScalaOlio存储库:github.com/chaotic3quilibrium/scala-olio
chaotic3quilibrium,2016年

5
这是一个高质量的答案,可以从中得到很多。谢谢
angabriel '16

1
似乎Odersky想要使用本地枚举来升级Dotty(未来的Scala 3.0)。hoo!github.com/lampepfl/dotty/issues/1970
chaotic3quilibrium

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)
}

对象是懒惰的;通过使用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

3
一个问题:最后一个解决方案称为“非惰性案例对象”,但在这种情况下,直到我们使用它们才加载对象:为什么将此解决方案称为非惰性?
Seb Cesbron

2
@Noel,您需要使用:paste将整个密封的层次结构粘贴到REPL中。如果不这样做,则具有密封基类/特征的单行将被视为一个文件,并立即被密封,并且不能在下一行扩展。
尔根·斯特罗贝尔

2
@GatesDA只有您的第一个代码段没有错误(因为您明确要求客户端声明和定义值。第二个和第三个解决方案都具有我在上一条评论中描述的细微错误(如果客户端碰巧访问货币) .GBP,首先,值列表将“乱序”。)我已经广泛地研究了Scala枚举域,并在我对同一线程的回答中进行了详细介绍:stackoverflow.com/a/25923651/501113
chaotic3quilibrium 2014年

1
也许这种方法的缺点之一(无论如何与Java Enums相比)是,当您在IDE中键入Currency <dot>时,它不会显示可用的选项。
伊万·巴拉索夫

1
如@SebCesbron所述,这里的case对象是惰性的。因此,如果我致电Currency.values,我只会取回以前访问的值。有什么办法解决吗?
Sasgorilla

27

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

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

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

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

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


15

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


如果你想

  • 有关非穷尽模式匹配的警告
  • 分配给每个枚举值的Int ID,您可以选择控制
  • 枚举值的不可变列表,按其定义的顺序
  • 从名称到枚举值的不可变Map
  • 从id到枚举值的不可变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()
}

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

Tyvm提供了这一点。我真的很感激。但是,我注意到它使用的是“ var”而不是val。这是FP世界中致命的致命罪恶。那么,有没有一种方法可以实现这一点,而无需使用var?只是好奇这是否是某种FP类型的边缘情况,并且我不理解您的实现是FP不可取的。
chaotic3quilibrium

2
我可能帮不了你。在Scala中编写类在内部发生变异,但对于使用它们的人却是不可变的,这是相当普遍的。在上面的示例中,DayOfWeek的用户无法对枚举进行突变;例如,无法在此之后更改星期二的ID或名称。但是,如果您想要一个内部无变异的实现,那么我什么也没有。不过,看到基于2.11中宏的漂亮的新枚举功能并不令我感到惊讶。想法在scala-lang上被提出。
AmigoNico

我在Scala工作表中遇到一个奇怪的错误。如果直接使用Value实例之一,则会收到初始化错误。但是,如果我调用.values方法以查看枚举的内容,则该方法有效,然后直接使用值实例即可。知道初始化错误是什么吗?不管调用约定如何,确保初始化以正确顺序发生的最佳方法是什么?
chaotic3quilibrium

@ chaotic3quilibrium:哇!感谢您的追求,当然也要感谢Rex Kerr的辛苦工作。我将在这里提到问题,并参考您创建的问题。
AmigoNico

“ [使用var]在FP世界中是致命的致命罪恶” –我认为这种观点没有被普遍接受。
Erik Kaplun


10

2017年3月更新:根据Anthony Accioly的评论,scala.Enumeration/enumPR已关闭。

Dotty(Scala的下一代编译器)将发挥主导作用,尽管 1970年发行了dotty, Martin Odersky 发行了PR 1958


注意:现在(2016年8月,超过6年后)建议删除scala.EnumerationPR 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接受的枚举)。


上面的PR已关闭(不高兴)。现在是2017年,看起来Dotty最终将获得一个枚举构造。这是问题马丁的公关。合并,合并,合并!
安东尼·阿乔利

8

当您需要在所有实例之间进行迭代或筛选时,案例类与枚举的另一个缺点是。这是Enumeration的内置功能(以及Java枚举),而case类并不自动支持这种功能。

换句话说:“没有简单的方法来获得带有案例类的枚举值总数的列表”。


5

如果您认真维护与其他JVM语言(例如Java)的互操作性,那么最好的选择是编写Java枚举。它们可以从Scala和Java代码中透明地工作,这远远超出了对scala.Enumerationcase对象的要求。如果可以避免的话,让我们在GitHub上为每个新的爱好项目没有一个新的枚举库!


4

我已经看到了使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)
}

也许有人会想出一个更好的技巧,而不是像我一样简单地将每个案例类添加到列表中。那是我当时能想到的。


为什么有两种单独的不适用方法?
Saish 2012年

@jho我一直试图按原样解决您的解决方案,但无法编译。在第二个代码段中,“类型V =站点”中引用了站点。我不确定要指的是清除编译错误。接下来,为什么要为“抽象类货币”提供空括号?难道他们只是被遗弃了吗?最后,为什么在“ var values = ...”中使用var?这是否意味着客户可以随时在代码中的任何位置为值分配新的列表?将其设为val而不是var会更可取吗?
chaotic3quilibrium 2014年

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枚举

优点:-使用选项进行实例化或直接假定为准确的函数(从持久性存储加载时更容易)-支持对所有可能值的迭代

缺点:-不支持非详尽搜索的编译警告(使模式匹配不太理想)

案例对象/密封特征

优点:-使用密封特征,我们可以预先实例化一些值,而其他值可以在创建时注入-完全支持模式匹配(已定义应用/未应用方法)

缺点:-从持久性存储实例化-您通常必须在此处使用模式匹配或定义所有“枚举值”的自己的列表

最终使我改变看法的是以下片段:

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对象/密封特征。


我可以看到第二种代码模式是理想的(从第一种代码模式中摆脱了两个辅助方法)。但是,我想出了一种方法,不必强迫您在这两种模式之间进行选择。我在发布到此线程的答案中涵盖了整个域:stackoverflow.com/a/25923651/501113
chaotic3quilibrium 2014年

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 }
}

0

对于仍在寻找如何使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
}

0

我认为case classesover 的最大优点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() = ...
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.