Các đối tượng trường hợp và liệt kê trong Scala


231

Có hướng dẫn thực hành tốt nhất nào về thời điểm sử dụng các lớp tình huống (hoặc đối tượng trường hợp) so với việc mở rộng Bảng liệt kê trong Scala không?

Họ dường như cung cấp một số lợi ích tương tự.


2
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

1
Xem thêm Scala 3 dựa trên Dottyenum (cho giữa năm 2020).
VonC

Câu trả lời:


223

Một sự khác biệt lớn là nó Enumerationđi kèm với sự hỗ trợ để khởi tạo chúng từ một số nameChuỗi. Ví dụ:

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

Sau đó, bạn có thể làm:

val ccy = Currency.withName("EUR")

Điều này hữu ích khi muốn duy trì liệt kê (ví dụ: vào cơ sở dữ liệu) hoặc tạo chúng từ dữ liệu cư trú trong các tệp. Tuy nhiên, tôi thấy nói chung rằng việc liệt kê là một chút vụng về trong Scala và có cảm giác về một tiện ích bổ sung vụng về, vì vậy bây giờ tôi có xu hướng sử dụng case objects. A case objectlinh hoạt hơn enum:

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

case class UnknownCurrency(name: String) extends Currency

Vì vậy, bây giờ tôi có lợi thế của ...

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

Như @ chaotic3quilibrium đã chỉ ra (với một số chỉnh sửa để dễ đọc):

Về mẫu "UnknownCurrency (mã)", có nhiều cách khác để xử lý việc không tìm chuỗi mã tiền tệ hơn là "phá vỡ" bản chất đóng của Currencyloại. UnknownCurrencyhiện thuộc loại Currencycó thể lẻn vào các phần khác của API.

Bạn nên đẩy trường hợp đó ra bên ngoài Enumerationvà khiến khách hàng xử lý một Option[Currency]loại có thể chỉ ra rõ ràng thực sự có vấn đề phù hợp và "khuyến khích" người dùng API sắp xếp nó ra.

Để theo dõi các câu trả lời khác ở đây, nhược điểm chính của case objects over Enumerations là:

  1. Không thể lặp lại tất cả các trường hợp của "liệt kê" . Đây chắc chắn là trường hợp, nhưng tôi thấy nó cực kỳ hiếm trong thực tế rằng điều này là bắt buộc.

  2. Không thể khởi tạo dễ dàng từ giá trị bền vững . Điều này cũng đúng nhưng, ngoại trừ trong trường hợp liệt kê rất lớn (ví dụ: tất cả các loại tiền tệ), điều này không có chi phí quá lớn.


10
Sự khác biệt khác là Enumulation enum được ra khỏi hộp, trong khi trường hợp đối tượng dựa trên enum đáng ghét không
om-nom-nom

1
Một điểm khác cho các đối tượng trường hợp là nếu bạn quan tâm đến khả năng tương tác của java. Enumutions sẽ trả về các giá trị là Enumutions.Value, do đó 1) yêu cầu thư viện scala, 2) mất thông tin loại thực tế.
juanmirocks

7
@oxbow_lakes Về điểm 1, cụ thể là phần này "... Tôi đã thấy nó cực kỳ hiếm trong thực tế rằng điều này là bắt buộc": Rõ ràng bạn hiếm khi làm nhiều công việc UI. Đây là một trường hợp sử dụng cực kỳ phổ biến; hiển thị danh sách (thả xuống) của các thành viên liệt kê hợp lệ để chọn.
hỗn loạn3quilibrium

Tôi không hiểu loại vật phẩm được ghép trade.ccytrong mẫu đặc điểm kín.
rloth

và không case objecttạo ra dấu chân mã (~ 4x) lớn hơn Enumeration? Phân biệt hữu ích đặc biệt cho scala.jscác dự án cần dấu chân nhỏ.
ecoe

69

CẬP NHẬT: Một giải pháp dựa trên vĩ mô mới đã được tạo ra vượt trội hơn nhiều so với giải pháp tôi phác thảo dưới đây. Tôi thực sự khuyên bạn nên sử dụng giải pháp dựa trên macro mới này . Và nó xuất hiện kế hoạch cho Dotty sẽ làm cho phong cách giải pháp enum này trở thành một phần của ngôn ngữ. Ôi trời ơi!

Tóm tắt:
Có ba mẫu cơ bản để cố gắng tái tạo Java Enumtrong một dự án Scala. Hai trong ba mẫu; trực tiếp sử dụng Java Enumscala.Enumeration, không có khả năng cho phép khớp mẫu đầy đủ của Scala. Và cái thứ ba; "đặc điểm kín + đối tượng trường hợp", không ... nhưng có các biến chứng khởi tạo lớp / đối tượng JVM dẫn đến việc tạo ra chỉ mục thứ tự không nhất quán.

Tôi đã tạo ra một giải pháp với hai lớp; Bảng liệt kê và bảng liệt kê Được đặt trong Gist này . Tôi đã không đăng mã vào chủ đề này vì tệp cho phép liệt kê khá lớn (+400 dòng - chứa rất nhiều ý kiến ​​giải thích bối cảnh thực hiện).

Chi tiết:
Câu hỏi bạn đặt ra khá chung chung; "... Khi nào nên sử dụng casecác lớpobjects so với mở rộng [scala.]Enumeration". Và hóa ra có NHIỀU câu trả lời có thể có, mỗi câu trả lời tùy thuộc vào sự tinh tế của các yêu cầu dự án cụ thể mà bạn có. Câu trả lời có thể được giảm xuống thành ba mẫu cơ bản.

Để bắt đầu, hãy chắc chắn rằng chúng ta đang làm việc từ cùng một ý tưởng cơ bản về việc liệt kê là gì. Chúng ta hãy định nghĩa một phép liệt kê chủ yếu theo các điều khoản Enumđược cung cấp kể từ Java 5 (1.5) :

  1. Nó chứa một tập hợp các thành viên được đặt tên tự nhiên
    1. Có một số lượng thành viên cố định
    2. Các thành viên được sắp xếp tự nhiên và được lập chỉ mục rõ ràng
      • Trái ngược với việc được sắp xếp dựa trên một số tiêu chí kiểm tra thành viên bẩm sinh
    3. Mỗi thành viên có một tên duy nhất trong tổng số tất cả các thành viên
  2. Tất cả các thành viên có thể dễ dàng được lặp lại thông qua các chỉ số của họ
  3. Một thành viên có thể được lấy với tên (phân biệt chữ hoa chữ thường)
    1. Sẽ rất tuyệt nếu một thành viên cũng có thể được lấy với tên không nhạy cảm trường hợp của nó
  4. Một thành viên có thể được lấy với chỉ mục của nó
  5. Thành viên có thể dễ dàng, minh bạch và sử dụng hiệu quả tuần tự hóa
  6. Thành viên có thể dễ dàng được mở rộng để giữ dữ liệu độc thân liên quan bổ sung
  7. Suy nghĩ vượt ra ngoài Java Enum, thật tuyệt khi có thể tận dụng một cách rõ ràng việc tận dụng mô hình phù hợp với mô hình của Scala để kiểm tra mức độ liệt kê

Tiếp theo, chúng ta hãy xem xét các phiên bản rút gọn của ba mẫu giải pháp phổ biến nhất được đăng:

A) Thực tế trực tiếp sử dụng mẫu JavaEnum (trong một dự án Scala / Java hỗn hợp):

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

Các mục sau đây từ định nghĩa liệt kê không có sẵn:

  1. 3.1 - Sẽ rất tuyệt nếu một thành viên cũng có thể được truy xuất với tên không nhạy cảm trường hợp của nó
  2. 7 - Suy nghĩ vượt ra ngoài Enum của Java, thật tuyệt khi có thể tận dụng một cách rõ ràng việc tận dụng mô hình phù hợp với mô hình của Scala để kiểm tra mức liệt kê

Đối với các dự án hiện tại của tôi, tôi không có lợi ích khi chấp nhận rủi ro xung quanh lộ trình dự án hỗn hợp Scala / Java. Và ngay cả khi tôi có thể chọn thực hiện một dự án hỗn hợp, mục 7 rất quan trọng để cho phép tôi nắm bắt các vấn đề về thời gian biên dịch nếu / khi tôi thêm / xóa thành viên liệt kê hoặc viết một số mã mới để xử lý các thành viên liệt kê hiện có.


B) Sử dụng mẫu " 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}
}

Các mục sau đây từ định nghĩa liệt kê không có sẵn:

  1. 1.2 - Thành viên được sắp xếp tự nhiên và được lập chỉ mục rõ ràng
  2. 2 - Tất cả các thành viên có thể dễ dàng được lặp lại thông qua các chỉ mục của họ
  3. 3 - Một thành viên có thể được lấy với tên (phân biệt chữ hoa chữ thường)
  4. 3.1 - Sẽ rất tuyệt nếu một thành viên cũng có thể được truy xuất với tên không nhạy cảm trường hợp của nó
  5. 4 - Một thành viên có thể được lấy với chỉ mục của nó

Người ta cho rằng nó thực sự đáp ứng các mục định nghĩa liệt kê 5 và 6. Đối với 5, đây là một sự căng thẳng để khẳng định nó hiệu quả. Đối với 6, thật không dễ dàng để mở rộng để chứa thêm dữ liệu đơn lẻ liên quan.


C) Sử dụng scala.Enumerationmẫu (lấy cảm hứng từ câu trả lời StackOverflow này ):

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

Các mục sau đây từ định nghĩa liệt kê không có sẵn (tình cờ giống hệt với danh sách để sử dụng trực tiếp Enum Java):

  1. 3.1 - Sẽ rất tuyệt nếu một thành viên cũng có thể được truy xuất với tên không nhạy cảm trường hợp của nó
  2. 7 - Suy nghĩ vượt ra ngoài Enum của Java, thật tuyệt khi có thể tận dụng một cách rõ ràng việc tận dụng mô hình phù hợp với mô hình của Scala để kiểm tra mức liệt kê

Một lần nữa cho các dự án hiện tại của tôi, mục 7 rất quan trọng trong việc cho phép tôi nắm bắt các vấn đề về thời gian biên dịch nếu / khi tôi thêm / xóa thành viên liệt kê hoặc đang viết một số mã mới để xử lý các thành viên liệt kê hiện có.


Vì vậy, với định nghĩa trên về liệt kê, không có giải pháp nào trong ba giải pháp trên hoạt động vì chúng không cung cấp mọi thứ được nêu trong định nghĩa liệt kê ở trên:

  1. Java Enum trực tiếp trong một dự án Scala / Java hỗn hợp
  2. "đặc điểm kín + đối tượng trường hợp"
  3. scala.Enumutions

Mỗi giải pháp này cuối cùng có thể được làm lại / mở rộng / tái cấu trúc để cố gắng đáp ứng một số yêu cầu còn thiếu của mỗi người. Tuy nhiên, cả Java Enumvà các scala.Enumerationgiải pháp đều không thể được mở rộng đủ để cung cấp mục 7. Và đối với các dự án của riêng tôi, đây là một trong những giá trị hấp dẫn hơn khi sử dụng loại đóng trong Scala. Tôi đặc biệt thích các cảnh báo / lỗi thời gian biên dịch để chỉ ra rằng tôi có một khoảng trống / vấn đề trong mã của mình thay vì phải lượm lặt nó ra khỏi một ngoại lệ / lỗi thời gian chạy sản xuất.


Về vấn đề đó, tôi bắt đầu làm việc với case objectcon đường để xem liệu tôi có thể tạo ra một giải pháp bao gồm tất cả các định nghĩa liệt kê ở trên không. Thử thách đầu tiên là vượt qua cốt lõi của vấn đề khởi tạo đối tượng lớp / đối tượng JVM (được đề cập chi tiết trong bài đăng StackOverflow này ). Và cuối cùng tôi đã có thể tìm ra một giải pháp.

Như giải pháp của tôi là hai đặc điểm; EnumutionsEnumutionsDecorated , và vì Enumerationđặc điểm này dài hơn 400 dòng (rất nhiều ý kiến ​​giải thích bối cảnh), tôi đã gửi nó vào chủ đề này (điều này sẽ khiến nó kéo dài xuống trang một cách cân nhắc). Để biết chi tiết, xin vui lòng nhảy trực tiếp đến Gist .

Đây là những gì giải pháp kết thúc giống như sử dụng cùng một ý tưởng dữ liệu như trên (phiên bản nhận xét đầy đủ có sẵn ở đây ) và được triển khai trong 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
}

Đây là một ví dụ sử dụng một cặp tính trạng liệt kê mới mà tôi đã tạo (nằm trong Gist này ) để thực hiện tất cả các khả năng mong muốn và được nêu trong định nghĩa liệt kê.

Một mối quan tâm được thể hiện là tên thành viên liệt kê phải được lặp lại ( decorationOrderedSettrong ví dụ trên). Mặc dù tôi đã giảm thiểu nó xuống một lần lặp lại, tôi không thể thấy làm thế nào để làm cho nó ít hơn do hai vấn đề:

  1. Khởi tạo đối tượng / lớp JVM cho mô hình đối tượng / trường hợp cụ thể này không được xác định (xem luồng Stackoverflow này )
  2. Nội dung được trả về từ phương thức getClass.getDeclaredClassescó một thứ tự không xác định (và rất khó có thể theo cùng thứ tự với các case objectkhai báo trong mã nguồn)

Do hai vấn đề này, tôi đã phải từ bỏ việc cố gắng tạo ra một thứ tự ngụ ý và phải yêu cầu rõ ràng khách hàng xác định và khai báo nó với một số khái niệm tập hợp được đặt hàng. Vì các bộ sưu tập Scala không có triển khai cài đặt theo thứ tự chèn, điều tốt nhất tôi có thể làm là sử dụng Listkiểm tra thời gian chạy và kiểm tra xem nó có thực sự là một tập hợp không. Đó không phải là cách tôi muốn đạt được điều này.

Và đưa ra thiết kế yêu cầu danh sách / bộ thứ tự thứ hai này val, đưa ra ChessPiecesEnhancedDecoratedví dụ ở trên, có thể thêm case object PAWN2 extends Membervà sau đó quên thêm Decoration(PAWN2,'P2', 2)vào decorationOrderedSet. Vì vậy, có một kiểm tra thời gian chạy để xác minh rằng danh sách không chỉ là một tập hợp, mà còn chứa TẤT CẢ các đối tượng trường hợp mở rộng sealed trait Member. Đó là một hình thức đặc biệt của địa ngục phản chiếu / vĩ mô để vượt qua.


Vui lòng để lại ý kiến ​​và / hoặc phản hồi về Gist .


Bây giờ tôi đã phát hành phiên bản đầu tiên của thư viện ScalaOlio (GPLv3) chứa các phiên bản cập nhật hơn của cả hai org.scalaolio.util.Enumerationorg.scalaolio.util.EnumerationDecorated: scalaolio.org
chaotic3quilibrium

Và để chuyển trực tiếp đến kho lưu trữ ScalaOlio trên Github: github.com/chaotic3quilibrium/scala-olio
chaotic3quilibrium

5
Đây là một câu trả lời chất lượng và rất nhiều để lấy từ nó. Cảm ơn bạn
angabriel

1
Có vẻ như Oderky đang muốn nâng cấp Dotty (Scala 3.0 trong tương lai) với một enum bản địa. Ôi trời ơi! github.com/lampepfl/dotty/issues/1970
hỗn loạn3quilibrium

62

Các đối tượng trường hợp đã trả lại tên của chúng cho các phương thức toString của chúng, vì vậy việc chuyển nó thành riêng là không cần thiết. Đây là một phiên bản tương tự như jho's (phương pháp tiện lợi được bỏ qua cho ngắn gọn):

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

Đối tượng lười biếng; bằng cách sử dụng vals thay vì chúng ta có thể bỏ danh sách nhưng phải lặp lại tên:

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

Nếu bạn không bận tâm đến việc gian lận, bạn có thể tải trước các giá trị liệt kê của mình bằng API phản chiếu hoặc một cái gì đó như Google Reflections. Các đối tượng trường hợp không lười biếng cung cấp cho bạn cú pháp rõ ràng nhất:

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
}

Đẹp và sạch sẽ, với tất cả các lợi thế của các lớp tình huống và bảng liệt kê Java. Cá nhân, tôi xác định các giá trị liệt kê bên ngoài đối tượng để phù hợp hơn với mã Scala thành ngữ:

object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency

3
một câu hỏi: giải pháp cuối cùng được gọi là "đối tượng trường hợp không lười biếng" nhưng trong trường hợp này đối tượng không được tải cho đến khi chúng tôi sử dụng chúng: tại sao bạn gọi giải pháp này là không lười biếng?
Seb Cesbron

2
@ Không, bạn cần sử dụng: dán để dán toàn bộ hệ thống phân cấp được niêm phong vào REPL. Nếu bạn không, dòng đơn với lớp cơ sở / đặc điểm được niêm phong sẽ được tính là một tệp duy nhất, được niêm phong ngay lập tức và không thể được mở rộng trên dòng tiếp theo.
Jürgen Strobel

2
@GatesDA Chỉ đoạn mã đầu tiên của bạn không có lỗi (vì rõ ràng bạn yêu cầu khách hàng khai báo và xác định giá trị. Cả hai giải pháp thứ hai và thứ ba của bạn đều có lỗi tinh vi mà tôi đã mô tả trong nhận xét cuối cùng của mình (nếu khách hàng tình cờ truy cập Tiền tệ .GBP trực tiếp và đầu tiên, các giá trị Danh sách sẽ "không theo thứ tự"). Tôi đã khám phá rộng rãi miền liệt kê Scala và đã trình bày chi tiết trong câu trả lời của tôi cho cùng chủ đề này: stackoverflow.com/a/25923651/501113
hỗn loạn3quilibrium

1
Có lẽ một trong những nhược điểm của cách tiếp cận này (dù sao so với Java Enums) là khi bạn nhập Đơn vị tiền tệ <dot> trong IDE, nó không hiển thị các tùy chọn khả dụng.
Ivan Balashov

1
Như @SebCesbron đã đề cập, các đối tượng trường hợp lười biếng ở đây. Vì vậy, nếu tôi gọi Currency.values, tôi chỉ nhận lại các giá trị mà tôi đã truy cập trước đó. Có đường nào quanh đó không?
Sasgorilla

27

Ưu điểm của việc sử dụng các lớp trường hợp so với Số liệu là:

  • Khi sử dụng các lớp trường hợp được niêm phong, trình biên dịch Scala có thể cho biết liệu kết quả khớp có được chỉ định đầy đủ hay không, ví dụ như khi tất cả các kết quả khớp có thể được sử dụng trong khai báo khớp. Với bảng liệt kê, trình biên dịch Scala không thể biết được.
  • Các lớp trường hợp tự nhiên hỗ trợ nhiều trường hơn Bảng liệt kê dựa trên giá trị hỗ trợ tên và ID.

Ưu điểm của việc sử dụng Số liệt kê thay vì các lớp trường hợp là:

  • Các liệt kê thường sẽ là một ít mã để viết.
  • Việc liệt kê dễ hiểu hơn một chút đối với người mới biết về Scala vì chúng phổ biến trong các ngôn ngữ khác

Vì vậy, nói chung, nếu bạn chỉ cần một danh sách các hằng số đơn giản theo tên, hãy sử dụng bảng liệt kê. Mặt khác, nếu bạn cần một cái gì đó phức tạp hơn một chút hoặc muốn sự an toàn bổ sung của trình biên dịch cho bạn biết nếu bạn có tất cả các kết quả khớp được chỉ định, hãy sử dụng các lớp tình huống.


15

CẬP NHẬT: Mã dưới đây có một lỗi, được mô tả ở đây . Chương trình thử nghiệm bên dưới hoạt động, nhưng nếu bạn sử dụng DayOfWeek.Mon (ví dụ) trước DayOfWeek, thì nó sẽ thất bại vì DayOfWeek chưa được khởi tạo (sử dụng một đối tượng bên trong không khiến đối tượng bên ngoài được khởi tạo). Bạn vẫn có thể sử dụng mã này nếu bạn làm một cái gì đó giống như val enums = Seq( DayOfWeek )trong lớp chính của mình, buộc khởi tạo enum của bạn hoặc bạn có thể sử dụng các sửa đổi của chaotic3quilibrium. Mong chờ một enum dựa trên vĩ mô!


Nếu bạn muốn

  • cảnh báo về mô hình không phù hợp
  • ID Int được gán cho từng giá trị enum mà bạn có thể tùy chọn kiểm soát
  • một danh sách bất biến của các giá trị enum, theo thứ tự chúng được xác định
  • một bản đồ bất biến từ tên đến giá trị enum
  • Bản đồ bất biến từ giá trị id đến enum
  • nơi gắn các phương thức / dữ liệu cho tất cả hoặc các giá trị enum cụ thể hoặc cho toàn bộ enum
  • sắp xếp các giá trị enum (để bạn có thể kiểm tra, ví dụ: cho dù ngày <Thứ tư)
  • khả năng mở rộng một enum để tạo ra những người khác

sau đó có thể được quan tâm. Phản hồi chào mừng.

Trong triển khai này có các lớp cơ sở Enum và EnumVal trừu tượng, mà bạn mở rộng. Chúng ta sẽ thấy các lớp đó trong một phút, nhưng trước tiên, đây là cách bạn sẽ xác định một 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()
}

Lưu ý rằng bạn phải sử dụng từng giá trị enum (gọi phương thức áp dụng của nó) để đưa nó vào cuộc sống. [Tôi muốn các đối tượng bên trong không lười biếng trừ khi tôi đặc biệt yêu cầu chúng. Tôi nghĩ.]

Tất nhiên chúng ta có thể thêm các phương thức / dữ liệu vào DayOfWeek, Val hoặc các đối tượng trường hợp riêng lẻ nếu chúng ta muốn.

Và đây là cách bạn sẽ sử dụng một enum như vậy:

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

}

Đây là những gì bạn nhận được khi biên dịch nó:

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

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

Bạn có thể thay thế "khớp ngày" bằng "khớp (ngày: @unchecked)" trong trường hợp bạn không muốn cảnh báo như vậy hoặc đơn giản chỉ bao gồm trường hợp bắt tất cả vào cuối.

Khi bạn chạy chương trình trên, bạn nhận được kết quả đầu ra này:

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

Lưu ý rằng vì Danh sách và Bản đồ là bất biến, bạn có thể dễ dàng loại bỏ các yếu tố để tạo các tập hợp con, mà không phá vỡ enum.

Đây là lớp Enum (và EnumVal bên trong nó):

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

}

Và đây là một cách sử dụng nâng cao hơn của nó để điều khiển ID và thêm dữ liệu / phương thức vào bản tóm tắt Val và cho chính enum:

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

Tyvm đã cung cấp này. Tôi rất trân trọng điều này. Tuy nhiên, tôi nhận thấy rằng nó đang sử dụng "var" trái ngược với val. Và đây là một tội lỗi biên giới trong thế giới FP. Vì vậy, có cách nào để thực hiện điều này sao cho không sử dụng var? Chỉ tò mò nếu đây là một số loại trường hợp cạnh loại FP và tôi không hiểu cách triển khai của bạn là không mong muốn.
hỗn loạn3quilibrium

2
Tôi có lẽ không thể giúp bạn. Scala khá phổ biến khi viết các lớp đột biến bên trong nhưng không thay đổi đối với những người sử dụng chúng. Trong ví dụ trên, người dùng DayOfWeek không thể thay đổi enum; không có cách nào, ví dụ, để thay đổi ID của thứ ba, hoặc tên của nó, sau thực tế. Nhưng nếu bạn muốn một triển khai không có đột biến trong nội bộ , thì tôi chẳng có gì cả. Tuy nhiên, tôi sẽ không ngạc nhiên khi thấy một cơ sở enum mới tốt đẹp dựa trên các macro trong 2.11; ý tưởng đang được đá xung quanh trên scala-lang.
AmigoNico

Tôi đang gặp một lỗi lạ trong Bảng tính Scala. Nếu tôi trực tiếp sử dụng một trong các trường hợp Giá trị, tôi sẽ gặp lỗi khởi tạo. Tuy nhiên, nếu tôi thực hiện cuộc gọi đến phương thức .values ​​để xem nội dung của phép liệt kê, thì nó hoạt động và sau đó trực tiếp sử dụng thể hiện giá trị hoạt động. Bất kỳ ý tưởng các lỗi khởi tạo là gì? Và cách tối ưu là gì để đảm bảo việc khởi tạo xảy ra theo đúng thứ tự bất kể quy ước gọi là gì?
hỗn loạn3quilibrium

@ chaotic3quilibrium: Wow! Cảm ơn bạn đã theo đuổi điều này, và tất nhiên cảm ơn Rex Kerr vì đã nâng vật nặng. Tôi sẽ đề cập đến vấn đề ở đây và đề cập đến câu hỏi bạn đã tạo.
AmigoNico

"[Sử dụng var] là một tội lỗi chết người trong thế giới FP", tôi không nghĩ rằng ý kiến ​​đó được chấp nhận rộng rãi.
Erik Kaplun

12

Tôi có một lib đơn giản đẹp ở đây cho phép bạn sử dụng các đặc điểm / lớp niêm phong làm giá trị enum mà không phải duy trì danh sách giá trị của riêng bạn. Nó dựa vào một macro đơn giản không phụ thuộc vào lỗi knownDirectSubclasses.

https://github.com/lloydmeta/enumeratum


10

Cập nhật tháng 3 năm 2017: như nhận xét của Anthony Accioly , scala.Enumeration/enumPR đã bị đóng cửa.

Dotty (trình biên dịch thế hệ tiếp theo cho Scala) sẽ dẫn đầu, mặc dù vấn đề dotty 1970 PR 1958 của Martin Oderky .


Lưu ý: hiện tại (tháng 8 năm 2016, hơn 6 năm sau) đề xuất xóa scala.Enumeration: PR 5352

Khấu hao scala.Enumeration, thêm @enumchú thích

Cú pháp

@enum
 class Toggle {
  ON
  OFF
 }

là một ví dụ triển khai có thể, ý định là cũng hỗ trợ các ADT tuân thủ các hạn chế nhất định (không có các tham số lồng nhau, đệ quy hoặc thay đổi khác nhau), ví dụ:

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

Khấu hao những thảm họa không được thừa nhận đó là scala.Enumeration.

Ưu điểm của @enum so với scala.Enumutions:

  • Thực tế hoạt động
  • Java interop
  • Không có vấn đề tẩy
  • Không có DSL nhỏ khó hiểu khi tìm hiểu cách liệt kê

Nhược điểm: Không có.

Điều này giải quyết vấn đề không thể có một cơ sở mã hỗ trợ Scala-JVM Scala.jsvà Scala-Native (mã nguồn Java không được hỗ trợ Scala.js/Scala-Native, mã nguồn Scala không thể xác định các enum được API hiện có trên Scala-JVM chấp nhận).


Các PR ở trên đã đóng cửa (không có niềm vui). Bây giờ là năm 2017 và có vẻ như Dotty cuối cùng sẽ có được một cấu trúc enum. Đây là vấn đềPR của Martin . Hợp nhất, hợp nhất, hợp nhất!
Anthony Accioly

8

Một nhược điểm khác của các lớp trường hợp so với Số liệt kê khi bạn sẽ cần lặp lại hoặc lọc qua tất cả các trường hợp. Đây là một khả năng liệt kê tích hợp (và cả enum Java) trong khi các lớp trường hợp không tự động hỗ trợ khả năng đó.

Nói cách khác: "không có cách nào dễ dàng để có được danh sách tổng số các giá trị được liệt kê với các lớp trường hợp".


5

Nếu bạn nghiêm túc về việc duy trì khả năng tương tác với các ngôn ngữ JVM khác (ví dụ Java) thì tùy chọn tốt nhất là viết các enum Java. Chúng hoạt động trong suốt từ cả mã Scala và Java, nhiều hơn có thể nói cho scala.Enumerationcác đối tượng hoặc trường hợp. Chúng ta đừng có thư viện liệt kê mới cho mọi dự án sở thích mới trên GitHub, nếu có thể tránh được!


4

Tôi đã thấy các phiên bản khác nhau của việc tạo ra một trường hợp bắt chước một bảng liệt kê. Đây là phiên bản của tôi:

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

Điều này cho phép bạn xây dựng các lớp trường hợp trông như sau:

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

Có lẽ ai đó có thể đưa ra một mẹo hay hơn là chỉ cần thêm một lớp trường hợp vào danh sách như tôi đã làm. Đây là tất cả những gì tôi có thể nghĩ ra vào lúc đó.


Tại sao hai phương pháp riêng biệt tho?
Saish

@jho Tôi đã cố gắng làm việc thông qua giải pháp của bạn, nhưng nó sẽ không được biên dịch. Trong đoạn mã thứ hai, có một tham chiếu đến Trang web trong "loại V = Trang web". Tôi không chắc chắn những gì được đề cập để làm rõ lỗi biên dịch. Tiếp theo, tại sao bạn lại cung cấp các dấu ngoặc rỗng cho "Tiền tệ lớp trừu tượng"? Họ không thể rời đi sao? Cuối cùng, tại sao bạn sử dụng var trong "var value = ..."? Điều này không có nghĩa là khách hàng có thể bất cứ lúc nào từ bất cứ nơi nào trong mã gán Danh sách mới cho các giá trị? Sẽ không tốt hơn nếu biến nó thành một val thay vì var?
hỗn loạn3quilibrium

2

Tôi đã qua lại hai lựa chọn này trong vài lần gần đây tôi cần chúng. Cho đến gần đây, sở thích của tôi là dành cho tùy chọn đối tượng đặc điểm / trường hợp được niêm phong.

1) Tuyên bố liệt kê Scala

object OutboundMarketMakerEntryPointType extends Enumeration {
  type OutboundMarketMakerEntryPointType = Value

  val Alpha, Beta = Value
}

2) Đặc điểm kín + Đối tượng trường hợp

sealed trait OutboundMarketMakerEntryPointType

case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType

case object BetaEntryPoint extends OutboundMarketMakerEntryPointType

Mặc dù cả hai điều này không thực sự đáp ứng tất cả những gì một bảng liệt kê java mang lại cho bạn, dưới đây là những ưu và nhược điểm:

Bảng liệt kê Scala

Ưu điểm: -Các liên kết để khởi tạo với tùy chọn hoặc trực tiếp giả định chính xác (dễ dàng hơn khi tải từ cửa hàng liên tục) - Hỗ trợ trên tất cả các giá trị có thể được hỗ trợ

Nhược điểm: Không hỗ trợ cảnh báo biên dịch cho tìm kiếm không toàn diện (làm cho kết hợp mẫu không lý tưởng hơn)

Đối tượng trường hợp / Đặc điểm niêm phong

Ưu điểm: -Sử dụng các đặc điểm được niêm phong, chúng tôi có thể khởi tạo trước một số giá trị trong khi các giá trị khác có thể được đưa vào tại thời điểm tạo - hỗ trợ đầy đủ cho khớp mẫu (phương pháp áp dụng / không phù hợp được xác định)

Nhược điểm: -Đánh giá từ một cửa hàng liên tục - bạn thường phải sử dụng khớp mẫu ở đây hoặc xác định danh sách của riêng bạn về tất cả các 'giá trị enum' có thể

Điều cuối cùng khiến tôi thay đổi ý kiến ​​của mình là một đoạn như đoạn trích sau:

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
}

Các .getcuộc gọi thật gớm ghiếc - sử dụng phép liệt kê thay vào đó tôi chỉ có thể gọi phương thức withName trên bảng liệt kê như sau:

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

Vì vậy, tôi nghĩ rằng sở thích của tôi sẽ tiếp tục là sử dụng Bảng liệt kê khi các giá trị được dự định truy cập từ kho lưu trữ và các đối tượng trường hợp / các đặc điểm được niêm phong khác.


Tôi có thể thấy mẫu mã thứ hai được mong muốn như thế nào (loại bỏ hai phương thức trợ giúp khỏi mẫu mã thứ nhất). Tuy nhiên, tôi đã tìm ra một cách như vậy bạn không bị buộc phải lựa chọn giữa hai mẫu này. Tôi bao gồm toàn bộ tên miền trong câu trả lời tôi đã đăng lên chủ đề này: stackoverflow.com/a/25923651/501113
chaotic3quilibrium

2

Tôi thích case objects(đó là vấn đề sở thích cá nhân). Để đối phó với các vấn đề cố hữu đối với phương pháp đó (chuỗi phân tích cú pháp và lặp lại trên tất cả các yếu tố), tôi đã thêm một vài dòng không hoàn hảo, nhưng hiệu quả.

Tôi đang dán cho bạn mã ở đây với hy vọng nó có thể hữu ích và những người khác cũng có thể cải thiện nó.

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

0

Đối với những người vẫn đang tìm cách làm cho câu trả lời của GatesDa hoạt động : Bạn chỉ có thể tham chiếu đối tượng trường hợp sau khi khai báo để khởi tạo nó:

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
}

0

Tôi nghĩ rằng lợi thế lớn nhất của việc có case classeshơn enumerationslà bạn có thể sử dụng kiểu lớp hay còn gọi là đa hình ad-hoc . Không cần phải ghép các enum như:

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

thay vào đó bạn sẽ có một cái gì đó như:

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