Scala中的Case对象与枚举的比较

251

有没有最佳实践指南,说明何时在Scala中使用case classes(或case objects)与扩展Enumeration?

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


2
我写了一个关于Scala枚举及其替代方案的小概述,你可能会觉得它有用:pedrorijo.com/blog/scala-enums/ - pedrorijo91
3
请参阅基于Dotty的Scala 3 enum(截至2020年年中)链接 - VonC
如果使用Scala 2.X,请使用由Typelevel管理的标准化实现:https://github.com/lloydmeta/enumeratum - chaotic3quilibrium
14个回答

239

一个重要的区别是,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作为Currency类型可能会溜入API的其他部分。

最好将该情况推送到Enumeration之外,并使客户端处理一个Option [Currency]类型,清楚地指示确实存在匹配问题,并“鼓励”API用户自行解决。

继续阅读此处的其他答案,case object相对于Enumeration的主要缺点是:

  1. 无法迭代“枚举”的所有实例。这当然是事实,但在实践中我发现非常少需要这样做。

  2. 从持久化值轻松实例化。这也是正确的,但除了在枚举很大的情况下(例如,所有货币),这并不会产生巨大的开销。


10
另一个区别是,Enumeration枚举类型默认是有序的,而基于case object的枚举类型显然不是。 - om-nom-nom
1
另一个支持 case 对象的点是,如果您关心 Java 互操作性。枚举将返回值作为 Enumeration.Value,因此需要 1)scala-library,并且 2)会失去实际类型信息。 - juanmirocks
7
关于第一点,具体来说是这部分“...我发现在实践中很少需要这样做”:显然你很少进行UI工作。这是一个非常常见的用例;显示一个(下拉)列表,其中包含可供选择的有效枚举成员。 - chaotic3quilibrium
我不理解在封闭特征示例中正在匹配的项目类型 trade.ccy - rloth
“case”对象是否比“Enumeration”生成更大(~4倍)的代码占用空间?这是一个有用的区分,特别适用于需要小占用空间的Scala.js项目。 - ecoe

73

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

摘要:
尝试在Scala项目中复制Java Enum有三种基本模式。其中两种模式:直接使用Java Enumscala.Enumeration,无法启用Scala的穷尽模式匹配。第三种模式:“密封的特质+case对象”,可以...但是会导致JVM类/对象初始化问题,从而导致不一致的序数索引生成。

我创建了两个类的解决方案:EnumerationEnumerationDecorated,位于这个Gist中。由于Enumeration文件很大(+400行,包含许多解释实现上下文的注释),因此我没有将代码发布到本主题中。

详情:
你正在提出一个相当普遍的问题:“...何时使用caseclassesobjects与扩展[scala.]Enumeration”。事实证明,有许多可能的答案,每个答案都取决于具体项目要求的细微差别。答案可以归纳为三种基本模式。

首先,让我们确保我们从相同的基本概念开始工作。让我们主要通过Java 5(1.5)提供的Enum来定义枚举:

  1. 它包含一个自然排序的命名成员的闭合集合
    1. 成员数量固定
    2. 成员是自然排序和显式索引
      • 与基于某些固有成员可查询标准进行排序相反
    3. 每个成员在所有成员的总集合中都有唯一的名称
  2. 所有成员可以根据它们的索引轻松地进行迭代
  3. 可以使用其(区分大小写的)名称检索成员
    1. 如果可以使用不区分大小写的名称检索成员,那将非常好
  4. 可以使用其索引检索成员
  5. 成员可以轻松、透明且高效地使用序列化
  6. 成员可以轻松扩展以保存其他关联的单例数据
  7. 超越Java的Enum,很好能够明确利用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;
  }
}

以下枚举定义中的项不可用:
  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. Java Enum在混合Scala / Java项目中直接使用
  2. "sealed trait + case objects"
  3. scala.Enumeration
这些解决方案中的每一个都可以最终重新设计/扩展/重构,以尝试涵盖每个缺失要求的某些部分。然而,无论是Java Enum还是scala.Enumeration解决方案都无法足够扩展以提供第7项。对于我的项目来说,这是使用Scala中的封闭类型的更具吸引力的价值之一。我强烈倾向于在编译时获得警告/错误,以指示代码中存在的差距/问题,而不是在生产运行时异常/故障中推断出它。

在这方面,我开始与case object路径一起工作,看是否能够产生一个解决方案,涵盖上述所有枚举定义。第一个挑战是推动JVM类/对象初始化问题的核心(在这个StackOverflow帖子中有详细介绍)。最终我终于找到了一个解决方案。

作为我的解决方案有两个特点: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
}

这是我创建的一对新的枚举特性的示例用法(位于此Gist中),以实现枚举定义中所需和概述的所有功能。其中一个关注点是必须重复枚举成员名称(例如上面的decorationOrderedSet)。虽然我将其最小化为单个重复,但由于两个问题,我无法看到如何使其更少:1. JVM对象/类初始化对于此特定对象/情况对象模型未定义(请参见此Stackoverflow线程)2.从方法getClass.getDeclaredClasses返回的内容具有未定义的顺序(并且很不可能与源代码中的case object声明相同)。
鉴于这两个问题,我不得不放弃尝试生成暗示的排序,并明确要求客户使用某种有序集合概念进行定义和声明。由于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版本:http://scalaolio.org/ - chaotic3quilibrium
要直接跳转到Github上的ScalaOlio存储库,请访问:https://github.com/chaotic3quilibrium/scala-olio - chaotic3quilibrium
5
这是一个高质量的回答,其中包含很多有价值的信息。谢谢。 - angabriel
1
看起来Odersky想要使用本地枚举升级Dotty(未来的Scala 3.0)。哇哦!https://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 的工具预加载枚举值。非延迟的 case object 给你最干净的语法:

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
一个问题:最后的解决方案被称为“非惰性case对象”,但在这种情况下,对象直到我们使用它们才会被加载:为什么您称此解决方案为非惰性? - Seb Cesbron
2
@Noel,你需要使用:paste将整个封闭层次结构粘贴到REPL中。如果不这样做,带有密封基类/特质的单行将被视为单个文件,立即被密封,并且无法在下一行进行扩展。 - Jürgen Strobel
2
@GatesDA 只有你的第一个代码片段没有错误(因为你明确要求客户端声明和定义值)。你的第二个和第三个解决方案都有我在上一条评论中描述的微妙错误(如果客户端碰巧直接首先访问Currency.GBP,那么values列表将“顺序错乱”)。我已经广泛探索了Scala枚举领域,并在我的回答中详细介绍了它:https://dev59.com/KHI-5IYBdhLWcg3wW28L#25923651 - chaotic3quilibrium
1
这种方法的一个缺点(与Java Enums相比)可能是,当您在IDE中键入Currency<dot>时,它不会显示可用选项。 - Ivan Balashov
1
正如@SebCesbron所提到的,这里的case对象是惰性加载的。因此,如果我调用Currency.values,我只会得到我之前访问过的值。有没有什么办法可以解决这个问题? - Sasgorilla
显示剩余3条评论

31
使用案例类(case classes)的优点而不是枚举(Enumerations)的优点包括:
  • 使用密封的案例类时,Scala编译器可以确定匹配是否完全指定,例如在匹配声明中espoused所有可能的匹配项。而对于枚举,则无法确定。
  • 案例类自然支持比基于值(Value based)的枚举更多的字段,后者仅支持名称和ID。
使用枚举而不是案例类的优点包括:
  • 通常编写枚举需要的代码比较少。
  • 对于刚接触Scala的人来说,枚举比较容易理解,因为它们在其他语言中很流行。
因此,一般情况下,如果你只需要一个简单的常数列表,可以使用枚举。否则,如果你需要更复杂的东西或者想要编译器提供额外的安全性来告诉你是否已经指定了所有的匹配项,那么可以使用案例类。

15

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


如果您想要:

  • 有关非穷尽模式匹配的警告
  • 将一个Int ID分配给每个枚举值,您可以选择控制
  • 按定义顺序的枚举值的不可变列表
  • 从名称到枚举值的不可变映射
  • 从ID到枚举值的不可变映射
  • 所有或特定枚举值的方法/数据的位置,或针对整个枚举
  • 有序的枚举值(因此您可以测试例如day < 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或各个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",或者在结尾处包含一个catch-all case。

运行上面的程序时,您会得到以下输出:

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

非常感谢您提供这个。我真的很感激。但是,我注意到它使用的是“var”,而不是“val”。在FP世界中,这是一种近乎致命的罪过。那么,有没有一种实现方式可以避免使用var呢?只是好奇这是否是某种FP类型边缘情况,我不理解您的实现为什么是FP不可取的。 - chaotic3quilibrium
2
我可能无法帮助你。在Scala中,编写在内部发生变化但对使用它们的人是不可变的类是相当常见的。在上面的示例中,DayOfWeek的用户无法改变枚举值;例如,在实际运用中无法更改星期二的ID或名称。但是,如果你想要一个内部完全没有变化的实现,那么我就没有什么能做到的了。不过,我不会感到惊讶,如果在2.11中基于宏的新Enum工具得到实现; Scala-lang上的想法正在酝酿中。 - AmigoNico
“在函数式编程领域中,使用var是一种近乎致命的罪过”,我认为这种观点并不被普遍接受。 - Erik Kaplun
根据上面的“更新-2014年9月17日”,我终于解决了客户无意中点击一个案例对象引发JVM类/对象初始化顺序问题时出现的“ID排序问题”。它位于以下Gist:https://gist.github.com/chaotic3quilibrium/57add1cd762eb90f6b24 - chaotic3quilibrium
我现在已经在同一线程中发布了一个答案,详细介绍了Scala中可用的许多枚举模式:https://dev59.com/KHI-5IYBdhLWcg3wW28L#25923651 - chaotic3quilibrium
显示剩余2条评论

13

我这里有一个不错的简单库,它允许你使用密封的 traits/classes 作为枚举值,而无需维护自己的值列表。它依赖于一个简单的宏,不依赖于有错误的knownDirectSubclasses方法。

https://github.com/lloydmeta/enumeratum


1
毫无疑问,对于Scala 2.X来说,这绝对是正确的解决方案。非常感谢您的制作。对于Scala 3(Dotty),它将配备完整的本地枚举实现。 - chaotic3quilibrium

10

更新于2017年3月:正如Anthony Accioly所评论的那样,scala.Enumeration/enum PR已被关闭。

Dotty(Scala的下一代编译器)将会主导,尽管有dotty issue 1970Martin Odersky's 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接受的枚举)。


1
上面的PR已经关闭(没有成功)。现在是2017年,看起来Dotty终于要得到一个枚举结构。这是问题Martin的PR。合并,合并,合并! - Anthony Accioly

8

与枚举相比,案例类的另一个劣势是在需要迭代或过滤所有实例时。这是枚举(以及Java枚举)的内置功能,而案例类不会自动支持此功能。

换句话说:“使用案例类没有简单的方法获取枚举值的完整列表。”


6

如果你认真考虑与其他JVM语言(如Java)保持互操作性,那么最好的选择是编写Java枚举。这些枚举可以在Scala和Java代码中透明地工作,而这不能说是scala.Enumeration或case对象所能做到的。如果可以避免,就不要为每个新的GitHub业余项目创建一个新的枚举库!


4

我看到了很多让case class模拟枚举的版本。这是我的版本:

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

也许有人能想出比我所做的简单地将每个情况类添加到列表中更好的技巧。这就是我当时能想到的全部。

为什么需要两个不同的unapply方法呢? - Saish
@jho,我一直在尝试按原样使用你的解决方案,但它无法编译。在第二个代码片段中,“type V = Site”中引用了Site,我不确定这是指什么,以消除编译错误。接下来,为什么要为“abstract class Currency”提供空括号?它们不能被省略吗?最后,为什么在“var values = ...”中使用var?这难道不意味着客户端可以随时从代码的任何地方分配一个新的List给values吗?将其设置为val而不是var是否更好? - chaotic3quilibrium

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接