Objek case vs Enumerasi dalam Scala

231

Apakah ada pedoman praktik terbaik tentang kapan menggunakan kelas kasus (atau objek kasus) vs memperpanjang Pencacahan di Scala?

Mereka tampaknya menawarkan beberapa manfaat yang sama.

Alex Miller
sumber
2
Saya telah menulis ikhtisar kecil tentang pencacahan scala dan alternatif, Anda mungkin menemukan itu berguna: pedrorijo.com/blog/scala-enums/
pedrorijo91
1
Lihat juga Scala 3 berbasis Dottyenum (untuk pertengahan 2020).
VonC

Jawaban:

223

Satu perbedaan besar adalah bahwa ia Enumerationdatang dengan dukungan untuk membuat mereka dari beberapa nameString. Sebagai contoh:

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

Maka Anda dapat melakukan:

val ccy = Currency.withName("EUR")

Ini berguna ketika ingin melanjutkan enumerasi (misalnya, ke database) atau membuatnya dari data yang berada di file. Namun, saya menemukan secara umum bahwa enumerasi agak canggung di Scala dan memiliki rasa add-on yang canggung, jadi saya sekarang cenderung menggunakan case objects. A case objectlebih fleksibel daripada enum:

sealed trait Currency { def name: String }
case object EUR extends Currency { val name = "EUR" } //etc.

case class UnknownCurrency(name: String) extends Currency

Jadi sekarang saya memiliki keunggulan ...

trade.ccy match {
  case EUR                   =>
  case UnknownCurrency(code) =>
}

Seperti yang ditunjukkan @ chaotic3quilibrium (dengan beberapa koreksi untuk memudahkan membaca):

Mengenai pola "UnknownCurrency (kode)", ada cara lain untuk menangani tidak menemukan string kode mata uang selain "melanggar" sifat set tertutup dari Currencyjenis tersebut. UnknownCurrencymenjadi tipe Currencysekarang dapat menyelinap ke bagian lain dari API.

Dianjurkan untuk mendorong kasus itu ke luar Enumerationdan membuat klien berurusan dengan Option[Currency]jenis yang jelas akan menunjukkan benar-benar ada masalah yang cocok dan "mendorong" pengguna API untuk mengatasinya sendiri.

Untuk menindaklanjuti jawaban lain di sini, kelemahan utama case objects over Enumerationadalah:

  1. Tidak dapat mengulangi semua contoh "enumerasi" . Ini memang masalahnya, tetapi dalam praktiknya sangat jarang hal ini diperlukan.

  2. Tidak dapat membuat instantiasi dengan mudah dari nilai yang ada . Ini juga benar tetapi, kecuali dalam kasus enumerasi besar (misalnya, semua mata uang), ini tidak menghadirkan overhead yang besar.

oxbow_lakes
sumber
10
Perbedaan lainnya adalah enum Enumerasi dipesan di luar kotak, sedangkan enum berdasarkan objek kasus tidak boleh
om-nom-nom
1
Poin lain untuk objek kasus adalah jika Anda peduli tentang interoperabilitas java. Enumerasi akan mengembalikan nilai-nilai sebagai Enumeration.Value, dengan demikian 1) membutuhkan scala-library, 2) kehilangan informasi tipe aktual.
juanmirocks
7
@oxbow_lakes Mengenai poin 1, khususnya bagian ini "... Saya merasa sangat jarang dalam praktik bahwa ini diperlukan": Rupanya Anda jarang melakukan banyak pekerjaan UI. Ini adalah kasus penggunaan yang sangat umum; menampilkan daftar (drop-down) anggota enumerasi yang valid untuk memilih.
chaotic3quilibrium
Saya tidak mengerti jenis barang yang cocok trade.ccydengan contoh sifat yang disegel.
rloth
dan jangan case objectmenghasilkan jejak kode yang lebih besar (~ 4x) dari Enumeration? Perbedaan yang berguna terutama untuk scala.jsproyek - proyek yang membutuhkan jejak kecil.
ecoe
69

UPDATE: Solusi berbasis makro baru telah dibuat yang jauh lebih unggul daripada solusi yang saya uraikan di bawah ini. Saya sangat merekomendasikan menggunakan solusi berbasis makro baru ini . Dan tampaknya rencana untuk Dotty akan membuat gaya solusi enum bagian dari bahasa. Whoohoo!

Ringkasan:
Ada tiga pola dasar untuk mencoba mereproduksi Jawa Enumdalam proyek Scala. Dua dari tiga pola; langsung menggunakan Java Enumdan scala.Enumeration, tidak mampu mengaktifkan pencocokan pola lengkap Scala. Dan yang ketiga; "disegel sifat + objek kasus", memang ... tetapi memiliki inisialisasi kelas / objek JVM yang menghasilkan generasi indeks ordinal yang tidak konsisten.

Saya telah menciptakan solusi dengan dua kelas; Enumeration and EnumerationDecorated , terletak di Intisari ini . Saya tidak memposting kode ke utas ini karena file untuk Enumerasi cukup besar (+400 baris - berisi banyak komentar yang menjelaskan konteks implementasi).

Detail:
Pertanyaan yang Anda ajukan cukup umum; "... kapan menggunakan casekelasobjects vs memperluas [scala.]Enumeration". Dan ternyata ada BANYAK kemungkinan jawaban, masing-masing jawaban tergantung pada seluk-beluk persyaratan proyek spesifik yang Anda miliki. Jawabannya dapat direduksi menjadi tiga pola dasar.

Untuk memulai, mari kita pastikan kita bekerja dari ide dasar yang sama tentang apa itu enumerasi. Mari kita mendefinisikan enumerasi sebagian besar dalam hal yang Enumdisediakan pada Java 5 (1.5) :

  1. Ini berisi kumpulan nama anggota tertutup yang dipesan secara alami
    1. Ada jumlah anggota tetap
    2. Anggota secara alami dipesan dan diindeks secara eksplisit
      • Berbeda dengan disortir berdasarkan kriteria queriable anggota inate
    3. Setiap anggota memiliki nama unik dalam set total semua anggota
  2. Semua anggota dapat dengan mudah diulang melalui berdasarkan indeks mereka
  3. Seorang anggota dapat diambil dengan nama (case case)
    1. Akan sangat menyenangkan jika anggota juga dapat diambil dengan nama case case-nya tidak sensitif
  4. Seorang anggota dapat diambil dengan indeksnya
  5. Anggota dapat dengan mudah, transparan dan efisien menggunakan serialisasi
  6. Anggota dapat dengan mudah diperpanjang untuk memiliki data singleton-ness terkait tambahan
  7. Berpikir di luar Jawa Enum, akan lebih baik untuk dapat secara eksplisit meningkatkan kecocokan pola pencocokan Scala untuk enumerasi

Selanjutnya, mari kita lihat versi tiga pola solusi yang paling umum diposting:

A) Sebenarnya langsung menggunakan pola JavaEnum (dalam proyek Scala / Java campuran):

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

Item berikut dari definisi enumerasi tidak tersedia:

  1. 3.1 - Alangkah baiknya jika seorang anggota juga dapat diambil dengan nama huruf besar-kecilnya
  2. 7 - Berpikir di luar Enum Jawa, alangkah baiknya untuk dapat secara eksplisit memanfaatkan pola pencocokan ketelitian Scala untuk pencacahan

Untuk proyek saya saat ini, saya tidak mendapat manfaat dari mengambil risiko di sekitar jalur proyek campuran Scala / Java. Dan bahkan jika saya bisa memilih untuk melakukan proyek campuran, item 7 sangat penting untuk memungkinkan saya menangkap masalah waktu kompilasi jika / ketika saya menambah / menghapus anggota enumerasi, atau sedang menulis beberapa kode baru untuk menangani anggota enumerasi yang ada.


B) Menggunakan pola " sealed trait+case objects ":

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

Item berikut dari definisi enumerasi tidak tersedia:

  1. 1.2 - Anggota secara alami dipesan dan diindeks secara eksplisit
  2. 2 - Semua anggota dapat dengan mudah diulangi berdasarkan indeks mereka
  3. 3 - Seorang anggota dapat diambil dengan nama (case case)
  4. 3.1 - Alangkah baiknya jika seorang anggota juga dapat diambil dengan nama huruf besar-kecilnya
  5. 4 - Anggota dapat diambil dengan indeksnya

Dapat diperdebatkan bahwa itu benar-benar memenuhi item definisi enumerasi 5 dan 6. Untuk 5, sangat sulit untuk mengklaim itu efisien. Untuk 6, itu tidak mudah untuk memperluas untuk menyimpan data terkait singleton-ness tambahan.


C) Menggunakan scala.Enumerationpola (terinspirasi oleh jawaban StackOverflow ini ):

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

Item berikut dari definisi enumerasi tidak tersedia (kebetulan identik dengan daftar untuk langsung menggunakan Java Enum):

  1. 3.1 - Alangkah baiknya jika seorang anggota juga dapat diambil dengan nama huruf besar-kecilnya
  2. 7 - Berpikir di luar Enum Jawa, alangkah baiknya untuk dapat secara eksplisit memanfaatkan pola pencocokan ketelitian Scala untuk pencacahan

Sekali lagi untuk proyek saya saat ini, item 7 sangat penting untuk memungkinkan saya menangkap masalah waktu kompilasi jika / ketika saya menambah / menghapus anggota enumerasi, atau sedang menulis beberapa kode baru untuk berurusan dengan anggota enumerasi yang ada.


Jadi, mengingat definisi enumerasi di atas, tidak satu pun dari tiga solusi di atas yang berfungsi karena mereka tidak memberikan semua yang diuraikan dalam definisi enumerasi di atas:

  1. Java Enum langsung dalam proyek Scala / Java campuran
  2. "objek sifat + peti tersegel"
  3. scala. Penghitungan

Masing-masing solusi ini pada akhirnya dapat dikerjakan ulang / diperluas / di refactored untuk mencoba menutupi beberapa kebutuhan yang hilang dari masing-masing. Namun, baik Java Enummaupun scala.Enumerationsolusi tidak dapat diperluas secara memadai untuk menyediakan item 7. Dan untuk proyek saya sendiri, ini adalah salah satu nilai yang lebih menarik dari penggunaan tipe tertutup dalam Scala. Saya sangat suka mengkompilasi peringatan waktu / kesalahan untuk menunjukkan bahwa saya memiliki celah / masalah dalam kode saya sebagai lawan harus mengumpulkannya dari pengecualian / kegagalan runtime produksi.


Dalam hal itu, saya mulai bekerja dengan case objectjalur untuk melihat apakah saya bisa menghasilkan solusi yang mencakup semua definisi enumerasi di atas. Tantangan pertama adalah mendorong inti dari masalah inisialisasi kelas / objek JVM (dibahas secara rinci dalam posting StackOverflow ini ). Dan saya akhirnya bisa menemukan solusi.

Karena solusi saya adalah dua sifat; Enumeration dan EnumerationDecorated , dan karena Enumerationsifatnya lebih dari +400 baris (banyak komentar yang menjelaskan konteks), saya tidak mau menempelkannya ke dalam utas ini (yang akan membuatnya meregangkan halaman secara serius). Untuk detailnya, silakan langsung menuju Intisari .

Inilah solusi yang akhirnya tampak seperti menggunakan ide data yang sama seperti di atas (versi yang sepenuhnya dikomentari tersedia di sini ) dan diimplementasikan dalam EnumerationDecorated.

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
}

Ini adalah contoh penggunaan sepasang sifat enumerasi baru yang saya buat (terletak di Intisari ini ) untuk mengimplementasikan semua kemampuan yang diinginkan dan diuraikan dalam definisi enumerasi.

Satu keprihatinan yang diungkapkan adalah bahwa nama anggota enumerasi harus diulang ( decorationOrderedSetdalam contoh di atas). Sementara saya meminimalkannya menjadi satu pengulangan, saya tidak bisa melihat bagaimana membuatnya lebih sedikit karena dua masalah:

  1. Inisialisasi objek / kelas JVM untuk objek / model objek objek tertentu ini tidak ditentukan (lihat utas Stackoverflow ini )
  2. Konten yang dikembalikan dari metode getClass.getDeclaredClassesmemiliki urutan yang tidak ditentukan (dan sangat tidak mungkin berada dalam urutan yang sama dengan case objectdeklarasi dalam kode sumber)

Dengan adanya dua masalah ini, saya harus berhenti berusaha untuk menghasilkan pemesanan tersirat dan harus secara eksplisit mengharuskan klien menentukan dan menyatakannya dengan semacam gagasan kumpulan pesanan. Karena koleksi Scala tidak memiliki implementasi implementasi urutan yang disisipkan, yang terbaik yang bisa saya lakukan adalah menggunakan Listdan kemudian memeriksa runtime apakah itu benar-benar sebuah set. Bukan bagaimana saya lebih suka untuk mencapai ini.

Dan mengingat desain yang diperlukan daftar kedua / set pemesanan ini val, mengingat ChessPiecesEnhancedDecoratedcontoh di atas, adalah mungkin untuk menambahkan case object PAWN2 extends Memberdan kemudian lupa untuk menambahkan Decoration(PAWN2,'P2', 2)ke decorationOrderedSet. Jadi, ada pemeriksaan runtime untuk memverifikasi bahwa daftar tersebut tidak hanya satu set, tetapi berisi SEMUA objek kasus yang memperpanjang sealed trait Member. Itu adalah bentuk khusus dari refleksi / makro neraka untuk dikerjakan.


Silakan tinggalkan komentar dan / atau umpan balik pada Intisari .

chaotic3quilibrium
sumber
Saya sekarang telah merilis versi pertama perpustakaan ScalaOlio (GPLv3) yang berisi lebih banyak versi terbaru dari keduanya org.scalaolio.util.Enumerationdan org.scalaolio.util.EnumerationDecorated: scalaolio.org
chaotic3quilibrium
Dan untuk melompat langsung ke repositori ScalaOlio di Github: github.com/chaotic3quilibrium/scala-olio
chaotic3quilibrium
5
Ini adalah jawaban yang berkualitas dan banyak yang bisa diambil darinya. Terima kasih
angabriel
1
Sepertinya Odersky ingin memperbarui Dotty (Scala 3.0 masa depan) dengan enum asli. Whoohoo! github.com/lampepfl/dotty/issues/1970
chaotic3quilibrium
62

Objek case sudah mengembalikan nama mereka untuk metode toString mereka, jadi meneruskannya secara terpisah tidak perlu. Berikut adalah versi yang mirip dengan jho (metode kenyamanan dihilangkan untuk singkatnya):

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

Objek malas; dengan menggunakan vals, kita dapat menjatuhkan daftar tetapi harus mengulangi namanya:

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

Jika Anda tidak keberatan melakukan kecurangan, Anda dapat memuat nilai enumerasi menggunakan API refleksi atau sesuatu seperti Google Refleksi. Objek kasing yang tidak malas memberi Anda sintaks terbersih:

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
}

Bagus dan bersih, dengan semua keunggulan kelas kasus dan enumerasi Java. Secara pribadi, saya mendefinisikan nilai enumerasi di luar objek agar lebih cocok dengan kode Scala idiomatik:

object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency
GatesDA
sumber
3
satu pertanyaan: solusi terakhir disebut "objek kasus tidak malas" tetapi dalam kasus ini objek tidak dimuat sampai kita menggunakannya: mengapa Anda menyebut solusi ini tidak malas?
Seb Cesbron
2
@ Noel, Anda perlu menggunakan: tempel untuk menempelkan seluruh hierarki yang disegel ke dalam REPL. Jika tidak, baris tunggal dengan kelas / sifat dasar yang disegel dianggap sebagai satu file, segera disegel, dan tidak dapat diperpanjang pada baris berikutnya.
Jürgen Strobel
2
@GatesDA Hanya cuplikan kode pertama Anda yang tidak memiliki bug (karena Anda secara eksplisit mengharuskan klien untuk mendeklarasikan dan menetapkan nilai. Kedua solusi kedua dan ketiga Anda memiliki bug yang halus yang saya jelaskan di komentar terakhir saya (jika klien kebetulan mengakses Mata Uang .GBP secara langsung dan pertama, Daftar nilai akan "rusak"). Saya telah menjelajahi domain enumerasi Scala secara luas dan telah membahasnya secara terperinci dalam jawaban saya untuk utas yang sama ini: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
1
Mungkin salah satu kelemahan dari pendekatan ini (dibandingkan dengan Java Enums pula) adalah bahwa ketika Anda mengetik Mata Uang <dot> dalam IDE itu tidak menunjukkan opsi yang tersedia.
Ivan Balashov
1
Seperti @SebCesbron menyebutkan, objek kasing malas di sini. Jadi jika saya menelepon Currency.values, saya hanya mendapatkan kembali nilai yang telah saya akses sebelumnya. Apakah ada jalan keluarnya?
Sasgorilla
27

Keuntungan menggunakan kelas kasus dibandingkan Enumerasi adalah:

  • Saat menggunakan kelas kasus tertutup, kompiler Scala dapat mengetahui apakah kecocokan sepenuhnya ditentukan misalnya ketika semua kecocokan yang mungkin terjadi didukung dalam deklarasi pencocokan. Dengan enumerasi, kompilator Scala tidak dapat memberi tahu.
  • Kelas kasus secara alami mendukung lebih banyak bidang daripada Enumerasi berbasis Nilai yang mendukung nama dan ID.

Keuntungan menggunakan Enumerasi daripada kelas kasus adalah:

  • Pencacahan umumnya akan menjadi sedikit lebih sedikit kode untuk ditulis.
  • Pencacahan sedikit lebih mudah dimengerti bagi seseorang yang baru mengenal Scala karena mereka lazim dalam bahasa lain

Jadi secara umum, jika Anda hanya perlu daftar konstanta sederhana dengan nama, gunakan enumerasi. Kalau tidak, jika Anda membutuhkan sesuatu yang sedikit lebih kompleks atau ingin keamanan tambahan dari kompiler memberi tahu Anda jika Anda memiliki semua kecocokan yang ditentukan, gunakan kelas kasus.

Harun
sumber
15

UPDATE: Kode di bawah ini memiliki bug, dijelaskan di sini . Program pengujian di bawah ini berfungsi, tetapi jika Anda menggunakan DayOfWeek.Mon (misalnya) sebelum DayOfWeek itu sendiri, itu akan gagal karena DayOfWeek belum diinisialisasi (penggunaan objek dalam tidak menyebabkan objek luar diinisialisasi). Anda masih dapat menggunakan kode ini jika Anda melakukan sesuatu seperti val enums = Seq( DayOfWeek )di kelas utama Anda, memaksa inisialisasi enum Anda, atau Anda dapat menggunakan modifikasi chaotic3quilibrium. Menantikan enum berbasis makro!


jika kamu mau

  • peringatan tentang kecocokan pola yang tidak lengkap
  • ID Int yang ditetapkan untuk setiap nilai enum, yang dapat Anda kontrol secara opsional
  • Daftar nilai enum yang tidak berubah, sesuai urutannya
  • Peta abadi dari nama ke nilai enum
  • Peta abadi dari id ke nilai enum
  • tempat untuk menempel metode / data untuk semua atau nilai enum tertentu, atau untuk enum secara keseluruhan
  • nilai enum yang dipesan (sehingga Anda dapat menguji, misalnya, apakah hari <Rabu)
  • kemampuan untuk memperpanjang satu enum untuk membuat yang lain

maka yang berikut ini mungkin menarik. Umpan balik.

Dalam implementasi ini ada kelas dasar Enum dan EnumVal abstrak, yang Anda memperpanjang. Kita akan melihat kelas-kelas itu sebentar lagi, tetapi pertama-tama, inilah cara Anda mendefinisikan 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()
}

Perhatikan bahwa Anda harus menggunakan setiap nilai enum (panggil metode yang berlaku) untuk menghidupkannya. [Aku berharap benda dalam tidak malas kecuali aku secara khusus memintanya. Kupikir.]

Kita tentu saja dapat menambahkan metode / data ke DayOfWeek, Val, atau objek kasus individual jika kita inginkan.

Dan inilah cara Anda menggunakan enum seperti itu:

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

}

Inilah yang Anda dapatkan ketika Anda mengompilasinya:

DayOfWeekTest.scala:31: warning: match is not exhaustive!
missing combination            Sat
missing combination            Sun

  def aufDeutsch( day: DayOfWeek.Val ) = day match {
                                         ^
one warning found

Anda dapat mengganti "pertandingan hari" dengan "pertandingan" (hari: @ cek ulang) "di mana Anda tidak ingin peringatan seperti itu, atau cukup sertakan kotak semua kasus di akhir.

Ketika Anda menjalankan program di atas, Anda mendapatkan output ini:

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

Perhatikan bahwa karena Daftar dan Peta tidak dapat diubah, Anda dapat dengan mudah menghapus elemen untuk membuat subset, tanpa merusak enum itu sendiri.

Ini adalah kelas Enum itu sendiri (dan EnumVal di dalamnya):

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

}

Dan di sini adalah penggunaan yang lebih maju yang mengontrol ID dan menambahkan data / metode ke abstraksi Val dan enum itu sendiri:

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)
}
AmigoNico
sumber
Tyvm untuk menyediakan ini. Saya sangat menghargai itu. Namun, saya memperhatikan bahwa itu menggunakan "var" sebagai lawan val. Dan ini adalah dosa berat batas di dunia FP. Jadi, apakah ada cara untuk mengimplementasikan ini sehingga tidak ada penggunaan var? Hanya ingin tahu apakah ini semacam kasus tepi jenis FP dan saya tidak mengerti bagaimana implementasi Anda adalah FP yang tidak diinginkan.
chaotic3quilibrium
2
Saya mungkin tidak bisa membantu Anda. Sangat umum di Scala untuk menulis kelas yang bermutasi secara internal tetapi tidak dapat diubah oleh mereka yang menggunakannya. Dalam contoh di atas, pengguna DayOfWeek tidak dapat bermutasi enum; tidak ada cara, misalnya, untuk mengubah ID hari Selasa, atau namanya, setelah fakta. Tetapi jika Anda menginginkan implementasi yang bebas dari mutasi secara internal , maka saya tidak punya apa-apa. Saya tidak akan terkejut, melihat fasilitas enum baru yang bagus berdasarkan makro pada 2.11; ide sedang ditendang di sekitar scala-lang.
AmigoNico
Saya mendapat kesalahan aneh di Lembar Kerja Scala. Jika saya langsung menggunakan salah satu instance Value, saya mendapatkan kesalahan inisialisasi. Namun, jika saya melakukan panggilan ke metode .values ​​untuk melihat konten enumerasi, itu berfungsi dan kemudian langsung menggunakan karya instance nilai. Adakah yang tahu kesalahan inisialisasi itu? Dan apa cara optimal untuk memastikan inisialisasi terjadi dalam urutan yang tepat terlepas dari konvensi pemanggilan?
chaotic3quilibrium
@ chaotic3quilibrium: Wow! Terima kasih telah mengejar ini, dan tentu saja terima kasih kepada Rex Kerr untuk pengangkatan berat. Saya akan menyebutkan masalah di sini dan merujuk ke pertanyaan yang Anda buat.
AmigoNico
"[Menggunakan var] adalah dosa berat batas di dunia FP" - Saya tidak berpikir bahwa pendapat diterima secara universal.
Erik Kaplun
12

Saya memiliki lib sederhana yang bagus di sini yang memungkinkan Anda untuk menggunakan sifat / kelas yang disegel sebagai nilai enum tanpa harus mempertahankan daftar nilai Anda sendiri. Itu bergantung pada makro sederhana yang tidak bergantung pada buggy knownDirectSubclasses.

https://github.com/lloydmeta/enumeratum

lloydmeta
sumber
10

Pembaruan Maret 2017: seperti yang dikomentari oleh Anthony Accioly , scala.Enumeration/enumPR telah ditutup.

Dotty (kompiler generasi berikutnya untuk Scala) akan memimpin, meskipun edisi dotty 1970 dan PR 1958 Martin Odersky .


Catatan: sekarang ada (Agustus 2016, 6+ tahun kemudian) proposal untuk dihapus scala.Enumeration: PR 5352

Jelek scala.Enumeration, tambahkan @enumanotasi

Sintaksnya

@enum
 class Toggle {
  ON
  OFF
 }

adalah contoh implementasi yang mungkin, maksudnya adalah untuk juga mendukung ADT yang sesuai dengan batasan tertentu (tidak ada nesting, rekursi, atau beragam parameter konstruktor), misalnya:

@enum
sealed trait Toggle
case object ON  extends Toggle
case object OFF extends Toggle

Mengecewakan bencana yang tidak dikurangi itu scala.Enumeration.

Keuntungan dari @enum lebih dari scala. Penghitungan:

  • Sebenarnya berhasil
  • Java interop
  • Tidak ada masalah penghapusan
  • Tidak ada mini-DSL yang membingungkan untuk dipelajari saat mendefinisikan enumerasi

Kekurangan: Tidak ada.

Ini mengatasi masalah tidak dapat memiliki satu basis kode yang mendukung Scala-JVM, Scala.jsdan Scala-Native (kode sumber Java tidak didukung Scala.js/Scala-Native, kode sumber Scala tidak dapat mendefinisikan enum yang diterima oleh API yang ada pada Scala-JVM).

VONC
sumber
PR di atas telah ditutup (tidak ada sukacita). Sekarang tahun 2017 dan sepertinya Dotty akhirnya akan mendapatkan enum construct. Inilah masalahnya dan PR Martin . Gabungkan, gabungkan, gabungkan!
Anthony Accioly
8

Kerugian lain dari kelas kasus versus Enumerasi ketika Anda harus mengulang atau memfilter semua contoh. Ini adalah kemampuan Enumerasi bawaan (dan enum Java juga) sementara kelas kasus tidak secara otomatis mendukung kemampuan tersebut.

Dengan kata lain: "tidak ada cara mudah untuk mendapatkan daftar set total nilai yang disebutkan dengan kelas kasus".

pengguna142435
sumber
5

Jika Anda serius mempertahankan interoperabilitas dengan bahasa JVM lainnya (mis. Java) maka opsi terbaik adalah menulis Java enum. Mereka bekerja secara transparan dari kedua Scala dan kode Java, yang lebih dari yang bisa dikatakan untuk scala.Enumerationatau objek kasus. Mari kita tidak memiliki perpustakaan enumerasi baru untuk setiap proyek hobi baru di GitHub, jika itu bisa dihindari!

Connor Doyle
sumber
4

Saya telah melihat berbagai versi membuat kelas kasus meniru enumerasi. Ini versi saya:

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

Yang memungkinkan Anda untuk membangun kelas kasus yang terlihat seperti berikut:

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

Mungkin seseorang bisa membuat trik yang lebih baik daripada hanya menambahkan kelas kasus masing-masing ke daftar seperti yang saya lakukan. Hanya itu yang bisa saya pikirkan saat itu.

jho
sumber
Mengapa ada dua metode terpisah yang tidak disetujui?
Saish
@Jho saya sudah mencoba untuk mengerjakan solusi Anda apa adanya, tetapi tidak akan dikompilasi. Dalam potongan kode kedua, ada referensi ke Situs di "tipe V = Situs". Saya tidak yakin apa yang dimaksud untuk menghapus kesalahan kompilasi. Selanjutnya, mengapa Anda menyediakan kawat gigi kosong untuk "Mata uang kelas abstrak"? Tidak bisakah mereka ditinggalkan begitu saja? Akhirnya, mengapa Anda menggunakan var di "var values ​​= ..."? Tidakkah ini berarti bahwa klien dapat kapan saja dari mana saja dalam kode memberikan Daftar baru ke nilai? Bukankah jauh lebih baik untuk menjadikannya val daripada var?
chaotic3quilibrium
2

Saya telah bolak-balik pada dua opsi ini beberapa kali terakhir saya membutuhkannya. Hingga baru-baru ini, preferensi saya adalah untuk opsi objek sifat / kasus tersegel.

1) Deklarasi Pencacahan Scala

object OutboundMarketMakerEntryPointType extends Enumeration {
  type OutboundMarketMakerEntryPointType = Value

  val Alpha, Beta = Value
}

2) Ciri Tertutup + Objek Kasus

sealed trait OutboundMarketMakerEntryPointType

case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType

case object BetaEntryPoint extends OutboundMarketMakerEntryPointType

Meskipun tidak satu pun dari ini benar-benar memenuhi semua yang diberikan enumerasi java kepada Anda, di bawah ini adalah pro dan kontra:

Pencacahan Scala

Kelebihan: -Fungsi untuk instantiating dengan opsi atau langsung mengasumsikan akurat (lebih mudah saat memuat dari toko yang persisten) -Iterasi atas semua nilai yang mungkin didukung

Kekurangan: -Peringatan kompilasi untuk pencarian yang tidak lengkap tidak didukung (membuat pencocokan pola kurang ideal)

Objek Kasus / Ciri tertutup

Kelebihan: -Menggunakan ciri-ciri tersegel, kita dapat melakukan pra-instantiasi beberapa nilai sementara yang lain dapat disuntikkan pada waktu pembuatan -Dukungan penuh untuk pencocokan pola (berlaku / metode tidak berlaku didefinisikan)

Cons: -Instantiating dari toko persisten - Anda sering harus menggunakan pencocokan pola di sini atau menentukan daftar Anda sendiri dari semua 'enum values' yang mungkin

Apa yang akhirnya membuat saya mengubah pendapat saya adalah seperti cuplikan berikut:

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
}

The .getpanggilan yang mengerikan - menggunakan pencacahan bukan saya hanya bisa memanggil metode withName pada pencacahan sebagai berikut:

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

Jadi saya pikir preferensi saya ke depan adalah menggunakan Enumerasi ketika nilai-nilai dimaksudkan untuk diakses dari repositori dan objek kasus / sifat tertutup jika tidak.

Anjing gila
sumber
Saya bisa melihat bagaimana pola kode kedua diinginkan (menyingkirkan dua metode pembantu dari pola kode pertama). Namun, saya menemukan cara sedemikian rupa sehingga Anda tidak dipaksa untuk memilih di antara dua pola ini. Saya membahas seluruh domain dalam jawaban yang telah saya
kirim
2

Saya lebih suka case objects(ini masalah preferensi pribadi). Untuk mengatasi masalah yang melekat pada pendekatan itu (parse string dan iterate atas semua elemen), saya telah menambahkan beberapa baris yang tidak sempurna, tetapi efektif.

Saya menempelkan Anda kode di sini mengharapkan itu bisa bermanfaat, dan juga bahwa orang lain dapat memperbaikinya.

/**
 * 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 }
}
jaguililla
sumber
0

Bagi mereka yang masih mencari cara untuk mendapatkan jawaban GatesDa untuk bekerja : Anda bisa mereferensikan objek kasus setelah mendeklarasikannya untuk membuat instance:

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; 
  EUR //THIS IS ONLY CHANGE
  case object GBP extends Currency; GBP //Inline looks better
}
V-Lamp
sumber
0

Saya pikir keuntungan terbesar dari memiliki case classeslebih dari itu enumerationsadalah Anda dapat menggunakan pola tipe class alias ad-hoc polymorphysm . Tidak perlu mencocokkan enum seperti:

someEnum match {
  ENUMA => makeThis()
  ENUMB => makeThat()
}

alih-alih, Anda akan memiliki sesuatu seperti:

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() = ...
}
Murat Mustafin
sumber