Jak przesłaniać zastosowanie w towarzyszu klasy case

Oto sytuacja. Chcę zdefiniować klasę case w taki sposób:

case class A(val s: String)

I chcę zdefiniować obiekt, aby upewnić się, że podczas tworzenia instancji klasy, wartość " s " jest zawsze wielkimi literami, jak TAK:

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

To jednak nie działa, ponieważ Scala skarży się, że metoda apply(s: String) jest zdefiniowana dwukrotnie. Rozumiem, że składnia klasy case automatycznie ją zdefiniuje, ale czy nie ma innego sposobu, aby to osiągnąć? Chciałbym zostać przy Klasa case, ponieważ chcę jej używać do dopasowywania wzorców.

Author: Frank S. Thomas, 2011-04-29

9 answers

Powodem konfliktu jest to, że Klasa case zapewnia dokładnie tę samą metodę apply () (ten sam podpis).

Przede wszystkim chciałbym zasugerować użycie require:

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

Spowoduje to wyświetlenie wyjątku, jeśli użytkownik spróbuje utworzyć instancję, w której s zawiera małe litery. Jest to dobre użycie klas przypadków, ponieważ to, co wkładasz do konstruktora, jest tym, co dostajesz, gdy używasz pattern matching (match).

Jeśli tego nie chcesz, to ja bym Utwórz konstruktor private i zmuś użytkowników do tylko użyj metody zastosuj:

class A private (val s: String) {
}

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

Jak widzisz, A nie jest już case class. Nie jestem pewien, czy klasy case Z niezmiennymi polami są przeznaczone do modyfikacji przychodzących wartości, ponieważ nazwa" Klasa case " sugeruje, że powinno być możliwe wyodrębnienie (niezmodyfikowanych) argumentów konstruktora za pomocą match.

 83
Author: olle kullberg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-29 07:20:27

Aktualizacja 2016/02/25:
Chociaż odpowiedź, którą napisałem poniżej, pozostaje wystarczająca, warto również odnieść się do innej powiązanej odpowiedzi na to pytanie odnośnie obiektu towarzyszącego klasy case. Mianowicie, jak dokładnie odtworzyć kompilator generowany niejawny obiekt towarzyszący, który ma miejsce, gdy definiuje się tylko samą klasę case. Dla mnie okazało się to sprzeczne z intuicją.


Podsumowanie:
Możesz zmienić wartość klasy case parametr przed jest przechowywany w klasie case całkiem po prostu, podczas gdy nadal pozostaje poprawnym (ated) ADT (abstrakcyjny typ danych). Chociaż rozwiązanie było stosunkowo proste, odkrywanie szczegółów było nieco trudniejsze.

Szczegóły:
Jeśli chcesz mieć pewność, że tylko poprawne instancje twojej klasy przypadków mogą zostać utworzone, co jest podstawowym założeniem ADT( abstrakcyjny typ danych), musisz zrobić kilka rzeczy.

Na przykład, a metoda generowana przez kompilator copy jest domyślnie dostarczana w klasie case. Tak więc, nawet jeśli byłbyś bardzo ostrożny, aby upewnić się, że tylko instancje zostały utworzone za pomocą metody apply jawnego obiektu towarzyszącego, która gwarantowała, że mogą zawierać tylko wartości wielkich liter, poniższy kod utworzyłby instancję klasy case Z małą literą:

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

Dodatkowo klasy case implementują java.io.Serializable. Oznacza to, że Twoja ostrożna strategia, aby mieć tylko przypadki wielkich liter, może być obalona przez prosty edytor tekstu i deserializacja.

Tak więc, dla wszystkich różnych sposobów, w jakie twoja klasa przypadków może być używana (dobroczynnie i / lub złośliwie), oto działania, które musisz wykonać: {]}

  1. dla Twojego obiektu towarzyszącego:
    1. utwórz go używając dokładnie tej samej nazwy, co twoja klasa przypadków
      • to ma dostęp do prywatnych części klasy case
    2. Utwórz metodę apply z dokładnie tym samym podpisem co główny konstruktor dla twoja klasa spraw
      • zostanie pomyślnie skompilowany po zakończeniu kroku 2.1
    3. zapewnienie implementacji uzyskującej instancję klasy case przy użyciu operatora new i dostarczenie pustej implementacji {}
      • to stworzy instancję klasy case ściśle na Twoich warunkach
      • należy podać pustą implementację {}, ponieważ Klasa case jest zadeklarowana abstract (patrz krok 2.1)
  2. Dla twoja klasa spraw:
    1. Declare it abstract
      • uniemożliwia kompilatorowi Scali wygenerowanie metody apply w obiekcie towarzyszącym, co było przyczyną "metoda jest zdefiniowana dwukrotnie..."błąd kompilacji (krok 1.2 powyżej)
    2. Oznacz główny konstruktor jako private[A]
      • podstawowy konstruktor jest teraz dostępny tylko dla samej klasy case i jej obiektu towarzyszącego (tego, który zdefiniowaliśmy powyżej w kroku 1.1)
    3. Utwórz metodę readResolve
        [43]} zapewnienie wdrożenia przy użyciu metody apply (krok 1.2 powyżej)
  3. Utwórz metodę copy
    1. Zdefiniuj, aby miała dokładnie ten sam podpis co główny konstruktor klasy case
    2. dla każdego parametru Dodaj wartość domyślną używając tej samej nazwy parametru (ex: s: String = s)
    3. Udostępnij implementację za pomocą metody apply (krok 1.2 poniżej)

Oto twój kod zmodyfikowany powyższymi działaniami:

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

A oto Twój kod po zaimplementowaniu require (sugerowanego w odpowiedzi @ollekullberg), a także zidentyfikowaniu idealnego miejsca do umieszczenia dowolnego rodzaju buforowania:

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

I ta wersja jest bardziej bezpieczna / solidna, jeśli kod ten będzie używany przez Java interop (ukrywa klasę case jako implementację i tworzy klasę końcową, która zapobiega derywacjom):

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

Chociaż to bezpośrednio odpowiada na twoje pytanie, istnieje jeszcze więcej sposobów na rozszerzenie tej ścieżki wokół klas przypadków poza buforowanie instancji. Na potrzeby własnego projektu stworzyłem jeszcze bardziej rozbudowane rozwiązanie , które mam udokumentowane w CodeReview (siostrzana strona StackOverflow). Jeśli w końcu przejrzysz je, użyjesz lub wykorzystasz moje rozwiązanie, rozważ pozostawienie mi opinii, sugestii lub pytań, a w granicach rozsądku Dołożę wszelkich starań, aby odpowiedzieć w ciągu dnia.

 24
Author: chaotic3quilibrium,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:17:57

Nie wiem, jak nadpisać metodę apply w obiekcie towarzyszącym (jeśli jest to w ogóle możliwe), ale możesz również użyć specjalnego typu dla łańcuchów wielkich liter:

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

Powyższy kod wyprowadza:

A(HELLO)

Powinieneś również spojrzeć na to pytanie i odpowiedzieć na nie: Scala: czy możliwe jest nadpisanie domyślnego konstruktora klasy case?

 11
Author: Frank S. Thomas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:54:39

Dla osób czytających to po kwietniu 2017: Od wersji Scala 2.12.2+, Scala domyślnie zezwala na nadpisywanie apply i unaply . Możesz uzyskać to zachowanie, podając opcję -Xsource:2.12 kompilatorowi w Scali 2.11.11+.

 5
Author: Mehmet Emre,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-05 07:44:22

Innym pomysłem przy zachowaniu klasy case i braku domyślnego defs lub innego konstruktora jest uczynienie podpisu apply nieco innym, ale z punktu widzenia użytkownika takim samym. Gdzieś widziałem ukrytą sztuczkę, ale nie pamiętam/nie znalazłem, który to był ukryty argument, więc wybrałem Boolean tutaj. Jeśli ktoś może mi pomóc i dokończyć sztuczkę...

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)
 4
Author: Peter Schmitz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-29 12:57:53

Działa ze zmiennymi var:

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

Ta praktyka jest najwyraźniej zachęcana w przypadku klas zamiast definiowania innego konstruktora. Zobacz tutaj.. Podczas kopiowania obiektu zachowujesz te same modyfikacje.

 3
Author: Mikaël Mayer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:34:18

Stanąłem przed tym samym problemem i to rozwiązanie jest dla mnie ok:

sealed trait A {
  def s:String
}

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

I, jeśli jakaś metoda jest potrzebna, po prostu zdefiniuj ją w trait i nadpisaj ją w klasie case.

 2
Author: Pere Ramirez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-03 14:54:27

Jeśli utkniesz ze starszą scalą, w której domyślnie nie możesz nadpisać lub nie chcesz dodawać flagi kompilatora, jak pokazała @mehmet-emre, i potrzebujesz klasy case, możesz wykonać następujące czynności:

case class A(private val _s: String) {
  val s = _s.toUpperCase
}
 0
Author: critium,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-25 15:44:34

Myślę, że to działa dokładnie tak, jak chcesz. Oto moja sesja REPL:

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)

Używa się Scali 2.8.1.finał

 -2
Author: mattrjacobs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-29 04:34:02