Case objects vs Enumerations in Scala

前端 未结 14 1194
误落风尘
误落风尘 2020-11-22 16:45

Are there any best-practice guidelines on when to use case classes (or case objects) vs extending Enumeration in Scala?

They seem to offer some of the same benefits.

14条回答
  •  长情又很酷
    2020-11-22 17:22

    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!


    If you want

    • warnings about non-exhaustive pattern matches
    • an Int ID assigned to each enum value, which you can optionally control
    • an immutable List of the enum values, in the order they were defined
    • an immutable Map from name to enum value
    • an immutable Map from id to enum value
    • places to stick methods/data for all or particular enum values, or for the enum as a whole
    • ordered enum values (so you can test, for example, whether day < Wednesday)
    • the ability to extend one enum to create others

    then the following may be of interest. Feedback welcome.

    In this implementation there are abstract Enum and EnumVal base classes, which you extend. We'll see those classes in a minute, but first, here's how you would define an enum:

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

    Note that you have to use each enum value (call its apply method) to bring it to life. [I wish inner objects weren't lazy unless I specifically ask for them to be. I think.]

    We could of course add methods/data to DayOfWeek, Val, or the individual case objects if we so desired.

    And here's how you would use such an enum:

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

    Here's what you get when you compile it:

    DayOfWeekTest.scala:31: warning: match is not exhaustive!
    missing combination            Sat
    missing combination            Sun
    
      def aufDeutsch( day: DayOfWeek.Val ) = day match {
                                             ^
    one warning found
    

    You can replace "day match" with "( day: @unchecked ) match" where you don't want such warnings, or simply include a catch-all case at the end.

    When you run the above program, you get this output:

    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
    

    Note that since the List and Maps are immutable, you can easily remove elements to create subsets, without breaking the enum itself.

    Here is the Enum class itself (and EnumVal within it):

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

    And here is a more advanced use of it which controls the IDs and adds data/methods to the Val abstraction and to the enum itself:

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

提交回复
热议问题