Những bất lợi khi khai báo các lớp trường hợp Scala là gì?


105

Nếu bạn đang viết mã sử dụng nhiều cấu trúc dữ liệu đẹp, không thể thay đổi, các lớp trường hợp dường như là một món quà trời cho, cung cấp cho bạn tất cả những thứ sau miễn phí chỉ với một từ khóa:

  • Mọi thứ bất biến theo mặc định
  • Getters được xác định tự động
  • Triển khai Decent toString ()
  • Tuân thủ bằng () và hashCode ()
  • Đối tượng đồng hành với phương thức unapply () để so khớp

Nhưng nhược điểm của việc xác định cấu trúc dữ liệu bất biến như một lớp trường hợp là gì?

Nó đặt ra những hạn chế nào đối với lớp học hoặc khách hàng của nó?

Có những tình huống nào mà bạn nên thích một lớp không phải chữ hoa chữ thường không?


Xem câu hỏi liên quan này: stackoverflow.com/q/4635765/156410
David

18
Tại sao điều này không mang tính xây dựng? Các mod trên trang web này quá nghiêm ngặt. Điều này có một số hữu hạn các câu trả lời thực tế có thể có.
Eloff

5
Đồng ý với Eloff. Đây là một câu hỏi mà tôi cũng muốn có câu trả lời, và các câu trả lời được cung cấp rất hữu ích, và đừng tỏ ra chủ quan. Tôi đã thấy nhiều câu hỏi 'làm thế nào để sửa đoạn trích mã của mình' gây ra nhiều tranh luận và ý kiến ​​hơn.
Herc

Câu trả lời:


51

Một bất lợi lớn: một lớp trường hợp không thể mở rộng một lớp trường hợp. Đó là hạn chế.

Các ưu điểm khác mà bạn đã bỏ qua, được liệt kê cho đầy đủ: tuần tự hóa / giải mã hóa tuân thủ, không cần sử dụng từ khóa "mới" để tạo.

Tôi thích các lớp không phải chữ hoa chữ thường cho các đối tượng có trạng thái có thể thay đổi, trạng thái riêng tư hoặc không có trạng thái (ví dụ: hầu hết các thành phần singleton). Các lớp trường hợp cho khá nhiều thứ khác.


48
Bạn có thể phân lớp một lớp trường hợp. Lớp con cũng không thể là một lớp ca - đó là hạn chế.
Seth Tisue

99

Đầu tiên là những điều tốt:

Mọi thứ bất biến theo mặc định

Có, và thậm chí có thể được ghi đè (sử dụng var) nếu bạn cần

Getters được xác định tự động

Có thể có trong bất kỳ lớp nào bằng cách thêm các tham số tiền tố bằng val

Decent toString()thực hiện

Có, rất hữu ích, nhưng có thể làm bằng tay trên bất kỳ lớp nào nếu cần

Tuân thủ equals()hashCode()

Kết hợp với việc khớp mẫu dễ dàng, đây là lý do chính mà mọi người sử dụng các lớp trường hợp

Đối tượng đồng hành với unapply()phương thức để đối sánh

Cũng có thể làm bằng tay trên bất kỳ lớp nào bằng cách sử dụng trình giải nén

Danh sách này cũng nên bao gồm phương pháp sao chép uber-strong, một trong những điều tốt nhất đến với Scala 2.8


Sau đó, điều tồi tệ, chỉ có một số hạn chế thực sự với các lớp trường hợp:

Bạn không thể xác định applytrong đối tượng đồng hành bằng cách sử dụng chữ ký giống như phương thức do trình biên dịch tạo

Trong thực tế, điều này hiếm khi là một vấn đề. Việc thay đổi hành vi của phương thức apply đã tạo được đảm bảo sẽ gây ngạc nhiên cho người dùng và nên được khuyến khích mạnh mẽ, lý do duy nhất để làm như vậy là xác thực các tham số đầu vào - một nhiệm vụ được thực hiện tốt nhất trong phần thân hàm tạo chính (cũng làm cho việc xác thực khả dụng khi sử dụng copy)

Bạn không thể phân lớp

Đúng, mặc dù vẫn có thể xảy ra trường hợp lớp tự nó là con của nó. Một mô hình phổ biến là xây dựng một hệ thống phân cấp các đặc điểm, sử dụng các lớp trường hợp như các nút lá của cây.

Nó cũng đáng chú ý là sealedsửa đổi. Bất kỳ lớp con nào của một đặc điểm với công cụ sửa đổi này phải được khai báo trong cùng một tệp. Khi đối sánh mẫu với các trường hợp của đặc điểm, trình biên dịch sau đó có thể cảnh báo bạn nếu bạn chưa kiểm tra tất cả các lớp con cụ thể có thể có. Khi được kết hợp với các lớp trường hợp, điều này có thể cung cấp cho bạn mức độ tin cậy rất cao vào mã của bạn nếu nó biên dịch mà không có cảnh báo.

Là một lớp con của Sản phẩm, các lớp trường hợp không được có nhiều hơn 22 tham số

Không có cách giải quyết thực sự nào, ngoại trừ việc ngừng lạm dụng các lớp với nhiều tham số này :)

Cũng thế...

Một hạn chế khác đôi khi được lưu ý là Scala (hiện tại) không hỗ trợ lazy valcác tham số lười biếng (như s, nhưng dưới dạng tham số). Giải pháp cho việc này là sử dụng một tham số theo tên và gán nó cho một giá trị lười biếng trong hàm tạo. Thật không may, các tham số theo tên không kết hợp với đối sánh mẫu, điều này ngăn kỹ thuật được sử dụng với các lớp trường hợp vì nó phá vỡ trình trích xuất do trình biên dịch tạo ra.

Điều này có liên quan nếu bạn muốn triển khai cấu trúc dữ liệu lười chức năng cao và hy vọng sẽ được giải quyết bằng việc bổ sung các tham số lười vào bản phát hành Scala trong tương lai.


1
Cảm ơn vì câu trả lời toàn diện. Tôi nghĩ rằng mọi thứ ngoại lệ "Bạn không thể phân lớp" có lẽ khó có thể sớm loại bỏ tôi.
Graham Lea,

15
Bạn có thể phân lớp một lớp trường hợp. Lớp con cũng không thể là một lớp ca - đó là hạn chế.
Seth Tisue

5
Giới hạn 22 tham số cho các lớp trường hợp bị loại bỏ trong Scala 2.11. Vấn đề.scala
Jonathan Crosmer

Không chính xác khi khẳng định "Bạn không thể xác định áp dụng trong đối tượng đồng hành bằng cách sử dụng chữ ký giống như phương thức do trình biên dịch tạo". Mặc dù nó yêu cầu phải nhảy qua một số vòng để làm như vậy (nếu bạn có ý định giữ lại chức năng đã từng được trình biên dịch scala tạo ra một cách vô hình), nhưng chắc chắn bạn có thể đạt được điều đó: stackoverflow.com/a/25538287/501113
mess3quil Balance

Tôi đã sử dụng rộng rãi các lớp trường hợp Scala và đã đưa ra một "mẫu lớp trường hợp" (cuối cùng sẽ kết thúc dưới dạng macro Scala) giúp giải quyết một số vấn đề được xác định ở trên: codereview.stackexchange.com/a/98367 / 4758
hỗn loạn3 trạng thái cân

10

Tôi nghĩ rằng nguyên tắc TDD áp dụng ở đây: không thiết kế quá mức. Khi bạn khai báo một cái gì đó là a case class, bạn đang khai báo rất nhiều chức năng. Điều đó sẽ làm giảm tính linh hoạt của bạn trong việc thay đổi lớp học trong tương lai.

Ví dụ: a case classcó một equalsphương thức trên các tham số của hàm tạo. Bạn có thể không quan tâm đến điều đó khi lần đầu tiên bạn viết lớp của mình, nhưng sau này, bạn có thể quyết định rằng bạn muốn bình đẳng để bỏ qua một số tham số này hoặc làm điều gì đó khác một chút. Tuy nhiên, mã máy khách có thể được viết trong thời gian trung bình phụ thuộc vào case classsự bình đẳng.


4
Tôi không nghĩ rằng mã khách hàng nên phụ thuộc vào ý nghĩa chính xác của 'bằng'; tùy thuộc vào một lớp học để quyết định xem 'bằng' có nghĩa gì đối với nó. Tác giả lớp có thể tự do thay đổi việc triển khai 'bằng' xuống dòng.
pkaeding

8
@pkaeding Bạn có thể tự do không có mã khách hàng phụ thuộc vào bất kỳ phương thức riêng tư nào. Mọi thứ công khai đều là hợp đồng mà bạn đã đồng ý.
Daniel C. Sobral

3
@ DanielC.Sobral Đúng, nhưng việc triển khai chính xác bằng () (nó dựa trên trường nào) không nhất thiết phải có trong hợp đồng. Ít nhất, bạn có thể loại trừ nó khỏi hợp đồng một cách rõ ràng khi bạn viết lớp lần đầu tiên.
herman

2
@ DanielC.Sobral Bạn đang tự mâu thuẫn với chính mình: bạn nói rằng mọi người thậm chí sẽ dựa vào việc triển khai bằng mặc định (so sánh danh tính đối tượng). Nếu điều đó đúng và bạn viết một triển khai bằng khác sau đó, mã của chúng cũng sẽ bị hỏng. Dù sao, nếu bạn chỉ định các điều kiện trước / sau và các bất biến, và mọi người bỏ qua chúng, đó là vấn đề của họ.
herman

2
@herman Không có gì mâu thuẫn trong những gì tôi đang nói. Đối với "vấn đề của họ", chắc chắn, trừ khi nó trở thành vấn đề của bạn . Ví dụ: giả sử vì họ là khách hàng lớn của công ty khởi nghiệp của bạn hoặc vì người quản lý của họ thuyết phục ban lãnh đạo cấp trên rằng quá tốn kém để họ thay đổi, vì vậy bạn phải hoàn tác các thay đổi của mình hoặc vì thay đổi gây ra nhiều triệu đô la lỗi và được hoàn nguyên, v.v. Nhưng nếu bạn đang viết mã vì sở thích và không quan tâm đến người dùng, hãy tiếp tục.
Daniel C. Sobral

7

Có những tình huống nào mà bạn nên thích một lớp không phải chữ hoa chữ thường không?

Martin Odersky cho chúng ta một điểm khởi đầu tốt trong khóa học Nguyên tắc lập trình hàm trong Scala (Bài giảng 4.6 - Khớp mẫu) mà chúng ta có thể sử dụng khi phải chọn giữa lớp và lớp ca. Chương 7 của Scala By Ví dụ có cùng một ví dụ.

Giả sử, chúng tôi muốn viết một trình thông dịch cho các biểu thức số học. Để giữ cho mọi thứ đơn giản ban đầu, chúng tôi giới hạn bản thân chỉ với các số và + phép toán. Những expres- sions như vậy có thể được biểu diễn như một hệ thống phân cấp lớp, với một lớp cơ sở trừu tượng Expr làm gốc, và hai lớp con Number và Sum. Sau đó, biểu thức 1 + (3 + 7) sẽ được biểu diễn dưới dạng

Tổng mới (Số mới (1), Tổng mới (Số mới (3), Số mới (7)))

abstract class Expr {
  def eval: Int
}

class Number(n: Int) extends Expr {
  def eval: Int = n
}

class Sum(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval + e2.eval
}

Hơn nữa, thêm một lớp Prod mới không đòi hỏi bất kỳ thay đổi nào đối với mã hiện có:

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}

Ngược lại, thêm một phương thức mới yêu cầu sửa đổi tất cả các lớp hiện có.

abstract class Expr { 
  def eval: Int 
  def print
} 

class Number(n: Int) extends Expr { 
  def eval: Int = n 
  def print { Console.print(n) }
}

class Sum(e1: Expr, e2: Expr) extends Expr { 
  def eval: Int = e1.eval + e2.eval
  def print { 
   Console.print("(")
   print(e1)
   Console.print("+")
   print(e2)
   Console.print(")")
  }
}

Vấn đề tương tự được giải quyết với các lớp trường hợp.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr

Thêm một phương pháp mới là một thay đổi cục bộ.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
  }
}

Thêm một lớp Prod mới yêu cầu có khả năng thay đổi tất cả các đối sánh mẫu.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
    case Prod(e1,e2) => e1.eval * e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
    case Prod(e1,e2) => ...
  }
}

Chuyển biên từ videolecture 4.6 Khớp mẫu

Cả hai thiết kế này đều hoàn toàn ổn và việc lựa chọn giữa chúng đôi khi là vấn đề về phong cách, nhưng tuy nhiên, vẫn có một số tiêu chí quan trọng.

Một tiêu chí có thể là, bạn có thường xuyên tạo các lớp biểu thức con mới hơn hay bạn thường xuyên tạo các phương thức mới hơn? Vì vậy, đó là một tiêu chí xem xét khả năng mở rộng trong tương lai và khả năng mở rộng hệ thống của bạn.

Nếu những gì bạn làm chủ yếu là tạo các lớp con mới, thì thực sự giải pháp phân rã hướng đối tượng có ưu thế hơn. Lý do là nó rất dễ dàng và là một thay đổi cục bộ để chỉ tạo một lớp con mới với phương thức eval , trong đó như trong giải pháp hàm, bạn phải quay lại và thay đổi mã bên trong phương thức eval và thêm một trường hợp mới với nó.

Mặt khác, nếu những gì bạn làm là tạo ra nhiều phương thức mới, nhưng bản thân hệ thống phân cấp lớp sẽ được giữ tương đối ổn định, thì việc so khớp mẫu thực sự có lợi. Bởi vì, một lần nữa, mỗi phương thức mới trong giải pháp đối sánh mẫu chỉ là một thay đổi cục bộ , cho dù bạn đặt nó trong lớp cơ sở, hoặc thậm chí có thể nằm ngoài hệ thống phân cấp lớp. Trong khi một phương thức mới, chẳng hạn như hiển thị trong phân rã hướng đối tượng sẽ yêu cầu một số gia tăng mới là mỗi lớp con. Vì vậy, sẽ có nhiều phần hơn, mà bạn phải chạm vào.

Vì vậy, vấn đề của khả năng mở rộng này trong hai chiều, nơi bạn có thể muốn thêm các lớp mới vào một hệ thống phân cấp, hoặc bạn có thể muốn thêm các phương thức mới, hoặc có thể cả hai, đã được đặt tên là vấn đề biểu thức .

Hãy nhớ rằng: chúng ta phải sử dụng điều này như một điểm khởi đầu chứ không phải như tiêu chí duy nhất.

nhập mô tả hình ảnh ở đây


0

Tôi trích dẫn này từ Scala cookbookbởi Alvin Alexanderchương 6: objects.

Đây là một trong nhiều điều mà tôi thấy thú vị trong cuốn sách này.

Để cung cấp nhiều hàm tạo cho một lớp trường hợp, điều quan trọng là phải biết khai báo lớp trường hợp thực sự làm gì.

case class Person (var name: String)

Nếu bạn nhìn vào mã mà trình biên dịch Scala tạo ra cho ví dụ lớp trường hợp, bạn sẽ thấy nó tạo ra hai tệp đầu ra, Person $ .class và Person.class. Nếu bạn tháo rời Person $ .class bằng lệnh javap, bạn sẽ thấy rằng nó chứa một phương thức áp dụng, cùng với nhiều phương thức khác:

$ javap Person$
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(java.lang.String); // the apply method (returns a Person) public java.lang.Object readResolve();
        public java.lang.Object apply(java.lang.Object);
    }

Bạn cũng có thể tháo rời Person.class để xem nó chứa những gì. Đối với một lớp đơn giản như thế này, nó chứa thêm 20 phương thức; sự phình ra ẩn này là một lý do khiến một số nhà phát triển không thích các lớp trường hợp.

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.