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

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


当前回答

我认为与枚举相比,使用case类的最大优点是可以使用类型类模式,也就是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() = ...
}

其他回答

我更喜欢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类的优点是:

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

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

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

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

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

https://github.com/lloydmeta/enumeratum

在过去几次我需要这两个选择的时候,我一直在反复考虑。直到最近,我的偏好一直是密封的trait/case对象选项。

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

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

缺点: 不支持非穷举搜索的编译警告(使模式匹配不理想)

Case对象/密封特征

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

缺点: -从持久存储实例化-你经常需要在这里使用模式匹配或者定义你自己的所有可能的“enum值”列表

最终让我改变观点的是以下片段:

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

所以我认为我的首选是使用枚举当值打算从存储库访问和case对象/密封特征。

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或谷歌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