Làm thế nào để mô hình các loại enum an toàn?


311

Scala không có loại an toàn enumnhư Java có. Đưa ra một tập hợp các hằng số liên quan, cách tốt nhất trong Scala để đại diện cho các hằng số đó là gì?


2
Tại sao không sử dụng java enum? Đây là một trong số ít những điều tôi vẫn thích sử dụng java đơn giản.
Tối đa

1
Tôi đã viết một tổng quan nhỏ về Bảng liệt kê và các lựa chọn thay thế, bạn có thể thấy nó hữu ích: pedrorijo.com/blog/scala-enums/
pedrorijo91

Câu trả lời:


187

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

Ví dụ sử dụng

  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
Nghiêm túc, ứng dụng không nên được sử dụng. Nó KHÔNG được sửa; Một lớp mới, Ứng dụng, đã được giới thiệu, không có vấn đề mà Schildmeijer đã đề cập. Do đó, "object foo mở rộng Ứng dụng {...}" Và bạn có quyền truy cập ngay vào các đối số dòng lệnh thông qua biến args.
AmigoNico

scala.Enumutions (là những gì bạn đang sử dụng trong mẫu mã "đối tượng WeekDay" ở trên) không cung cấp kết hợp mẫu đầy đủ. Tôi đã nghiên cứu tất cả các kiểu liệt kê khác nhau hiện đang được sử dụng trong Scala và đưa ra và tổng quan về chúng trong câu trả lời StackOverflow này (bao gồm một mẫu mới cung cấp tốt nhất cả scala.Enumutions và mẫu "đối tượng bịt kín + trường hợp": stackoverflow. com / a / 25923651/501113
hỗn loạn3quilibrium

377

Tôi phải nói rằng ví dụ được sao chép từ tài liệu Scala của skaffman ở trên là tiện ích hạn chế trong thực tế (bạn cũng có thể sử dụng case objects).

Để có được một cái gì đó gần giống với Java nhất Enum(nghĩa là với các phương thức toStringvalueOfphương thức hợp lý - có lẽ bạn đang duy trì các giá trị enum cho cơ sở dữ liệu), bạn cần sửa đổi nó một chút. Nếu bạn đã sử dụng mã của skaffman :

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

Trong khi sử dụng khai báo sau:

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

Bạn nhận được kết quả hợp lý hơn:

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

7
Btw. Phương thức valueOf hiện đã chết :-(
greenoldman

36
Sự valueOfthay thế của @macias là withName, không trả lại Tùy chọn và ném NSE nếu không có kết quả khớp. Cái gì!
Bluu

6
@Bluu Bạn có thể tự thêm valueOf: def valueOf (name: String) = WeekDay.values.find (_. ToString == name) để có Tùy chọn
centr

@centr Khi tôi cố gắng tạo Map[Weekday.Weekday, Long]và thêm một giá trị Mon, trình biên dịch sẽ đưa ra một lỗi loại không hợp lệ. Ngày dự kiến. Ngày tìm thấy giá trị? Lý do tại sao điều này xảy ra?
Sohaib

@Sohaib Nó phải là Bản đồ [Weekday.Value, Long].
centr

99

Có nhiều cách làm.

1) Sử dụng các ký hiệu. Tuy nhiên, nó sẽ không cung cấp cho bạn bất kỳ loại an toàn nào, ngoài việc không chấp nhận các biểu tượng không phải là biểu tượng được mong đợi. Tôi chỉ đề cập đến nó ở đây cho đầy đủ. Đây là một ví dụ về việc sử dụng:

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) Sử dụng lớp Enumeration:

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

hoặc, nếu bạn cần nối tiếp hoặc hiển thị nó:

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

Điều này có thể được sử dụng như thế này:

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 /

Thật không may, nó không đảm bảo rằng tất cả các trận đấu được tính. Nếu tôi quên đặt Row hoặc Cột trong trận đấu, trình biên dịch Scala sẽ không cảnh báo tôi. Vì vậy, nó mang lại cho tôi một số loại an toàn, nhưng không nhiều như có thể đạt được.

3) Đối tượng trường hợp:

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

Bây giờ, nếu tôi bỏ qua một trường hợp trên a match, trình biên dịch sẽ cảnh báo tôi:

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

    what match {
    ^
one warning found

Nó được sử dụng khá nhiều theo cùng một cách, và thậm chí không cần 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 /

Sau đó, bạn có thể tự hỏi tại sao lại sử dụng Bảng liệt kê thay vì các đối tượng trường hợp. Như một vấn đề của thực tế, các đối tượng trường hợp có lợi thế nhiều lần, chẳng hạn như ở đây. Mặc dù vậy, lớp Enumutions có nhiều phương thức Collection, chẳng hạn như các phần tử (iterator trên Scala 2.8), trả về một Iterator, map, FlatMap, bộ lọc, v.v.

Câu trả lời này về cơ bản là một phần được chọn từ bài viết này trong blog của tôi.


"... không chấp nhận các biểu tượng không phải là biểu tượng được mong đợi"> Tôi đoán bạn có nghĩa là các Symbolthể hiện không thể có khoảng trắng hoặc ký tự đặc biệt. Hầu hết mọi người khi lần đầu tiên bắt gặp Symbollớp học có thể nghĩ như vậy, nhưng thực sự không chính xác. Symbol("foo !% bar -* baz")biên dịch và chạy hoàn toàn tốt. Nói cách khác, bạn hoàn toàn có thể tạo ra các Symboltrường hợp bao bọc bất kỳ chuỗi (bạn không thể thực hiện nó với đường cú pháp "hôn mê đơn"). Điều duy nhất Symbolđảm bảo là tính duy nhất của bất kỳ biểu tượng nào, làm cho nó nhanh hơn một chút để so sánh và so sánh.
Régis Jean-Gilles

@ RégisJean-Gilles Không, ý tôi là bạn không thể truyền a String, ví dụ, làm đối số cho một Symboltham số.
Daniel C. Sobral

Vâng, tôi đã hiểu phần đó, nhưng đó là một điểm khá thú vị nếu bạn thay thế String bằng một lớp khác về cơ bản là một trình bao bọc xung quanh một chuỗi và có thể được chuyển đổi tự do theo cả hai hướng (như trường hợp Symbol). Tôi đoán đó là những gì bạn muốn nói khi nói "Nó sẽ không mang lại cho bạn bất kỳ loại an toàn nào", điều đó không rõ ràng khi OP yêu cầu rõ ràng về các giải pháp an toàn loại. Tôi không chắc tại thời điểm viết bài bạn có biết rằng nó không chỉ không an toàn vì những thứ đó không phải là enum, mà còn Symbol không đảm bảo rằng đối số được thông qua sẽ không có ký tự đặc biệt.
Régis Jean-Gilles

1
Để giải thích, khi bạn nói "không chấp nhận các biểu tượng không có biểu tượng được mong đợi", thì có thể được đọc là "không chấp nhận các giá trị không phải là trường hợp của Biểu tượng" (rõ ràng là đúng) hoặc "không chấp nhận các giá trị không phải là Các chuỗi giống như định danh đơn giản, còn gọi là 'biểu tượng' "(không đúng và là một quan niệm sai lầm rằng hầu như bất kỳ ai cũng có lần đầu tiên chúng ta bắt gặp các biểu tượng scala, do thực tế rằng lần gặp đầu tiên là mặc dù 'fooký hiệu đặc biệt không loại trừ chuỗi không định danh). Đây là quan niệm sai lầm mà tôi muốn xua tan cho bất kỳ độc giả nào trong tương lai.
Régis Jean-Gilles

@ RégisJean-Gilles Ý tôi là cái trước đây, cái rõ ràng là đúng. Ý tôi là, nó rõ ràng đúng với bất cứ ai đã từng gõ tĩnh. Trước đó, có rất nhiều cuộc thảo luận về giá trị tương đối của kiểu gõ tĩnh và "động", và rất nhiều người quan tâm đến Scala đến từ một nền tảng gõ động, vì vậy tôi nghĩ rằng nó không đi mà không nói. Tôi thậm chí sẽ không nghĩ đến việc đưa ra nhận xét đó ngày nay. Cá nhân, tôi nghĩ Biểu tượng của Scala là xấu xí và dư thừa, và không bao giờ sử dụng nó. Tôi ủng hộ bình luận cuối cùng của bạn, vì đó là một điểm tốt.
Daniel C. Sobral

52

Một cách khai báo hơi dài dòng hơn một chút:

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

Tất nhiên vấn đề ở đây là bạn sẽ cần phải giữ trật tự của tên và vals đồng bộ, điều này dễ thực hiện hơn nếu tên và val được khai báo trên cùng một dòng.


11
Điều này thoạt nhìn có vẻ sạch sẽ hơn, nhưng có nhược điểm là yêu cầu người bảo trì giữ đồng bộ hóa cả hai danh sách. Đối với các ngày trong tuần, nó không xuất hiện. Nhưng nói chung, một giá trị mới có thể được chèn hoặc xóa một và hai danh sách có thể không đồng bộ, trong trường hợp đó, các lỗi tinh vi có thể được đưa ra.
Brent Faust

1
Theo nhận xét trước, rủi ro là hai danh sách khác nhau có thể âm thầm không đồng bộ. Mặc dù đây không phải là vấn đề đối với ví dụ nhỏ hiện tại của bạn, nhưng nếu có thêm nhiều thành viên (như trong hàng chục đến hàng trăm), tỷ lệ của hai danh sách âm thầm không đồng bộ sẽ cao hơn đáng kể. Ngoài ra scala.Enumutions không thể hưởng lợi từ các cảnh báo / lỗi phù hợp với mô hình toàn diện thời gian biên dịch của Scala. Tôi đã tạo một câu trả lời StackOverflow chứa giải pháp thực hiện kiểm tra thời gian chạy để đảm bảo hai danh sách vẫn được đồng bộ hóa: stackoverflow.com/a/25923651/501113
chaotic3quilibrium

17

Bạn có thể sử dụng một lớp trừu tượng kín thay vì liệt kê, ví dụ:

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

}

Đặc điểm kín với các đối tượng trường hợp cũng là một khả năng.
Ashalynd

2
Mẫu "đặc điểm niêm phong + đối tượng trường hợp" có các vấn đề mà tôi mô tả chi tiết trong câu trả lời StackOverflow. Tuy nhiên, tôi đã tìm ra cách giải quyết tất cả các vấn đề liên quan đến mẫu này cũng được đề cập trong chuỗi: stackoverflow.com/a/25923651/501113
chaotic3quilibrium

7

vừa phát hiện ra liệt kê . nó khá tuyệt vời và cũng tuyệt vời không kém nó được biết đến nhiều hơn!


2

Sau khi thực hiện nghiên cứu sâu rộng về tất cả các tùy chọn xung quanh "bảng liệt kê" trong Scala, tôi đã đăng một tổng quan đầy đủ hơn về miền này trên một luồng StackOverflow khác . Nó bao gồm một giải pháp cho mẫu "đối tượng đặc trưng + trường hợp được niêm phong" trong đó tôi đã giải quyết vấn đề sắp xếp khởi tạo lớp / đối tượng JVM.



1

Trong Scala, nó rất thoải mái với https://github.com/lloydmeta/enumeratum

Dự án thực sự tốt với các ví dụ và tài liệu

Chỉ ví dụ này từ tài liệu của họ sẽ khiến bạn quan tâm đến

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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.