วิธีการแบบจำลองประเภทปลอดภัย enum ประเภท?


311

Scala ไม่มีประเภทที่ปลอดภัยenumเหมือน Java ที่มี เมื่อพิจารณาจากค่าคงที่ที่เกี่ยวข้องชุดใดจะเป็นวิธีที่ดีที่สุดในสกาล่าในการเป็นตัวแทนค่าคงที่เหล่านั้น


2
ทำไมไม่ใช้แค่ java enum ล่ะ? นี่เป็นหนึ่งในบางสิ่งที่ฉันยังต้องการใช้ java ธรรมดา
Max

1
ฉันได้เขียนภาพรวมเล็ก ๆ เกี่ยวกับ scala การแจงนับและทางเลือกคุณอาจพบว่ามีประโยชน์: pedrorijo.com/blog/scala-enums/
pedrorijo91

คำตอบ:


187

http://www.scala-lang.org/docu/files/api/scala/Enumeration.html

ตัวอย่างการใช้

  object Main extends App {

    object WeekDay extends Enumeration {
      type WeekDay = Value
      val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
    }
    import WeekDay._

    def isWorkingDay(d: WeekDay) = ! (d == Sat || d == Sun)

    WeekDay.values filter isWorkingDay foreach println
  }

2
ไม่ควรใช้แอปพลิเคชันอย่างจริงจัง มันไม่ได้รับการแก้ไข; มีการแนะนำคลาสใหม่ให้กับแอพซึ่งไม่มีปัญหาที่ Schildmeijer พูดถึง ดังนั้น "object foo ขยาย App {... }" และคุณสามารถเข้าถึงอาร์กิวเมนต์บรรทัดคำสั่งได้ทันทีผ่านตัวแปร args
AmigoNico

scala.Enumeration (ซึ่งเป็นสิ่งที่คุณใช้ในตัวอย่างโค้ด "object WeekDay" ด้านบน) ไม่มีการจับคู่รูปแบบครบถ้วนสมบูรณ์ ผมได้ค้นคว้าศึกษาทุกรูปแบบการแจงนับที่แตกต่างกันในขณะนี้ถูกนำมาใช้ใน Scala และการให้และภาพรวมของพวกเขาในคำตอบนี้ StackOverflow (รวมถึงรูปแบบใหม่ซึ่งให้บริการที่ดีที่สุดของทั้งสอง scala.Enumeration และ "ปิดผนึกลักษณะ + กรณีวัตถุ" รูปแบบ: StackOverflow com / a / 25923651/501113
chaotic3quilibrium

377

ฉันต้องบอกว่าตัวอย่างที่คัดลอกจากเอกสาร Scalaโดยskaffmanด้านบนนั้นมีประโยชน์ จำกัด ในทางปฏิบัติ (คุณอาจใช้เช่นกันcase object)

เพื่อให้ได้สิ่งที่ใกล้เคียงกับ Java มากที่สุดEnum(เช่นมีเหตุผลtoStringและvalueOfวิธีการ - บางทีคุณยังคงยืนยันค่า enum ในฐานข้อมูล) คุณจำเป็นต้องแก้ไขเล็กน้อย หากคุณใช้รหัสของskaffman :

WeekDay.valueOf("Sun") //returns None
WeekDay.Tue.toString   //returns Weekday(2)

ในขณะที่ใช้การประกาศต่อไปนี้:

object WeekDay extends Enumeration {
  type WeekDay = Value
  val Mon = Value("Mon")
  val Tue = Value("Tue") 
  ... etc
}

คุณจะได้ผลลัพธ์ที่สมเหตุสมผลมากขึ้น:

WeekDay.valueOf("Sun") //returns Some(Sun)
WeekDay.Tue.toString   //returns Tue

7
Btw ตอนนี้วิธี valueOf ตายแล้ว :-(
greenoldman

36
การเปลี่ยน @macias valueOfคือwithNameซึ่งไม่ส่งคืนตัวเลือกและส่ง NSE หากไม่มีการแข่งขัน อะไรกัน!
Bluu

6
@Bluu คุณสามารถเพิ่ม valueOf ด้วยตัวคุณเอง: def valueOf (ชื่อ: String) = WeekDay.values.find (_. toString == name) เพื่อมีตัวเลือก
centr

@centr เมื่อฉันพยายามที่จะสร้างMap[Weekday.Weekday, Long]และเพิ่มมูลค่าพูดMonกับมันคอมไพเลอร์โยนข้อผิดพลาดประเภทที่ไม่ถูกต้อง คาดว่าจะเป็นวันธรรมดา ๆ ทุกวันพบว่าคุ้มค่า? ทำไมสิ่งนี้ถึงเกิดขึ้น
Sohaib

@Sohaib มันควรเป็น Map [Weekday.Value, Long]
centr

98

มีหลายวิธีในการทำ

1) ใช้สัญลักษณ์ มันจะไม่ให้ความปลอดภัยใด ๆ แก่คุณยกเว้นว่าจะไม่ยอมรับสัญลักษณ์ที่ไม่ใช่สัญลักษณ์ที่คาดว่าจะเป็น ฉันแค่พูดถึงที่นี่เพื่อความสมบูรณ์ นี่คือตัวอย่างการใช้งาน:

def update(what: Symbol, where: Int, newValue: Array[Int]): MatrixInt =
  what match {
    case 'row => replaceRow(where, newValue)
    case 'col | 'column => replaceCol(where, newValue)
    case _ => throw new IllegalArgumentException
  }

// At REPL:   
scala> val a = unitMatrixInt(3)
a: teste7.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 0 1 /

scala> a('row, 1) = a.row(0)
res41: teste7.MatrixInt =
/ 1 0 0 \
| 1 0 0 |
\ 0 0 1 /

scala> a('column, 2) = a.row(0)
res42: teste7.MatrixInt =
/ 1 0 1 \
| 0 1 0 |
\ 0 0 0 /

2) ใช้คลาสEnumeration:

object Dimension extends Enumeration {
  type Dimension = Value
  val Row, Column = Value
}

หรือหากคุณต้องการทำให้เป็นอันดับหรือแสดง:

object Dimension extends Enumeration("Row", "Column") {
  type Dimension = Value
  val Row, Column = Value
}

สิ่งนี้สามารถใช้ดังนี้:

def update(what: Dimension, where: Int, newValue: Array[Int]): MatrixInt =
  what match {
    case Row => replaceRow(where, newValue)
    case Column => replaceCol(where, newValue)
  }

// At REPL:
scala> a(Row, 2) = a.row(1)
<console>:13: error: not found: value Row
       a(Row, 2) = a.row(1)
         ^

scala> a(Dimension.Row, 2) = a.row(1)
res1: teste.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 1 0 /

scala> import Dimension._
import Dimension._

scala> a(Row, 2) = a.row(1)
res2: teste.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 1 0 /

น่าเสียดายที่มันไม่ได้รับประกันว่าจะมีการแข่งขันทั้งหมด ถ้าฉันลืมใส่ Row หรือ Column ในการแข่งขันคอมไพเลอร์ Scala จะไม่เตือนฉัน ดังนั้นมันจึงทำให้ผมบางความปลอดภัยประเภท แต่ไม่มากเท่าที่จะได้รับ

3) วัตถุกรณี:

sealed abstract class Dimension
case object Row extends Dimension
case object Column extends Dimension

ตอนนี้ถ้าฉันออกจากกรณีในmatchคอมไพเลอร์จะเตือนฉัน:

MatrixInt.scala:70: warning: match is not exhaustive!
missing combination         Column

    what match {
    ^
one warning found

มันใช้วิธีเดียวกันและไม่จำเป็นต้องimport:

scala> val a = unitMatrixInt(3)
a: teste3.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 0 1 /

scala> a(Row,2) = a.row(0)
res15: teste3.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 1 0 0 /

คุณอาจสงสัยว่าทำไมต้องใช้การแจงนับแทนที่จะเป็นวัตถุเคส ตามความเป็นจริงวัตถุเคสมีข้อดีหลายครั้งเช่นที่นี่ แม้ว่าคลาส Enumeration จะมีวิธีการรวบรวมมากมายเช่นองค์ประกอบ (ตัววนซ้ำบน Scala 2.8) ซึ่งจะส่งกลับค่า Iterator, แผนที่, flatMap, ตัวกรอง ฯลฯ

คำตอบนี้เป็นส่วนที่เลือกจากบทความนี้ในบล็อกของฉัน


"... ไม่ยอมรับสัญลักษณ์ที่ไม่ใช่สัญลักษณ์ที่คาดว่าจะได้"> ฉันเดาว่าหมายความว่าSymbolอินสแตนซ์ไม่สามารถมีช่องว่างหรืออักขระพิเศษได้ คนส่วนใหญ่เมื่อเผชิญหน้ากับSymbolชั้นเรียนครั้งแรกอาจคิดอย่างนั้น แต่จริง ๆ แล้วไม่ถูกต้อง Symbol("foo !% bar -* baz")รวบรวมและทำงานได้อย่างสมบูรณ์แบบ กล่าวอีกนัยหนึ่งคุณสามารถสร้างSymbolอินสแตนซ์ที่ล้อมรอบสตริงใด ๆ ได้อย่างสมบูรณ์(คุณไม่สามารถทำได้ด้วยการซินแท็กซ์น้ำตาลเดี่ยว "coma") สิ่งเดียวที่Symbolรับประกันได้คือความเป็นเอกลักษณ์ของสัญลักษณ์ที่กำหนดทำให้การเปรียบเทียบและจับคู่ได้เร็วขึ้นเล็กน้อย
Régis Jean-Gilles

@ RégisJean-Gilles No ฉันหมายความว่าคุณไม่สามารถส่ง a Stringยกตัวอย่างเช่นเป็นอาร์กิวเมนต์ของSymbolพารามิเตอร์
Daniel C. Sobral

ใช่ฉันเข้าใจส่วนนั้น แต่มันก็เป็นจุดที่สงสัยหากคุณแทนที่Stringด้วยคลาสอื่นซึ่งโดยทั่วไปแล้วเป็น wrapper รอบ ๆ สตริงและสามารถแปลงได้อย่างอิสระในทั้งสองทิศทาง (ตามกรณีSymbol) ฉันเดาว่านั่นคือสิ่งที่คุณหมายถึงเมื่อพูดว่า "มันจะไม่ให้ความปลอดภัยใด ๆ กับคุณ" แต่ก็ไม่ชัดเจนนักเนื่องจาก OP ได้ขอวิธีแก้ปัญหาอย่างปลอดภัยสำหรับประเภท ฉันไม่แน่ใจว่าในขณะที่เขียนคุณรู้ว่าไม่เพียง แต่มันจะไม่ปลอดภัยเพราะสิ่งเหล่านั้นไม่ได้ enums เลย แต่ยัง Symbolไม่รับประกันว่าอาร์กิวเมนต์ที่ผ่านจะไม่มีตัวอักษรพิเศษ
Régis Jean-Gilles

1
หากต้องการอธิบายอย่างละเอียดเมื่อคุณพูดว่า "ไม่ยอมรับสัญลักษณ์ที่ไม่ใช่สัญลักษณ์ซึ่งคาดว่าจะเป็นสัญลักษณ์" คุณสามารถอ่านได้ว่า "ไม่ยอมรับค่าที่ไม่ใช่อินสแตนซ์ของ Symbol" (ซึ่งเห็นได้ชัดว่าเป็นจริง) หรือ "ไม่ยอมรับค่าที่ไม่ใช่ ระบุเหมือนธรรมดาสตริง aka 'สัญลักษณ์'"(ซึ่งไม่เป็นความจริงและเป็นความเข้าใจผิดว่าใครสวยมากมีเป็นครั้งแรกที่เราพบสัญลักษณ์สกาล่าเนื่องจากความจริงที่ว่าพบครั้งแรกคือว่าพิเศษ'fooสัญกรณ์ซึ่งไม่ดักคอ สตริงที่ไม่ใช่ตัวระบุ) นี่คือความเข้าใจผิดที่ฉันต้องการปัดเป่าสำหรับผู้อ่านในอนาคตใด ๆ
Régis Jean-Gilles

@ RégisJean-Gilles ฉันหมายถึงอดีตซึ่งเป็นความจริงที่เห็นได้ชัด ฉันหมายถึงมันเป็นความจริงที่ชัดเจนสำหรับทุกคนที่เคยพิมพ์แบบสแตติก เมื่อก่อนมีการพูดคุยกันมากมายเกี่ยวกับข้อดีของการพิมพ์แบบคงที่และ "ไดนามิก" และผู้คนจำนวนมากที่สนใจใน Scala มาจากพื้นหลังการพิมพ์แบบไดนามิกดังนั้นฉันคิดว่ามันไม่ได้ไปโดยไม่พูดอะไร ฉันจะไม่คิดถึงคำพูดนั้นในทุกวันนี้ โดยส่วนตัวฉันคิดว่าสัญลักษณ์ของสกาล่านั้นน่าเกลียดและซ้ำซ้อนและไม่เคยใช้เลย ฉันยกระดับความคิดเห็นล่าสุดของคุณเนื่องจากเป็นจุดที่ดี
Daniel C. Sobral

52

วิธี verbose น้อยกว่าเล็กน้อยในการประกาศ enumerations ชื่อ:

object WeekDay extends Enumeration("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") {
  type WeekDay = Value
  val Sun, Mon, Tue, Wed, Thu, Fri, Sat = Value
}

WeekDay.valueOf("Wed") // returns Some(Wed)
WeekDay.Fri.toString   // returns Fri

แน่นอนปัญหาที่นี่คือคุณจะต้องเรียงลำดับของชื่อและ vals ให้ตรงกันซึ่งจะทำได้ง่ายขึ้นหากประกาศชื่อและ val ในบรรทัดเดียวกัน


11
สิ่งนี้ดูสะอาดตาตั้งแต่แรกเห็น แต่มีข้อเสียคือต้องให้ผู้ดูแลรักษารายการอื่น ๆ ให้ตรงกัน สำหรับตัวอย่างของวันในสัปดาห์ก็ไม่น่าจะเกิดขึ้น แต่โดยทั่วไปแล้วค่าใหม่สามารถแทรกหรือลบออกหนึ่งรายการและทั้งสองรายการอาจไม่ซิงค์กันซึ่งในกรณีนี้อาจมีข้อบกพร่องเล็กน้อย
Brent Faust

1
ตามความคิดเห็นก่อนหน้านี้ความเสี่ยงคือรายการที่แตกต่างกันสองรายการที่ไม่มีการซิงค์ แม้ว่ามันจะไม่เป็นปัญหาสำหรับตัวอย่างเล็ก ๆ ในปัจจุบันของคุณหากมีสมาชิกจำนวนมาก (เช่นในหลายสิบถึงหลายร้อย) อัตราต่อรองของทั้งสองรายการจะหายไปจากการซิงค์อย่างเงียบ ๆ นอกจากนี้ scala การคำนวณไม่สามารถได้รับประโยชน์จากการรวบรวมคำเตือน / ข้อผิดพลาดของการจับคู่รูปแบบเวลาแบบละเอียดของ Scala ฉันได้สร้างคำตอบ StackOverflow ซึ่งมีโซลูชันที่ดำเนินการตรวจสอบรันไทม์เพื่อให้แน่ใจว่าทั้งสองรายการยังคงซิงค์กัน: stackoverflow.com/a/25923651/501113
chaotic3quilibrium

17

คุณสามารถใช้คลาสนามธรรมที่ปิดผนึกแทนการแจงนับตัวอย่างเช่น:

sealed abstract class Constraint(val name: String, val verifier: Int => Boolean)

case object NotTooBig extends Constraint("NotTooBig", (_ < 1000))
case object NonZero extends Constraint("NonZero", (_ != 0))
case class NotEquals(x: Int) extends Constraint("NotEquals " + x, (_ != x))

object Main {

  def eval(ctrs: Seq[Constraint])(x: Int): Boolean =
    (true /: ctrs){ case (accum, ctr) => accum && ctr.verifier(x) }

  def main(args: Array[String]) {
    val ctrs = NotTooBig :: NotEquals(5) :: Nil
    val evaluate = eval(ctrs) _

    println(evaluate(3000))
    println(evaluate(3))
    println(evaluate(5))
  }

}

ลักษณะที่ปิดผนึกด้วยวัตถุเคสก็เป็นไปได้เช่นกัน
Ashalynd

2
รูปแบบ "วัตถุลักษณะ + กรณีที่ปิดผนึก" มีปัญหาที่ฉันให้รายละเอียดในคำตอบ StackOverflow อย่างไรก็ตามฉันคิดออกว่าจะแก้ไขปัญหาทั้งหมดที่เกี่ยวข้องกับรูปแบบนี้ได้อย่างไรซึ่งครอบคลุมในเธรด: stackoverflow.com/a/25923651/501113
chaotic3quilibrium


2

หลังจากทำวิจัยอย่างกว้างขวางเกี่ยวกับตัวเลือกทั้งหมดที่อยู่รอบ ๆ "enumerations" ใน Scala ผมโพสต์ภาพรวมที่สมบูรณ์มากขึ้นของโดเมนนี้อีกด้าย StackOverflow มันมีวิธีแก้ปัญหากับรูปแบบ "ลักษณะนิสัย + กรณีวัตถุ" ที่ฉันได้แก้ไขปัญหาการสั่งซื้อการเริ่มต้นคลาส / วัตถุ JVM



1

ใน Scala สะดวกสบายมากกับhttps://github.com/lloydmeta/enumeratum

โครงการดีมากพร้อมตัวอย่างและเอกสารประกอบ

เพียงแค่ตัวอย่างจากเอกสารของพวกเขาควรทำให้คุณสนใจ

import enumeratum._

sealed trait Greeting extends EnumEntry

object Greeting extends Enum[Greeting] {

  /*
   `findValues` is a protected method that invokes a macro to find all `Greeting` object declarations inside an `Enum`

   You use it to implement the `val values` member
  */
  val values = findValues

  case object Hello   extends Greeting
  case object GoodBye extends Greeting
  case object Hi      extends Greeting
  case object Bye     extends Greeting

}

// Object Greeting has a `withName(name: String)` method
Greeting.withName("Hello")
// => res0: Greeting = Hello

Greeting.withName("Haro")
// => java.lang.IllegalArgumentException: Haro is not a member of Enum (Hello, GoodBye, Hi, Bye)

// A safer alternative would be to use `withNameOption(name: String)` method which returns an Option[Greeting]
Greeting.withNameOption("Hello")
// => res1: Option[Greeting] = Some(Hello)

Greeting.withNameOption("Haro")
// => res2: Option[Greeting] = None

// It is also possible to use strings case insensitively
Greeting.withNameInsensitive("HeLLo")
// => res3: Greeting = Hello

Greeting.withNameInsensitiveOption("HeLLo")
// => res4: Option[Greeting] = Some(Hello)

// Uppercase-only strings may also be used
Greeting.withNameUppercaseOnly("HELLO")
// => res5: Greeting = Hello

Greeting.withNameUppercaseOnlyOption("HeLLo")
// => res6: Option[Greeting] = None

// Similarly, lowercase-only strings may also be used
Greeting.withNameLowercaseOnly("hello")
// => res7: Greeting = Hello

Greeting.withNameLowercaseOnlyOption("hello")
// => res8: Option[Greeting] = Some(Hello)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.