如何在Scala中为枚举类型添加方法?

41

1
不是针对您的问题,但您是否考虑使用案例对象(case objects)代替枚举,如此处所讨论的:https://dev59.com/KHI-5IYBdhLWcg3wW28L - Ross
1
我已经在https://dev59.com/xlLTa4cB1Zd3GeqPWwx-上回答了这个问题,并提供了几个选项。 - Ken Bloom
在进行了广泛的选项研究后,我发布了一个更完整的领域概述,包括“sealed trait + case object”模式的通用解决方案,其中我解决了JVM类/对象初始化顺序问题:https://dev59.com/KHI-5IYBdhLWcg3wW28L#25923651 - chaotic3quilibrium
12个回答

41

以下是一个示例,演示如何通过扩展Enumeration.Val类向Scala枚举添加属性。

object Planet extends Enumeration { 
   protected case class Val(val mass: Double, val radius: Double) extends super.Val { 
     def surfaceGravity: Double = Planet.G * mass / (radius * radius) 
     def surfaceWeight(otherMass: Double): Double = otherMass * surfaceGravity 
   } 
   implicit def valueToPlanetVal(x: Value) = x.asInstanceOf[Val] 

   val G: Double = 6.67300E-11 
   val Mercury = Val(3.303e+23, 2.4397e6) 
   val Venus   = Val(4.869e+24, 6.0518e6) 
   val Earth   = Val(5.976e+24, 6.37814e6) 
   val Mars    = Val(6.421e+23, 3.3972e6) 
   val Jupiter = Val(1.9e+27, 7.1492e7) 
   val Saturn  = Val(5.688e+26, 6.0268e7) 
   val Uranus  = Val(8.686e+25, 2.5559e7) 
   val Neptune = Val(1.024e+26, 2.4746e7) 
} 

scala> Planet.values.filter(_.radius > 7.0e6) 
res16: Planet.ValueSet = Planet.ValueSet(Jupiter, Saturn, Uranus, Neptune) 

3
你还可以使用 super.Val 来扩展,不需要传递 nextIdname 参数。就像这样:protected case class Val(val mass: Double, val radius: Double) extends super.Val。这样你就可以创建没有名称的值,例如:val Mercury = Val(3.303e+23, 2.4397e6) - Jan van der Vorst
1
我不明白在构造函数参数中复制名称的优点在哪里?枚举不应该知道其值的名称吗?官方文档说他们确实知道。http://www.scala-lang.org/api/current/index.html#scala.Enumeration - Val
投票批准编辑,但回答可以通过演示如何从“val”声明中检索名称来改进。 - Patrick M
1
你还需要import scala.language.implicitConversions来抑制编译器警告。 - 200_success

34

Chris的解决方案的基础上,您可以通过隐式转换实现更好的语法:

object Suit extends Enumeration {
   val Clubs, Diamonds, Hearts, Spades = Value

   class SuitValue(suit: Value) {
      def isRed = !isBlack
      def isBlack = suit match {
         case Clubs | Spades => true
         case _              => false
      }
   }

   implicit def value2SuitValue(suit: Value) = new SuitValue(suit)
} 

然后,您可以调用例如 Suit.Clubs.isRed


9
现在,你甚至可以使用隐式类来代替 class + implicit def:implicit class SuitValue(...) {...}。 - ozeebee

13

Aaron的解决方案的基础上,使用Scala 2.10中的隐式类,可以得到更简洁的形式:

object Suit extends Enumeration {
   val Clubs, Diamonds, Hearts, Spades = Value

   implicit class SuitValue(suit: Value) {
      def isRed = !isBlack
      def isBlack = suit match {
         case Clubs | Spades => true
         case _              => false
      }
   }
} 

然后你可以像这样使用它:Suit.Clubs.isRed


12

Scala 枚举 与 Java 枚举不同。

目前,没有一种明智的方式来添加方法。虽然有一些解决方法,但没有一种适用于所有情况, 而且看起来像是语法上的垃圾。

我尝试过类似的操作(向一个类的枚举实例添加方法,并且能够在运行时创建新的实例,并且在对象和类的新实例之间具有工作等价关系), 但被错误 #4023 ("getClasses/getDeclaredClasses seems to miss some (REPL) or all (scalac) classes (objects) declared") 所阻止。

可以查看我的这些相关问题:

说实话,我不会使用 Enumeration。这是一个源自于 Scala 1.0 (2004) 的类, 它有一些奇怪的东西, 并且没有多少人(除了那些编写它的人)知道如何在没有首先进行教程的情况下使用它。

如果我绝对需要枚举,我会在 Java 中编写该类。


axel22: 嗯,我不太确定它会重用多少Java反射支持。(完全没有JVM的帮助实现所有内容将非常缓慢,并且会大大膨胀.class文件。)根据sala.reflect的重用级别,它也会遇到同样的问题。在我看来,修复这个错误是显而易见的解决方案。 - soc
@soc:你可能是对的,我想getDeclaredClasses应该更快。 - axel22
1
@soc:至少当他们编写scala.reflect时,他们必须测试一切是否按照预期工作。 - Ken Bloom
1
@Ken:我希望他们能尽早修复它 :-) - soc
@soc 在进行了广泛的选项研究后,我发布了一个更完整的领域概述,包括解决“sealed trait + case object”模式的解决方案,其中我解决了JVM类/对象初始化顺序问题:https://dev59.com/KHI-5IYBdhLWcg3wW28L#25923651 - chaotic3quilibrium
显示剩余4条评论

11

如果您不需要遍历枚举值或进行其他枚举操作,我建议使用ADT代替Enumeration

sealed abstract class Enum {
  def method: String = this match {
    case One => "1"
    case Two => "2"
    case Three => "3"
  }
}
case object One extends Enum
case object Two extends Enum
case object Three extends Enum

相较于使用Enumeration的方法,这种方法有一个优点是在 match表达式中遗漏了一个或多个情况时,编译器会发出警告。


1
@mR_fr0g: 代数数据类型 - missingfaktor
虽然这样做可以提供详尽的模式匹配,但它需要显式定义整数值,从而引入了人为错误的可能性。也就是说,如果有人添加了一个 Four 的 case 对象,然后扩展了 match 语句,加上 case Four => "3",那么反向映射将无法工作,因为现在有两个相同序数值的 case 对象。 - chaotic3quilibrium

7
你可以这样做:
object Suit extends Enumeration {
  val Clubs, Diamonds, Hearts, Spades = Value

  def isRed(suit : Value) = !isBlack(suit)
  def isBlack(suit : Value) = suit match {
    case Clubs | Spades => true
    case _              => false
  }
}

显然这不是完美的,但你可以这样做:
Suit.isBlack(Suit.Clubs)

2
不错!考虑到当前的限制,将静态辅助方法添加到周围的类似乎是最佳方法。 - soc

4

Scala的枚举类型不允许向枚举值中添加属性和/或方法。但是,使用这个新的MyEnumeration,您就可以实现这一点。

abstract class MyEnumeration {
  // "Value" must be the name of the class defining your values type Value
  type Value

  // Contains your values in definition order
  private val vals = collection.mutable.LinkedHashMap[String, Value]()

  // A mixin for your values class to automatically collect the values
  protected trait ValuesCollector { self: Value =>
    private val ordinal = vals.size

    vals += (fieldNames(ordinal) -> self)

    def getName = fieldNames(ordinal)
    override def toString = getName
  }

  def apply(ordinal: Int) = vals(fieldNames(ordinal))
  def apply(fldName: String) = vals(fldName)

  def values = vals.values
  def namedValues: collection.Map[String, Value] = vals

  // Getting the field names through reflection.
  // Copied from scala.Enumeration
  private val fieldNames = getClass.getMethods filter (m =>
    m.getParameterTypes.isEmpty &&
    classOf[ValuesCollector].isAssignableFrom(m.getReturnType) &&
    m.getDeclaringClass != classOf[MyEnumeration]) map (_.getName)

}

这里展示了Scala中的Planet示例。

object Planet extends MyEnumeration {

  case class Value(val mass: Double, val radius: Double) extends ValuesCollector {
    // universal gravitational constant  (m3 kg-1 s-2)
    private val G = 6.67300E-11;

    def surfaceGravity = G * mass / (radius * radius)
    def surfaceWeight(otherMass: Double) = otherMass * surfaceGravity

  }

  val MERCURY = Value(3.303e+23, 2.4397e6)
  val VENUS = Value(4.869e+24, 6.0518e6)
  val EARTH = Value(5.976e+24, 6.37814e6)
  val MARS = Value(6.421e+23, 3.3972e6)
  val JUPITER = Value(1.9e+27, 7.1492e7)
  val SATURN = Value(5.688e+26, 6.0268e7)
  val URANUS = Value(8.686e+25, 2.5559e7)
  val NEPTUNE = Value(1.024e+26, 2.4746e7)
  val PLUTO = Value(1.27e+22, 1.137e6)

}

object PlanetTest {
  def main(args: Array[String]) {
    val earthWeight = 175
    val mass = earthWeight/Planet.EARTH.surfaceGravity
    for (p <- Planet.values) println("Your weight on %s is %f" format (p, p.surfaceWeight(mass)))
    /* Your weight on MERCURY is 66.107583
     * Your weight on VENUS is 158.374842
     * Your weight on EARTH is 175.000000
     * Your weight on MARS is 66.279007
     * Your weight on JUPITER is 442.847567
     * Your weight on SATURN is 186.552719
     * Your weight on URANUS is 158.397260
     * Your weight on NEPTUNE is 199.207413
     * Your weight on PLUTO is 11.703031
     */
  }

} 

4
object Unit extends Enumeration {
  abstract class UnitValue(var name: String) extends Val(name) {
    def m: Unit
  }
  val G = new UnitValue("g") {
    def m {
        println("M from G")
    }
  }
  val KG = new UnitValue("kg") {
    def m {
        println("M from KG")
    }
  }
}

等一下,在枚举中,你必须写A、B、C = Value,https://dev59.com/dmgu5IYBdhLWcg3wzKAc。我不明白你对枚举的扩展。 - Val

1

查看scala.Enumeration源代码后,我得到了以下内容:


object MyEnum extends Enumeration {
  val ONE = new Val { def method = "1" }
  val TWO = new Val { def method = "2" }
  val THREE = new Val { def method = "3" }
}

似乎很难摆脱“new”,因为匿名类被使用了。 如果有人知道如何做到这一点,请告诉我 :)


1
如果您绝对需要每个枚举值都有方法,并且需要能够迭代这些值,可以像这样操作:
object BatchCategory extends Enumeration {
  class BatchCategory extends Val {
    val isOfficial, isTest, isUser = false
  }

  val OFFICIAL = new BatchCategory { override val isOfficial = true }
  val TEST =     new BatchCategory { override val isTest = true }
  val USER =     new BatchCategory { override val isUser = true }

  // Needed to get BatchCategory from Enumeration.values
  implicit def valueToBatchCategory(v: Value): BatchCategory = v match {
    case bc: BatchCategory => bc
    case x => throw new IllegalArgumentException("Value is not a BatchCategory: " + x)
  }

  def valueOf(catStr: String): BatchCategory = {
    BatchCategory.values.
      find { v => val s = v.toString; s.take(1) == catStr || s == catStr }.
      getOrElse(throw new IllegalArgumentException("Unknown category '" + catStr + "' !  "))
  }

  def main(args: Array[String]) {
    BatchCategory.values.foreach(v => println(v + " isOfficial=" + v.isOfficial))
  }
}

打印

OFFICIAL isOfficial=true
TEST isOfficial=false
USER isOfficial=false

这是为一些无法移动到更合理的枚举策略之外的旧代码所做的。


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