Cách ghi đè áp dụng trong trường hợp đồng hành


84

Đây là tình huống. Tôi muốn định nghĩa một lớp trường hợp như vậy:

case class A(val s: String)

và tôi muốn xác định một đối tượng để đảm bảo rằng khi tôi tạo các phiên bản của lớp, giá trị của 's' luôn là chữ hoa, như sau:

object A {
  def apply(s: String) = new A(s.toUpperCase)
}

Tuy nhiên, điều này không hiệu quả vì Scala phàn nàn rằng phương thức apply (s: String) được định nghĩa hai lần. Tôi hiểu rằng cú pháp lớp trường hợp sẽ tự động xác định nó cho tôi, nhưng không có cách nào khác để tôi có thể đạt được điều này? Tôi muốn gắn bó với lớp trường hợp vì tôi muốn sử dụng nó để đối sánh mẫu.


3
Có lẽ thay đổi tiêu đề để "Làm thế nào để ghi đè áp dụng trong trường hợp lớp đồng hành"
ziggystar

1
Không sử dụng đường nếu nó không làm những gì bạn muốn ...
Raphael

7
@Raphael Điều gì xảy ra nếu bạn muốn đường nâu, tức là chúng tôi muốn đường có một số thuộc tính đặc biệt .. Tôi có câu hỏi chính xác giống như OP: các lớp trường hợp là v hữu ích nhưng đó là trường hợp sử dụng đủ phổ biến để muốn trang trí đối tượng đồng hành với một bổ sung áp dụng.
StephenBoesch

FYI Điều này được cố định trong scala 2.12+. Việc xác định một phương thức áp dụng kết hợp khác trong đồng hành ngăn cản việc tạo phương thức áp dụng mặc định.
món hầmSquared

Câu trả lời:


90

Lý do của xung đột là lớp trường hợp cung cấp cùng một phương thức apply () (cùng một chữ ký).

Trước hết, tôi muốn đề nghị bạn sử dụng request:

case class A(s: String) {
  require(! s.toCharArray.exists( _.isLower ), "Bad string: "+ s)
}

Điều này sẽ ném ra một Ngoại lệ nếu người dùng cố gắng tạo một phiên bản trong đó s bao gồm các ký tự chữ thường. Đây là một cách sử dụng tốt của các lớp trường hợp, vì những gì bạn đưa vào hàm tạo cũng là những gì bạn nhận được khi bạn sử dụng khớp mẫu ( match).

Nếu đây không phải là điều bạn muốn, thì tôi sẽ tạo hàm tạo privatevà buộc người dùng chỉ sử dụng phương thức áp dụng:

class A private (val s: String) {
}

object A {
  def apply(s: String): A = new A(s.toUpperCase)
}

Như bạn thấy, A không còn là a case class. Tôi không chắc liệu các lớp trường hợp có trường không thay đổi được dùng để sửa đổi các giá trị đến hay không, vì tên "lớp trường hợp" ngụ ý rằng có thể trích xuất các đối số hàm tạo (không sửa đổi) bằng cách sử dụng match.


5
Cuộc toCharArraygọi không cần thiết, bạn cũng có thể viết s.exists(_.isLower).
Frank S. Thomas

4
BTW tôi nghĩ s.forall(_.isUpper)là dễ hiểu hơn !s.exists(_.isLower).
Frank S. Thomas,

cảm ơn! Điều này chắc chắn làm việc cho nhu cầu của tôi. @Frank, tôi đồng ý rằng s.forall(_isupper)nó dễ đọc hơn. Tôi sẽ sử dụng nó kết hợp với gợi ý của @ olle.
John S

4
+1 cho "tên" lớp trường hợp "ngụ ý rằng có thể trích xuất các đối số của hàm tạo (không được sửa đổi) bằng cách sử dụng match."
Eugen Labun

2
@ollekullberg Bạn không cần phải rời xa việc sử dụng một lớp trường hợp (và mất tất cả các tính năng bổ sung mà một lớp trường hợp cung cấp theo mặc định) để đạt được hiệu quả mong muốn của OP. Nếu bạn thực hiện hai sửa đổi, bạn có thể có loại trường hợp của mình và ăn nó! A) đánh dấu lớp case là trừu tượng và B) đánh dấu phương thức khởi tạo của lớp case là private [A] (trái ngược với chỉ private). Có một số vấn đề khác phức tạp hơn xung quanh việc mở rộng các lớp trường hợp bằng cách sử dụng kỹ thuật này. Vui lòng xem câu trả lời tôi đã đăng để biết thêm chi tiết kỹ lưỡng hơn: stackoverflow.com/a/25538287/501113
mess3quil Cân bằng

28

CẬP NHẬT 25/02/2016:
Mặc dù câu trả lời tôi viết bên dưới vẫn đủ, nhưng cũng đáng để tham khảo một câu trả lời liên quan khác cho câu hỏi này liên quan đến đối tượng đồng hành của lớp trường hợp. Cụ thể, làm thế nào để người ta tái tạo chính xác đối tượng đồng hành ngầm định được tạo ra bởi trình biên dịch xảy ra khi người ta chỉ định nghĩa chính lớp trường hợp. Đối với tôi, nó hóa ra là phản trực quan.


Tóm tắt:
Bạn có thể thay đổi giá trị của một tham số lớp trường hợp trước khi nó được lưu trữ trong lớp trường hợp khá đơn giản trong khi nó vẫn còn là một ADT hợp lệ (ated) (Kiểu dữ liệu trừu tượng). Mặc dù giải pháp tương đối đơn giản, nhưng việc khám phá các chi tiết lại khó hơn một chút.

Chi tiết:
Nếu bạn muốn đảm bảo chỉ có thể khởi tạo các trường hợp hợp lệ của lớp trường hợp của mình, đây là một giả định thiết yếu đằng sau ADT (Kiểu dữ liệu trừu tượng), có một số điều bạn phải làm.

Ví dụ, một copyphương thức do trình biên dịch tạo ra được cung cấp theo mặc định trên một lớp trường hợp. Vì vậy, ngay cả khi bạn đã rất cẩn thận để đảm bảo chỉ các phiên bản được tạo thông qua applyphương thức của đối tượng đồng hành rõ ràng , điều này đảm bảo rằng chúng chỉ có thể chứa các giá trị chữ hoa, đoạn mã sau sẽ tạo ra một trường hợp lớp có giá trị chữ thường:

val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"

Ngoài ra, các lớp trường hợp thực hiện java.io.Serializable. Điều này có nghĩa là chiến lược cẩn thận của bạn để chỉ có các trường hợp chữ hoa có thể bị lật đổ bằng một trình chỉnh sửa văn bản đơn giản và deserialization.

Vì vậy, đối với tất cả các cách khác nhau mà lớp trường hợp của bạn có thể được sử dụng (nhân từ và / hoặc ác ý), đây là các hành động bạn phải thực hiện:

  1. Đối với đối tượng đồng hành rõ ràng của bạn:
    1. Tạo nó bằng cách sử dụng chính xác tên giống như lớp trường hợp của bạn
      • Điều này có quyền truy cập vào các phần riêng tư của lớp trường hợp
    2. Tạo ra một apply phương thức có cùng chữ ký với hàm tạo chính cho lớp trường hợp của bạn
      • Điều này sẽ biên dịch thành công khi bước 2.1 hoàn thành
    3. Cung cấp một triển khai lấy một phiên bản của lớp case bằng cách sử dụng newtoán tử và cung cấp một triển khai trống{}
      • Điều này bây giờ sẽ khởi tạo loại trường hợp nghiêm ngặt theo các điều khoản của bạn
      • Việc triển khai trống {}phải được cung cấp vì lớp trường hợp được khai báo abstract(xem bước 2.1)
  2. Đối với loại trường hợp của bạn:
    1. Khai báo nó abstract
      • Ngăn trình biên dịch Scala tạo một applyphương thức trong đối tượng đồng hành là nguyên nhân gây ra lỗi biên dịch "phương thức được xác định hai lần ..." (bước 1.2 ở trên)
    2. Đánh dấu hàm tạo chính là private[A]
      • Hàm tạo chính hiện chỉ có sẵn cho chính lớp trường hợp và đối tượng đồng hành của nó (cái mà chúng tôi đã xác định ở trên trong bước 1.1)
    3. Tạo một readResolve phương pháp
      1. Cung cấp triển khai bằng phương pháp áp dụng (bước 1.2 ở trên)
    4. Tạo một copy phương pháp
      1. Xác định nó có chữ ký chính xác như hàm tạo chính của lớp case
      2. Đối với mỗi tham số, hãy thêm một giá trị mặc định bằng cách sử dụng cùng một tên tham số (ví dụ: s: String = s :)
      3. Cung cấp triển khai bằng phương pháp áp dụng (bước 1.2 bên dưới)

Đây là mã của bạn đã được sửa đổi với các hành động trên:

object A {
  def apply(s: String, i: Int): A =
    new A(s.toUpperCase, i) {} //abstract class implementation intentionally empty
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

Và đây là mã của bạn sau khi thực hiện yêu cầu (được đề xuất trong câu trả lời @ollekullberg) và cũng xác định vị trí lý tưởng để đặt bất kỳ loại bộ nhớ đệm nào:

object A {
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i) {} //abstract class implementation intentionally empty
  }
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

Và phiên bản này an toàn / mạnh mẽ hơn nếu mã này sẽ được sử dụng thông qua Java interop (ẩn lớp trường hợp như một sự triển khai và tạo ra một lớp cuối cùng ngăn chặn các dẫn xuất):

object A {
  private[A] abstract case class AImpl private[A] (s: String, i: Int)
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i)
  }
}
final class A private[A] (s: String, i: Int) extends A.AImpl(s, i) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

Trong khi điều này trực tiếp trả lời câu hỏi của bạn, thậm chí còn có nhiều cách hơn để mở rộng con đường này xung quanh các lớp trường hợp ngoài bộ nhớ đệm cá thể. Đối với nhu cầu dự án của riêng tôi, tôi đã tạo ra một giải pháp mở rộng hơn nữa mà tôi đã ghi lại trên CodeReview (một trang web chị em của StackOverflow). Nếu bạn cuối cùng đã xem qua, sử dụng hoặc tận dụng giải pháp của tôi, vui lòng cân nhắc để lại cho tôi phản hồi, đề xuất hoặc câu hỏi và trong lý do, tôi sẽ cố gắng hết sức để trả lời trong vòng một ngày.


Tôi vừa đăng một giải pháp mở rộng mới hơn để trở nên thành ngữ Scala hơn và bao gồm việc sử dụng ScalaCache để dễ dàng lưu vào bộ nhớ đệm các trường hợp lớp (không được phép chỉnh sửa câu trả lời hiện có theo quy tắc meta): codereview.stackexchange.com/a/98367/4758
hỗn loạn3 trạng thái cân bằng,

cảm ơn cho lời giải thích chi tiết này. Tuy nhiên, tôi đang đấu tranh để hiểu, tại sao cần phải triển khai readResolve. Bởi vì biên dịch cũng hoạt động mà không cần thực thi readResolve.
mogli

đã đăng một câu hỏi riêng biệt: stackoverflow.com/questions/32236594/…
mogli

12

Tôi không biết làm thế nào để ghi đè applyphương thức trong đối tượng đồng hành (nếu điều đó thậm chí có thể) nhưng bạn cũng có thể sử dụng một loại đặc biệt cho chuỗi chữ hoa:

class UpperCaseString(s: String) extends Proxy {
  val self: String = s.toUpperCase
}

implicit def stringToUpperCaseString(s: String) = new UpperCaseString(s)
implicit def upperCaseStringToString(s: UpperCaseString) = s.self

case class A(val s: UpperCaseString)

println(A("hello"))

Đoạn mã trên xuất ra:

A(HELLO)

Bạn cũng nên xem câu hỏi này và đó là câu trả lời: Scala: có thể ghi đè phương thức khởi tạo lớp trường hợp mặc định không?


Cảm ơn vì điều đó - tôi đã suy nghĩ cùng dòng nhưng không biết về Proxy! Có thể tốt hơn s.toUpperCase một lần .
Ben Jackson

@Ben Tôi không thấy nơi nào toUpperCaseđược gọi nhiều hơn một lần.
Frank S. Thomas

bạn khá đúng val self, không phải def self. Tôi vừa có C ++ trên não.
Ben Jackson

6

Đối với những người đọc nội dung này sau tháng 4 năm 2017: Kể từ Scala 2.12.2+, Scala cho phép ghi đè áp dụng và hủy áp dụng theo mặc định . Bạn cũng có thể nhận được hành vi này bằng cách cung cấp -Xsource:2.12tùy chọn cho trình biên dịch trên Scala 2.11.11+.


1
Điều đó có nghĩa là gì? Làm thế nào tôi có thể áp dụng kiến ​​thức này vào một giải pháp? bạn có thể cung cấp một ví dụ?
k0pernikus

Lưu ý rằng unpply không được sử dụng cho các lớp trường hợp so khớp mẫu, điều này làm cho việc ghi đè nó khá vô dụng (nếu bạn -Xprintlà một matchcâu lệnh, bạn sẽ thấy rằng nó không được sử dụng).
J Cracknell

5

Nó hoạt động với các biến var:

case class A(var s: String) {
   // Conversion
   s = s.toUpperCase
}

Thực hành này rõ ràng được khuyến khích trong các lớp trường hợp thay vì xác định một phương thức khởi tạo khác. Xem tại đây. . Khi sao chép một đối tượng, bạn cũng giữ các sửa đổi tương tự.


4

Một ý tưởng khác trong khi giữ lớp trường hợp và không có định nghĩa ngầm hoặc một phương thức khởi tạo khác là làm cho chữ ký của applyhơi khác nhưng từ góc độ người dùng thì giống nhau. Ở đâu đó tôi đã nhìn thấy thủ thuật ngầm, nhưng không thể nhớ / tìm thấy đó là đối số ngầm nào, vì vậy tôi đã chọn Booleanở đây. Nếu ai đó có thể giúp tôi và hoàn thành thủ thuật ...

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)

Tại các trang web cuộc gọi, nó sẽ cung cấp cho bạn lỗi biên dịch (tham chiếu không rõ ràng đến định nghĩa quá tải). Nó chỉ hoạt động nếu các kiểu scala khác nhau nhưng giống nhau sau khi xóa, ví dụ: có hai chức năng khác nhau cho Danh sách [Int] và Danh sách [Chuỗi].
Mikaël Mayer

Tôi không thể làm cho con đường giải pháp này hoạt động (với 2.11). Cuối cùng tôi đã tìm ra lý do tại sao anh ấy không thể cung cấp phương thức áp dụng của riêng mình trên đối tượng đồng hành rõ ràng. Tôi đã trình bày chi tiết trong câu trả lời tôi vừa mới công bố: stackoverflow.com/a/25538287/501113
chaotic3quilibrium

3

Tôi đã đối mặt với cùng một vấn đề và giải pháp này phù hợp với tôi:

sealed trait A {
  def s:String
}

object A {
  private case class AImpl(s:String)
  def apply(s:String):A = AImpl(s.toUpperCase)
}

Và, nếu cần bất kỳ phương thức nào, chỉ cần xác định nó trong đặc điểm và ghi đè nó trong lớp trường hợp.


0

Nếu bạn bị mắc kẹt với scala cũ hơn mà bạn không thể ghi đè theo mặc định hoặc bạn không muốn thêm cờ trình biên dịch như @ mehmet-emre đã hiển thị và bạn yêu cầu một lớp trường hợp, bạn có thể làm như sau:

case class A(private val _s: String) {
  val s = _s.toUpperCase
}

0

Kể từ năm 2020 trên Scala 2.13, kịch bản ghi đè một lớp trường hợp áp dụng phương pháp với cùng một chữ ký hoạt động hoàn toàn tốt.

case class A(val s: String)

object A {
  def apply(s: String) = new A(s.toUpperCase)
}

đoạn mã trên biên dịch và chạy tốt trong Scala 2.13 cả ở chế độ REPL và không REPL.


-2

Tôi nghĩ rằng điều này hoạt động chính xác như cách bạn muốn. Đây là phiên REPL của tôi:

scala> case class A(val s: String)
defined class A

scala> object A {
     | def apply(s: String) = new A(s.toUpperCase)
     | }
defined module A

scala> A("hello")
res0: A = A(HELLO)

Điều này đang sử dụng Scala 2.8.1.final


3
Nó không hoạt động ở đây nếu tôi đặt mã vào một tệp và cố gắng biên dịch nó.
Frank S. Thomas,

Tôi tin rằng tôi đã đề xuất điều gì đó tương tự trong một câu trả lời trước đó và ai đó nói rằng nó chỉ hoạt động trong repl do cách hoạt động của repl.
Ben Jackson

5
REPL về cơ bản tạo ra một phạm vi mới với mỗi dòng, bên trong dòng trước đó. Đó là lý do tại sao một số thứ không hoạt động như mong đợi khi dán từ REPL vào mã của bạn. Vì vậy, hãy luôn kiểm tra cả hai.
gregturn

1
Cách thích hợp để kiểm tra đoạn mã trên (không hoạt động) là sử dụng: dán vào REPL để đảm bảo cả trường hợp và đối tượng được xác định cùng nhau.
StephenBoesch
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.