Как добавить метод в перечисление в Scala?

В Java вы могли бы:

public enum Enum {
    ONE {
        public String method() {
            return "1";
        }
    },
    TWO {
        public String method() {
            return "2";
        }
    },
    THREE {
        public String method() {
            return "3";
        }
    };

    public abstract String method();
}

Как вы делаете это в Scala?

РЕДАКТИРОВАТЬ / Полезные ссылки:

12 ответов

Решение
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")
    }
  }
}

Вот пример добавления атрибутов в перечисления scala путем расширения класса Enumeration.Val.

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) 

Основываясь на решении Криса, вы можете добиться более приятного синтаксиса с неявным преобразованием:

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,

Разработка решения Аарона, еще более компактной формы в 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

Перечисления Scala отличаются от перечислений Java.

На данный момент нет способа добавить к нему методы (в здравом смысле). Есть некоторые обходные пути, но ничего, что работает во всех случаях и не похоже на синтаксический мусор.

Я попробовал что-то похожее (добавление методов к перечисляемому экземпляру класса, при этом возможность создавать новые экземпляры во время выполнения и иметь рабочие отношения эквивалентности между objectс и new экземпляры класса), но был остановлен ошибкой # 4023 ("getClasses / getDeclaredClasses, кажется, пропускает некоторые (REPL) или все (scalac) объявленные классы (объекты)").

Посмотрите на эти вопросы, связанные с мной:

Честно говоря, я бы не стал использовать Enumeration, Это класс, созданный в Scala 1.0 (2004), и в нем есть странные вещи, и не многие люди (кроме тех, кто его написал) понимают, как использовать его без учебника.

Если бы мне абсолютно требовалось перечисление, я бы просто написал этот класс на Java.

Если вам не нужно перебирать значения перечисления или делать какие-либо другие вещи перечисления, я бы посоветовал использовать 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 выражение.

Вы можете сделать это:

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)

Перечисление 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.

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

} 

Если вам абсолютно необходимо иметь методы для каждого значения перечисления и иметь возможность перебирать значения, вы можете сделать что-то вроде этого:

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

Это было сделано для некоторого унаследованного кода, который нельзя переместить в более разумную стратегию enum, кроме Enumeration.

После проверки исходного кода 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" }
}

Кажется, трудно избавиться от "нового", так как используется анонимный класс. Если кто-нибудь знает, как это сделать, дайте мне знать:)

В Scala3 до сих пор нет хорошего решения, но это можно сделать менее/более хорошим способом, используя лямбда-выражения или функцию расширения.

1-й подход: метод пути как лямбда. Недостаток: невозможно использовать такие методы без круглых скобок ()

      enum Enum1 (val method: ()=>String) derives CanEqual :
  case ONE   extends Enum1(() => "1")
  case TWO   extends Enum1(() => "2")
  case THREE extends Enum1(() => "3")

2-й подход: метод расширения (вы можете поместить его в любой «объект», просто не забудьте его импортировать) Преимущество: подход действительно scala Недостаток: вы можете пропустить новое значение перечисления в совпадении, если используете настройки компилятора по умолчанию (будет только предупреждение) Используйте параметр компилятора '-Wconf:msg=match не может быть исчерпывающим: ошибка', чтобы получить ошибку компиляции.

      enum Enum2 derives CanEqual:
  case ONE, TWO, THREE
object Enum2 :
  extension (en: Enum2)
    // without parentheses () (side-effects are not expected)
    def method1: String = en match
      case Enum2.ONE   => "1"
      case Enum2.TWO   => "2"
      case Enum2.THREE => "3"
    // with parentheses () (side-effects are expected)
    def method2(): String = en match
      case Enum2.ONE   => "11"
      case Enum2.TWO   => "22"
      case Enum2.THREE => "33"

Тест

      
class EnumsTest {
  import scala.language.unsafeNulls
  import org.assertj.core.api.Assertions.assertThat

  @Test
  def enumsTest(): Unit = {
    assertThat(Enum1.ONE.method()).isEqualTo("1")
    assertThat(Enum1.TWO.method()).isEqualTo("2")
    assertThat(Enum1.THREE.method()).isEqualTo("3")

    // of course does not work without using ()
    //assertThat(Enum1.ONE.method).isEqualTo("1")

    assertThat(Enum2.ONE.method1).isEqualTo("1")
    assertThat(Enum2.TWO.method1).isEqualTo("2")
    assertThat(Enum2.THREE.method1).isEqualTo("3")

    assertThat(Enum2.ONE.method2()).isEqualTo("11")
    assertThat(Enum2.TWO.method2()).isEqualTo("22")
    assertThat(Enum2.THREE.method2()).isEqualTo("33")
  }
}

Ответ, который говорит о том, что перечисления Scala не поддерживают значения, настроенные для аргументов и методов, кажется неправильным. Другие ответы (некоторые из них включают implicit) демонстрируют, что это возможно, но создают впечатление, что требует дублирования имен: ваше значение объявлено как поле объекта java, и, во-вторых, имя передается конструктору значений в виде строки, тогда как весь смысл Enums состоит в создании повторяемого имени -> Карта значений и Scala могут обойтись без избыточности:

object Ops1 extends Enumeration {

    protected case class OpsVal(f: Int => Int) extends super.Val(/*nextId*/)

    val ZERO = new FuncVal (x => 0)
    val DOUBLE = new FuncVal (x => 2 * x )

    implicit def convert(v: Value) = v.asInstanceOf[OpsVal]

}

// implicit is not needed
Ops1.ZERO.f(1)                            //> res0: Int = 0

// implicit is needed
Ops1.values map (v => (v + "=>" + v.f(1)))//> res1: scala.collection.immutable.SortedSet[String] = TreeSet(DOUBLE=>2, ZERO=>0)

Я думаю, что выше, является более кратким, чем

object Ops2 extends Enumeration {

    protected abstract class OpsVal extends Val() {
      def f(a: Int): Int
    }

    val ZERO = new OpsVal { def f(x: Int) = 0 }
    val DOUBLE = new OpsVal { def f(x: Int) = 2 * x }

    implicit def convert(valu: Value) = valu.asInstanceOf[OpsVal]
}
Ops2.ZERO.f(1) // implicit is not needed  //> res2: Int = 0

// implicit is needed
Ops2_3.values map (v => (v, v(1)))        //> res7: scala.collection.immutable.SortedSet[(e.Ops2_3.Value, Int)] = TreeSet
                                              //| ((ZERO,0), (DOUBLE,2))

Поскольку для каждого значения существует один метод, мы можем преобразовать их в функции

object Ops2_3 extends Enumeration {

    protected case class FuncVal(f: Int => Int) extends Val {
        def apply(x: Int) = f(x) // no need to extend Function1 explicitly
    }

    val ZERO = new FuncVal (x => 0)
    val DOUBLE = new FuncVal (x => 2 * x )

    implicit def convert(v: Value) = v.asInstanceOf[FuncVal]

}
Ops2_3.ZERO(1) // implicit is not needed  //> res6: Int = 0

// implicit is needed
Ops2_3.values map (v => (v, v(1)))        //> res7: scala.collection.immutable.SortedSet[(e.Ops2_3.Value, Int)] = TreeSet
                                              //| ((ZERO,0), (DOUBLE,2))

Функции, совместно используемые всеми значениями, могут быть определены следующим образом (можно использовать в анализаторе arg)

val args: Array[String] = "-silent -samples 100 -silent ".split(" +").toArray
                                              //> args  : Array[String] = Array(-silent, -samples, 100, -silent)
object Opts extends Enumeration {

    val nopar, silent, samples = new Val() {
        def apply() = args.contains(toString)
        def asInt(default: Int) = { val i = args.indexOf(toString) ;  if (i == -1) default else args(i+1).toInt}
        def asInt: Int = asInt(-1)
        override def toString = "-" + super.toString
    }
}

Opts.nopar()                              //> res0: Boolean = false
Opts.samples.asInt                        //> res1: Int = 100

Другие пользователи приводят доводы в пользу случаев запечатанных черт + макросов. Итерация по запечатанным чертам в Scala?

Другие вопросы по тегам