ChatGPT解决这个技术问题 Extra ChatGPT

Scala中的案例对象与枚举

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

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

我写了一个关于 scala 枚举和替代方案的小概述,你可能会发现它很有用:pedrorijo.com/blog/scala-enums/
另请参阅 Dotty-based Scala 3 enum(2020 年中)。
如果使用 Scala 2.X,请使用由 Typelevel 管理的标准化实现:github.com/lloydmeta/enumeratum

C
Community

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

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

然后你可以这样做:

val ccy = Currency.withName("EUR")

当希望持久化枚举(例如,到数据库)或从文件中的数据创建枚举时,这很有用。但是,总的来说,我发现枚举在 Scala 中有点笨拙,并且给人一种笨拙的附加组件的感觉,因此我现在倾向于使用 case objectcase 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 现在可以潜入 API 的其他部分。建议将该案例推到 Enumeration 之外,并让客户端处理一个 Option[Currency] 类型,这将清楚地表明确实存在匹配问题,并“鼓励”API 的用户自己解决问题。

要跟进此处的其他答案,case object 优于 Enumeration 的主要缺点是:

无法遍历“枚举”的所有实例。情况确实如此,但我发现在实践中很少需要这样做。无法从持久值轻松实例化。这也是正确的,但除了大量枚举(例如,所有货币)的情况外,这不会产生巨大的开销。


另一个区别是枚举枚举是开箱即用的,而基于案例对象的枚举显然不是
案例对象的另一点是您是否关心 java 互操作性。 Enumeration 将返回值作为 Enumeration.Value,因此 1) 需要 scala-library,2) 丢失实际类型信息。
@oxbow_lakes关于第1点,特别是这部分“......我发现在实践中这是必需的”:显然你很少做很多UI工作。这是一个非常常见的用例;显示可供选择的有效枚举成员的(下拉)列表。
我不明白在密封特征示例中匹配 trade.ccy 的项目的类型。
并且 case object 生成的代码占用量不会比 Enumeration 大(~4 倍)吗?有用的区别,特别是对于需要占用空间小的 scala.js 项目。
C
Community

更新:已创建一个新的 macro based solution,它远远优于我在下面概述的解决方案。我强烈推荐使用这个新的 macro based solutionAnd it appears plans for Dotty will make this style of enum solution part of the language. 哇哦!

总结:
尝试在 Scala 项目中重现 Java Enum 有三种基本模式。三种模式中的两种;直接使用 Java Enumscala.Enumeration,无法启用 Scala 的详尽模式匹配。第三个; “密封特征 + 案例对象”,确实......但有 JVM class/object initialization complications 导致不一致的序数索引生成。

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

详情:
你问的问题很笼统; “...何时使用 caseclassesobjects 与扩展 [scala.]Enumeration”。事实证明,有很多可能的答案,每个答案都取决于您所拥有的特定项目要求的微妙之处。答案可以简化为三种基本模式。

首先,让我们确保我们的工作与枚举的基本概念相同。让我们主要根据 Enum provided as of Java 5 (1.5) 来定义一个枚举:

它包含一个自然有序的封闭命名成员集 成员数量固定可以根据索引轻松遍历成员 可以使用其(区分大小写的)名称检索成员 如果还可以使用不区分大小写的名称检索成员,那将是非常好的 可以使用其索引检索成员 成员可能很容易,透明且高效地使用序列化 成员可以很容易地扩展以保存额外的关联单例数据 考虑到 Java 的枚举之外,能够显式地利用 Scala 的模式匹配穷举检查来进行枚举会很好

接下来,让我们看一下发布的三种最常见的解决方案模式的简化版本:

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

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

3.1 - 如果成员也可以用其不区分大小写的名称来检索,那就太好了 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.2 - 成员自然排序并显式索引 2 - 所有成员都可以根据其索引轻松迭代 3 - 可以使用其(区分大小写)名称检索成员 3.1 - 如果还可以检索成员,那就太好了名称不区分大小写 4 - 可以使用索引检索成员

可以说它确实符合枚举定义项 5 和 6。对于 5,声称它是有效的有点牵强。对于 6,扩展以保存额外的关联单例数据并不容易。


C) 使用 scala.Enumeration 模式 (受 this StackOverflow answer 启发):

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 枚举的列表相同):

3.1 - 如果一个成员也可以用其不区分大小写的名称来检索,那就太好了 7 - 超越 Java 的 Enum 思考,能够显式地利用 Scala 的模式匹配穷举检查来进行枚举会很好

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

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

Java Enum 直接在混合 Scala/Java 项目“密封特征 + 案例对象”中 scala.Enumeration

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

在这方面,我开始使用 case object 路径,看看我是否可以产生一个涵盖上述所有枚举定义的解决方案。第一个挑战是突破 JVM 类/对象初始化问题的核心(在 this StackOverflow post 中有详细介绍)。我终于能够想出一个解决方案。

因为我的解决方案是两个特征; EnumerationEnumerationDecorated,并且由于 Enumeration 特征超过 +400 行(很多解释上下文的评论),我放弃将它粘贴到这个线程中(这会使它在页面上显着延伸)。详情请直接跳至Gist

以下是使用与上述相同的数据理念(完全注释版本 available here)并在 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
}

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

表达的一种担忧是枚举成员名称必须重复(上面示例中的 decorationOrderedSet)。虽然我确实将其最小化为一次重复,但由于两个问题,我不知道如何使其更少:

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

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

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


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


我现在发布了 ScalaOlio 库 (GPLv3) 的第一个版本,其中包含 org.scalaolio.util.Enumerationorg.scalaolio.util.EnumerationDecorated 的更多最新版本:scalaolio.org
并直接跳转到 Github 上的 ScalaOlio 存储库:github.com/chaotic3quilibrium/scala-olio
这是一个高质量的答案,可以从中学到很多。谢谢
看起来 Odersky 想要使用原生枚举升级 Dotty(未来的 Scala 3.0)。哇哦! github.com/lampepfl/dotty/issues/1970
G
GatesDA

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

一个问题:最后一个解决方案称为“非惰性案例对象”,但在这种情况下,直到我们使用它们才加载对象:你为什么称这个解决方案为非惰性?
@Noel,您需要使用 :paste 将整个密封层次结构粘贴到 REPL 中。如果您不这样做,则带有密封基类/特征的单行算作单个文件,将立即密封,并且不能在下一行扩展。
@GatesDA 只有您的第一个代码片段没有错误(因为您明确要求客户端声明和定义值。您的第二个和第三个解决方案都有我在上一条评论中描述的微妙错误(如果客户端碰巧访问 Currency .GBP 直接,首先,值 List 将“乱序”)。我已经广泛探索了 Scala 枚举域,并在我对同一线程的回答中详细介绍了它:stackoverflow.com/a/25923651/501113
这种方法的缺点之一(无论如何与 Java 枚举相比)可能是当您在 IDE 中键入 Currency 时,它不会显示可用选项。
正如@SebCesbron 提到的,案例对象在这里是惰性的。因此,如果我调用 Currency.values,我只会取回我之前访问过的值。有什么办法吗?
A
Aaron

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

当使用密封案例类时,Scala 编译器可以判断匹配是否完全指定,例如,当匹配声明中支持所有可能的匹配时。使用枚举,Scala 编译器无法分辨。

与支持名称和 ID 的基于值的枚举相比,案例类自然支持更多字段。

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

枚举通常会少写一些代码。

枚举对于刚接触 Scala 的人来说更容易理解,因为它们在其他语言中很流行

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


1
16 revs

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

如果你想

关于非详尽模式匹配的警告

分配给每个枚举值的 Int ID,您可以选择控制它

枚举值的不可变列表,按定义的顺序排列

从名称到枚举值的不可变映射

从 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 或单个案例对象。

以下是您将如何使用这样的枚举:

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_       =       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 类型的边缘情况,我不明白您的实现如何不受欢迎。
我可能帮不了你。在 Scala 中编写在内部发生变异但对使用它们的人来说是不可变的类是相当普遍的。在上面的例子中,DayOfWeek 的用户不能改变枚举;例如,事后无法更改星期二的 ID 或其名称。但是如果你想要一个内部没有突变的实现,那么我什么都没有。不过,如果在 2.11 中看到一个基于宏的不错的新枚举工具,我不会感到惊讶。想法正在 scala-lang 上四处流传。
我在 Scala Worksheet 中遇到了一个奇怪的错误。如果我直接使用其中一个 Value 实例,则会收到初始化错误。但是,如果我调用 .values 方法来查看枚举的内容,那么它会起作用,然后直接使用值实例就可以了。知道初始化错误是什么吗?无论调用约定如何,确保初始化以正确顺序发生的最佳方法是什么?
@chaotic3quilibrium:哇!感谢您追求这一点,当然也感谢 Rex Kerr 的繁重工作。我将在这里提到问题并参考您创建的问题。
“[使用 var] 在 FP 世界中是一种致命的罪过”——我认为这种观点并未被普遍接受。
l
lloydmeta

我在这里有一个很好的简单库,它允许您使用密封的特征/类作为枚举值,而无需维护自己的值列表。它依赖于一个不依赖于错误 knownDirectSubclasses 的简单宏。

https://github.com/lloydmeta/enumeratum


毫无疑问,对于 Scala 2.X,这绝对是正确的解决方案。 Tysvm 用于生产它。对于 Scala 3 (Dotty),它将带有完整的原生 Enum 实现。
C
Community

2017 年 3 月更新:如 Anthony Accioly 所述,scala.Enumeration/enum PR 已关闭。

Dotty(Scala 的下一代编译器)将处于领先地位,不过 dotty issue 1970Martin Odersky's 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 互操作 没有擦除问题 定义枚举时不会混淆 mini-DSL 缺点:无。这解决了无法拥有一个支持 Scala-JVM、Scala.js 和 Scala-Native 的代码库的问题(Scala.js/Scala-Native 不支持 Java 源代码,Scala 源代码无法定义枚举) Scala-JVM 上的现有 API 接受)。


上面的 PR 已经关闭(不高兴)。现在是 2017 年,看起来 Dotty 终于要获得一个枚举构造了。这是 issueMartin's PR。合并,合并,合并!
J
Jacek Laskowski

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

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


C
Connor Doyle

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


j
jho

我见过使案例类模仿枚举的各种版本。这是我的版本:

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

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


为什么有两个单独的 unapply 方法呢?
@jho 我一直在尝试按原样解决您的解决方案,但它不会编译。在第二个代码片段中,“type V = Site”中有对 Site 的引用。我不确定那是指清除编译错误。接下来,为什么要为“抽象类货币”提供空括号?他们就不能被留下吗?最后,为什么在“var values = ...”中使用 var?这是否意味着客户可以随时从代码中的任何位置为值分配一个新列表?将其设为 val 而不是 var 不是更可取吗?
j
jaguililla

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

M
Mad Dog

在我需要它们的最后几次,我一直在这两个选项上来回走动。直到最近,我的偏好一直是sealed trait/case object 选项。

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 枚举给你的所有东西,但以下是优缺点:

斯卡拉枚举

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

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

案例对象/密封特征

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

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

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

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

所以我认为我的偏好是在打算从存储库访问值时使用枚举,否则使用案例对象/密封特征。


我可以看到第二个代码模式是多么理想(从第一个代码模式中去掉两个辅助方法)。但是,我想出了一种方法,这样您就不会被迫在这两种模式之间进行选择。我在发布到此线程的答案中涵盖了整个域:stackoverflow.com/a/25923651/501113
C
Community

对于那些仍在寻找如何获取 GatesDa's answer to work 的人:您可以在声明实例对象后引用它来实例化它:

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
}

M
Murat Mustafin

我认为 case classes 优于 enumerations 的最大优势在于您可以使用 type class pattern aka ad-hoc polymorphysm。不需要匹配枚举,例如:

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() = ...
}