Oto sytuacja. Chcę zdefiniować taką klasę przypadku:
case class A(val s: String)
i chcę zdefiniować obiekt, aby zapewnić, że podczas tworzenia instancji klasy wartość „s” jest zawsze wielka, na przykład:
object A {
def apply(s: String) = new A(s.toUpperCase)
}
Jednak to nie działa, ponieważ Scala narzeka, że metoda apply (s: String) jest zdefiniowana dwukrotnie. Rozumiem, że składnia klasy przypadku automatycznie zdefiniuje ją za mnie, ale czy nie ma innego sposobu, w jaki mogę to osiągnąć? Chciałbym trzymać się klasy przypadku, ponieważ chcę jej używać do dopasowywania wzorców.
scala
pattern-matching
case-class
John S.
źródło
źródło
Odpowiedzi:
Przyczyną konfliktu jest to, że klasa case zapewnia dokładnie tę samą metodę apply () (ten sam podpis).
Przede wszystkim chciałbym zasugerować użycie wymagają:
case class A(s: String) { require(! s.toCharArray.exists( _.isLower ), "Bad string: "+ s) }
Spowoduje to zgłoszenie wyjątku, jeśli użytkownik spróbuje utworzyć wystąpienie, w którym s zawiera małe litery. Jest to dobre zastosowanie klas przypadków, ponieważ to, co umieszczasz w konstruktorze, jest również tym, co otrzymujesz, gdy używasz dopasowywania wzorców (
match
).Jeśli tego nie chcesz, zrobiłbym konstruktora
private
i zmusiłbym użytkowników do używania tylko 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 przypadków z niezmiennymi polami są przeznaczone do modyfikacji przychodzących wartości, ponieważ nazwa „klasa przypadku” sugeruje, że powinno być możliwe wyodrębnienie (niezmodyfikowanych) argumentów konstruktora przy użyciumatch
.źródło
toCharArray
nie jest konieczny, możesz też napisaćs.exists(_.isLower)
.s.forall(_.isUpper)
łatwiej to zrozumieć niż!s.exists(_.isLower)
.s.forall(_isupper)
jest to łatwiejsze do odczytania. Użyję tego w połączeniu z sugestią @ olle.match
."UPDATE 2016/02/25:
Chociaż odpowiedź, którą napisałem poniżej, pozostaje wystarczająca, warto również odwołać się do innej powiązanej odpowiedzi na to, dotyczącej obiektu towarzyszącego klasy przypadku. Mianowicie, w jaki sposób dokładnie odtworzyć niejawny obiekt towarzyszący wygenerowany przez kompilator, który pojawia się, gdy definiuje się tylko samą klasę przypadku. Dla mnie okazało się to sprzeczne z intuicją.
Podsumowanie:
Możesz zmienić wartość parametru klasy przypadku, zanim zostanie on zapisany w klasie przypadku, w prosty sposób, podczas gdy nadal pozostanie prawidłowym (ated) ADT (Abstract Data Type). Chociaż rozwiązanie było stosunkowo proste, odkrycie szczegółów było nieco trudniejsze.
Szczegóły:
Jeśli chcesz mieć pewność, że tylko poprawne instancje klasy przypadku będą mogły zostać kiedykolwiek utworzone, co jest podstawowym założeniem ADT (abstrakcyjny typ danych), musisz zrobić kilka rzeczy.
Na przykład
copy
metoda wygenerowana przez kompilator jest domyślnie udostępniana w klasie sprawy. Tak więc, nawet gdybyś był bardzo ostrożny, aby upewnić się, że tylko instancje zostały utworzone za pomocą metody jawnego obiektu towarzyszącego,apply
która gwarantuje, że mogą one zawsze zawierać tylko wielkie litery, poniższy kod utworzy instancję klasy z małymi literami:val a1 = A("Hi There") //contains "HI THERE" val a2 = a1.copy(s = "gotcha") //contains "gotcha"
Dodatkowo klasy przypadków implementują
java.io.Serializable
. Oznacza to, że twoją ostrożną strategię, aby mieć tylko wielkie litery, można obalić za pomocą prostego edytora tekstu i deserializacji.Tak więc, dla wszystkich różnych sposobów wykorzystania klasy sprawy (życzliwie i / lub wrogo), oto działania, które musisz wykonać:
apply
metodę z dokładnie tym samym podpisem, co główny konstruktor dla Twojej klasy casenew
operatora i podając pustą implementację{}
{}
Należy podać pustą implementację, ponieważ została zadeklarowana klasa przypadkuabstract
(patrz krok 2.1)abstract
apply
metody w obiekcie towarzyszącym, co powoduje błąd kompilacji „metoda jest zdefiniowana dwukrotnie ...” (krok 1.2 powyżej)private[A]
readResolve
metodęcopy
metodęs: String = s
)Oto twój kod zmodyfikowany za pomocą powyższych działań:
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 wymagania (sugerowanego w odpowiedzi @ollekullberg), a także wskazaniu 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) }
Ta wersja jest bezpieczniejsza / bardziej niezawodna, jeśli ten kod zostanie użyty za pośrednictwem środowiska Java (ukrywa klasę przypadku jako implementację i tworzy ostateczną klasę, która zapobiega wyprowadzaniu):
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 o klasy przypadków poza buforowanie instancji. Na potrzeby własnego projektu stworzyłem jeszcze bardziej rozbudowane rozwiązanie, które udokumentowałem w witrynie CodeReview (siostrzanej witrynie StackOverflow). Jeśli w końcu przejrzysz moje rozwiązanie, użyjesz lub wykorzystasz moje rozwiązanie, rozważ zostawienie mi opinii, sugestii lub pytań. Postaram się odpowiedzieć w ciągu jednego dnia.
źródło
Nie wiem, jak przesłonić
apply
metodę w obiekcie towarzyszącym (jeśli to w ogóle możliwe), ale możesz również użyć specjalnego typu dla ciągó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ś także spojrzeć na to pytanie i odpowiedzi: Scala: czy można przesłonić domyślny konstruktor klasy przypadku?
źródło
Proxy
! Może lepiejs.toUpperCase
kiedyś .toUpperCase
jest wywoływany więcej niż raz.val self
, niedef self
. Właśnie mam C ++ w mózgu.Dla osób czytających to po kwietniu 2017 r .: Od wersji Scala 2.12.2+, Scala domyślnie zezwala na nadpisywanie stosowania i anulowania . Możesz uzyskać to zachowanie, dając
-Xsource:2.12
opcję kompilatorowi również w wersji 2.11.11+.źródło
-Xprint
match
Działa ze zmiennymi var:
case class A(var s: String) { // Conversion s = s.toUpperCase }
Ta praktyka jest najwyraźniej zalecana w klasach przypadków zamiast definiowania innego konstruktora. Spójrz tutaj. . Podczas kopiowania obiektu zachowujesz te same modyfikacje.
źródło
Innym pomysłem, zachowując klasę przypadku i nie mając ukrytych braków lub innego konstruktora, jest uczynienie podpisu
apply
nieco innym, ale z perspektywy użytkownika takim samym. Gdzieś widziałem ukrytą sztuczkę, ale nie mogę sobie przypomnieć / znaleźć, który to niejawny argument, więc wybrałemBoolean
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)
źródło
Napotkałem ten sam problem i to rozwiązanie jest dla mnie w porządku:
sealed trait A { def s:String } object A { private case class AImpl(s:String) def apply(s:String):A = AImpl(s.toUpperCase) }
A jeśli potrzebna jest jakakolwiek metoda, po prostu zdefiniuj ją w trait i nadpisz w klasie case.
źródło
Jeśli utkniesz ze starszą skalą, w której domyślnie nie możesz nadpisać lub nie chcesz dodawać flagi kompilatora, jak pokazano @ mehmet-emre, i potrzebujesz klasy przypadku, możesz wykonać następujące czynności:
case class A(private val _s: String) { val s = _s.toUpperCase }
źródło
Począwszy od 2020 w Scali 2.13, powyższy scenariusz zastępowania metody stosowania klasy przypadków z tym samym podpisem działa całkowicie dobrze.
case class A(val s: String) object A { def apply(s: String) = new A(s.toUpperCase) }
powyższy fragment kodu kompiluje się i działa dobrze w Scali 2.13, zarówno w trybie REPL, jak i nie-REPL.
źródło
Myślę, że to już 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 Scala 2.8.1.final
źródło