在Scala中,何时使用案例类(或案例对象)与扩展枚举有什么最佳实践指南吗?

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


当前回答

与枚举相比,使用case类的优点是:

当使用密封大小写类时,Scala编译器可以判断匹配是否完全指定,例如,当所有可能的匹配都支持在匹配声明中。对于枚举,Scala编译器无法判断。 Case类自然比支持名称和ID的基于值的枚举支持更多的字段。

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

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

所以一般来说,如果你只需要一个简单的常量列表的名称,使用枚举。否则,如果你需要一些更复杂的东西,或者想要编译器告诉你是否指定了所有匹配的额外安全,用例类。

其他回答

UPDATE: The code below has a bug, described here. The test program below works, but if you were to use DayOfWeek.Mon (for example) before DayOfWeek itself, it would fail because DayOfWeek has not been initialized (use of an inner object does not cause an outer object to be initialized). You can still use this code if you do something like val enums = Seq( DayOfWeek ) in your main class, forcing initialization of your enums, or you can use chaotic3quilibrium's modifications. Looking forward to a macro-based enum!


如果你愿意

关于非穷尽模式匹配的警告 分配给每个枚举值的Int ID,您可以选择控制它 一个不可变的枚举值列表,按照它们定义的顺序 从名称到enum值的不可变映射 从id到enum值的不可变映射 为所有或特定枚举值粘贴方法/数据,或为整个枚举粘贴方法/数据的位置 排序enum值(这样您就可以测试,例如,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或个别案例对象添加方法/数据,如果我们愿意的话。

下面是如何使用这样的枚举:

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

一个很大的区别是枚举支持从某个名称String实例化它们。例如:

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

然后你可以这样做:

val ccy = Currency.withName("EUR")

这在希望持久化枚举(例如,到数据库)或从驻留在文件中的数据创建枚举时非常有用。然而,我发现在Scala中枚举通常有点笨拙,给人一种笨拙的附加组件的感觉,所以我现在倾向于使用case对象。case对象比enum更灵活:

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

正如@ chaotic3equilibrium所指出的(为了便于阅读,做了一些更正):

关于“UnknownCurrency(code)”模式,除了“破坏”currency类型的封闭集性质外,还有其他方法可以处理找不到货币代码字符串的问题。类型为Currency的UnknownCurrency现在可以潜入API的其他部分。 建议将这种情况推到枚举之外,并让客户端处理Option[Currency]类型,这将清楚地表明确实存在匹配问题,并“鼓励”API的用户自己进行排序。

为了跟进这里的其他答案,case对象相对于Enumerations的主要缺点是:

不能遍历“枚举”的所有实例。这当然是事实,但我发现在实践中很少需要这样做。 不容易从持久化值实例化。这也是正确的,但是,除了在大量枚举的情况下(例如,所有货币),这并不会带来巨大的开销。

更新: 一个新的基于宏的解决方案已经创建,它远远优于我下面概述的解决方案。我强烈推荐使用这种新的基于宏的解决方案。Dotty的计划似乎将使这种枚举解决方案成为语言的一部分。Whoohoo !

简介: 尝试在Scala项目中重现Java Enum有三种基本模式。三种模式中的两种;直接使用Java Enum和scala。枚举,不能启用Scala的穷举模式匹配。第三个;“密封特质+格对象”,是否…但是有JVM类/对象初始化的复杂性,导致不一致的序号索引生成。

I have created a solution with two classes; Enumeration and EnumerationDecorated, located in this Gist. I didn't post the code into this thread as the file for Enumeration was quite large (+400 lines - contains lots of comments explaining implementation context). Details: The question you're asking is pretty general; "...when to use caseclassesobjects vs extending [scala.]Enumeration". And it turns out there are MANY possible answers, each answer depending on the subtleties of the specific project requirements you have. The answer can be reduced down to three basic patterns.

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

It contains a naturally ordered closed set of named members There is a fixed number of members Members are naturally ordered and explicitly indexed As opposed to being sorted based on some inate member queriable criteria Each member has a unique name within the total set of all members All members can easily be iterated through based on their indexes A member can be retrieved with its (case sensitive) name It would be quite nice if a member could also be retrieved with its case insensitive name A member can be retrieved with its index Members may easily, transparently and efficiently use serialization Members may be easily extended to hold additional associated singleton-ness data Thinking beyond Java's Enum, it would be nice to be able to explicitly leverage Scala's pattern matching exhaustiveness checking for an enumeration

接下来,让我们来看看三种最常见的解决方案模式: 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;
  }
}

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

如果一个成员也可以用它不区分大小写的名字来检索,那就太好了 7 -超越Java的Enum,如果能够显式地利用Scala的模式匹配耗尽性检查枚举就好了

对于我目前的项目,我没有在Scala/Java混合项目路径上冒险的好处。即使我可以选择做一个混合项目,如果/当我添加/删除枚举成员或编写一些新代码来处理现有的枚举成员时,第7项对于允许我捕获编译时问题也是至关重要的。 B)使用“密封特征+格对象”模式:

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 -所有成员都可以根据它们的索引进行迭代 成员可以通过其名称(区分大小写)进行检索 如果一个成员也可以用它不区分大小写的名字来检索,那就太好了 成员可以用它的索引来检索

它确实符合枚举定义第5和6项,这是有争议的。对于5人来说,说它是有效的有点牵强。对于6来说,扩展以保存额外的关联单例数据并不容易。 C)使用scala。枚举模式(受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的列表相同):

如果一个成员也可以用它不区分大小写的名字来检索,那就太好了 7 -超越Java的Enum,如果能够显式地利用Scala的模式匹配耗尽性检查枚举就好了

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


因此,给定上面的枚举定义,以上三个解决方案都不能工作,因为它们不能提供上面枚举定义中概述的所有内容:

Java Enum直接在混合Scala/Java项目 “密封trait + case对象” scala。枚举

Each of these solutions can be eventually reworked/expanded/refactored to attempt to cover some of each one's missing requirements. However, neither the Java Enum nor the scala.Enumeration solutions can be sufficiently expanded to provide item 7. And for my own projects, this is one of the more compelling values of using a closed type within Scala. I strongly prefer compile time warnings/errors to indicate I have a gap/issue in my code as opposed to having to glean it out of a production runtime exception/failure.


在这方面,我开始使用case对象路径,看看是否可以生成一个涵盖上述所有枚举定义的解决方案。第一个挑战是解决JVM类/对象初始化的核心问题(在这篇StackOverflow文章中有详细介绍)。我终于找到了解决办法。

我的解决方案有两个特点;Enumeration和enumerationented,并且由于Enumeration trait超过+400行长(许多注释解释上下文),我放弃将其粘贴到这个线程(这将使它延伸到页面相当大)。详情请直接跳到主旨。

下面是使用与上面相同的数据思想(此处提供完整的注释版本)并在enumerationdecoration中实现的解决方案。

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)。虽然我确实把它减少到一次重复,但由于两个问题,我不知道如何让它更少:

这个特定对象/case对象模型的JVM对象/类初始化是未定义的(参见这个Stackoverflow线程) 方法getClass返回的内容。getDeclaredClasses有一个未定义的顺序(它不太可能与源代码中的case对象声明的顺序相同)

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

And given the design required this second list/set ordering val, given the ChessPiecesEnhancedDecorated example above, it was possible to add case object PAWN2 extends Member and then forget to add Decoration(PAWN2,'P2', 2) to decorationOrderedSet. So, there is a runtime check to verify that the list is not only a set, but contains ALL of the case objects which extend the sealed trait Member. That was a special form of reflection/macro hell to work through. Please leave comments and/or feedback on the Gist.

我更喜欢case对象(这是个人喜好的问题)。为了解决这种方法固有的问题(解析字符串并遍历所有元素),我添加了一些不完美但有效的行。

我把代码粘贴在这里,希望它有用,也希望其他人可以改进它。

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

我见过让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)
    }
}

它允许你构造如下所示的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)
}

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