Як замінити застосування в супутнику класу справи


84

Тож ось ситуація. Я хочу визначити клас справи таким чином:

case class A(val s: String)

і я хочу визначити об'єкт, щоб переконатися, що коли я створюю екземпляри класу, значення 's' завжди має великі регістри, приблизно так:

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

Однак це не працює, оскільки Scala скаржиться на те, що метод apply (s: String) визначений двічі. Я розумію, що синтаксис класу case автоматично визначатиме його для мене, але чи немає іншого способу досягти цього? Я хотів би дотримуватися класу case, оскільки я хочу використовувати його для зіставлення шаблонів.


3
Можливо, змінити заголовок на "Як відмінити заявку в супутнику класу справи"
ziggystar

1
Не використовуйте цукор, якщо він не робить того, що ви хочете ...
Рафаель

7
@Raphael Що робити, якщо ви хочете коричневий цукор, тобто ми хочемо цукор з деякими особливими атрибутами .. У мене точно такий же запит, як і у OP: класи case корисні, але це досить поширений випадок використання, щоб прикрасити супутній об’єкт додаткова заявка.
StephenBoesch

FYI Це виправлено в масштабі 2.12+. Визначення інакше взаємодіючого методу застосування у супутнику заважає генерувати метод застосування за замовчуванням.
stewSquared

Відповіді:


90

Причиною конфлікту є те, що клас case забезпечує точно такий самий метод apply () (той самий підпис).

Перш за все я хотів би запропонувати вам використовувати require:

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

Це викличе виняток, якщо користувач намагається створити екземпляр, де s включає символи нижнього регістру. Це вдале використання класів кейсів, оскільки те, що ви вкладаєте в конструктор, це те, що ви отримуєте, коли використовуєте зіставлення шаблонів ( match).

Якщо це не те, що ви хочете, я б зробив конструктор privateі змусив користувачів використовувати лише метод apply:

class A private (val s: String) {
}

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

Як бачите, А вже не є case class. Я не впевнений, чи класи case з незмінними полями призначені для модифікації вхідних значень, оскільки назва "case class" означає, що має бути можливо витягти (немодифікований) аргумент конструктора за допомогою match.


5
toCharArrayВиклик не є необхідним, ви могли б також написатиs.exists(_.isLower) .
Frank S. Thomas

4
До речі, я думаю, що s.forall(_.isUpper)це легше зрозуміти, ніж !s.exists(_.isLower).
Френк С. Томас

Дякую! Це, безумовно, працює на мої потреби. @Frank, я згоден, що s.forall(_isupper)це легше для читання. Я буду використовувати це разом із пропозицією @ olle.
John S

4
+1 для "ім'я" клас справи "означає, що має бути можливо витягнути (немодифікований) аргументи конструктора за допомогою match."
Eugen Labun,

2
@ollekullberg Для досягнення бажаного ефекту ОП вам не потрібно відмовлятися від використання класу case (і втратити всі зайві смаки, які клас case case надає за замовчуванням). Якщо ви внесете дві модифікації, ви можете отримати свій клас справи і з'їсти його теж! A) позначте клас case як абстрактний та B) позначте конструктор класу case як private [A] (на відміну від просто private). Є кілька інших більш тонких питань щодо розширення класів кейсів за допомогою цієї техніки. Будь ласка , дивіться відповідь я відправив для більш ретельних деталей: stackoverflow.com/a/25538287/501113
chaotic3quilibrium

28

ОНОВЛЕННЯ 2016/02/25:
Незважаючи на те, що відповідь, яку я написав нижче, залишається достатньою, варто також посилатись на іншу відповідну відповідь на це стосовно супутнього об’єкта класу case. А саме, як можна точно відтворити згенерований компілятором неявний об'єкт-супутник, який виникає, коли визначається лише сам клас case. Для мене це виявилось протилежним інтуїтивним.


Короткий зміст:
Ви можете змінити значення параметра класу справи, перш ніж він буде збережений у класі справи досить просто, поки він все ще залишається дійсним (зміненим) ADT (Абстрактний тип даних). Хоча рішення було відносно простим, виявлення деталей було дещо складнішим.

Детально:
Якщо ви хочете, щоб екземпляри могли бути лише дійсними екземплярами вашого класу випадку, що є важливим припущенням для ADT (Абстрактного типу даних), вам слід зробити ряд речей.

Наприклад, сформований компілятор copy метод, за замовчуванням надається для класу case. Отже, навіть якщо ви були дуже обережні, щоб лише екземпляри створювались за допомогою applyметоду явного об’єкта-супутника, який гарантував, що вони можуть містити лише великі регістри, наступний код створить екземпляр класу справи з малим значенням:

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

Крім того, реалізуються класи case java.io.Serializable. Це означає, що ваша обережна стратегія, щоб мати лише екземпляри верхнього регістру, може бути зруйнована за допомогою простого текстового редактора та десеріалізації.

Отже, для всіх різних способів використання вашого класу case (доброзичливо та / або зловмисно), ось такі дії, які ви повинні вжити:

  1. Для вашого явного об’єкта-супутника:
    1. Створіть його, використовуючи абсолютно те саме ім’я, що і ваш клас справи
      • Це має доступ до приватних частин класу справи
    2. Створіть apply метод із точно таким самим підписом, що і основний конструктор для вашого класу case
      • Це буде успішно скомпільовано після завершення кроку 2.1
    3. Надайте реалізацію, яка отримує екземпляр класу case за допомогою newоператора та забезпечує порожню реалізацію{}
      • Тепер це створить інстанцію класу справи суворо на ваших умовах
      • {}Потрібно надати порожню реалізацію, оскільки оголошено клас case abstract(див. Крок 2.1)
  2. Для вашого класу справ:
    1. Заявіть це abstract
      • Забороняє компілятору Scala генерувати applyметод у об'єкті-супутнику, що спричиняє помилку компіляції "метод визначається двічі ..."
    2. Позначте основний конструктор як private[A]
      • Тепер основний конструктор доступний лише для самого класу case та його супутнього об'єкта (того, який ми визначили вище на кроці 1.1)
    3. Створіть readResolveметод
      1. Надайте реалізацію, використовуючи метод apply (крок 1.2 вище)
    4. Створіть copyметод
      1. Визначте його таким самим підписом, як основний конструктор класу case
      2. Для кожного параметра додайте значення за замовчуванням, використовуючи те саме ім’я параметра (наприклад: s: String = s )
      3. Надайте реалізацію, використовуючи метод apply (крок 1.2 нижче)

Ось ваш код, модифікований вищевказаними діями:

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

І ось ваш код після реалізації require (запропонованого у відповіді @ollekullberg), а також визначення ідеального місця для розміщення будь-якого кешування:

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

І ця версія є більш безпечною / надійною, якщо цей код буде використовуватися через взаємодію Java (приховує клас case як реалізацію і створює кінцевий клас, який запобігає виведенню):

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

Хоча це безпосередньо відповідає на ваше запитання, є ще більше способів розширити цей шлях навколо класів випадків, крім кешування екземплярів. Для власних потреб проекту я створив ще більш широке рішення, яке задокументував на CodeReview (дочірній сайт StackOverflow). Якщо ви все-таки переглянете його, використаєте чи використаєте моє рішення, будь ласка, залиште мені відгук, пропозиції чи запитання, і в межах розумної причини я зроблю все можливе, щоб відповісти протягом дня.


Я щойно опублікував нове експансивне рішення, щоб бути більш ідіоматичним Scala та включити використання ScalaCache для легкого кешування екземплярів класів кейсів (не дозволялося редагувати існуючу відповідь відповідно до мета правил): codereview.stackexchange.com/a/98367/4758
chaotic3quilibrium

дякую за це детальне пояснення. Але я намагаюся зрозуміти, чому потрібна реалізація readResolve. Оскільки компіляція також працює без реалізації readResolve.
могли

відповідав окреме питання: stackoverflow.com/questions/32236594 / ...
Mogli

12

Я не знаю, як замінити applyметод у об'єкті-супутнику (якщо це навіть можливо), але ви також можете використовувати спеціальний тип для рядків верхнього регістру:

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

Виведені вище коди виводять:

A(HELLO)

Ви також повинні поглянути на це запитання та відповіді на нього: Scala: чи можна замінити конструктор класу case за замовчуванням?


Дякую за це - я думав у тому ж ключі, але не знав про це Proxy! Можливо, краще s.toUpperCase один раз, хоча.
Бен Джексон,

@Ben Я не бачу, куди toUpperCaseтелефонують більше одного разу.
Frank S. Thomas

Ви цілком праві val self, ні def self. Я щойно отримав C ++ на мозок.
Бен Джексон,

6

Для людей, які читають це після квітня 2017 року: станом на Scala 2.12.2+, Scala дозволяє за замовчуванням замінювати застосовувати та скасовувати . Ви можете отримати таку поведінку, також надавши -Xsource:2.12можливість компілятору в Scala 2.11.11+.


1
Що це означає? Як я можу застосувати ці знання до рішення? Можете навести приклад?
k0pernikus

Зауважте, що unapply не використовується для збігу класів регістрів, що робить його заміщення досить марним (якщо ви -Xprint висловите matchтвердження, ви побачите, що воно не використовується).
J Cracknell,

5

Він працює зі змінними змінної:

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

Ця практика, очевидно, заохочується у класах case замість того, щоб визначати інший конструктор. Дивіться тут. . Під час копіювання об'єкта ви також зберігаєте ті самі зміни.


4

Ще одна ідея, зберігаючи клас case і не маючи неявного defs або іншого конструктора, полягає в тому, щоб зробити підпис applyдещо іншим, але з точки зору користувача однаковим. Десь я бачив неявний трюк, але не можу згадати / знайти, який це був неявний аргумент, тому я вибрав Booleanтут. Якщо хтось може мені допомогти і закінчити трюк ...

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

На сайтах викликів це видасть помилку компіляції (неоднозначне посилання на перевантажене визначення). Це працює, лише якщо типи шкали різні, але однакові після стирання, наприклад, мати дві різні функції для List [Int] та List [String].
Mikaël Mayer

Я не зміг заробити цей шлях рішення (з 2.11). Нарешті я зрозумів, чому він не міг надати власний метод apply для явного об'єкта-супутника. Я детально його у відповідь я просто розмістив: stackoverflow.com/a/25538287/501113
chaotic3quilibrium

3

Я зіткнувся з тією ж проблемою, і це рішення для мене добре:

sealed trait A {
  def s:String
}

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

І, якщо потрібен будь-який метод, просто визначте його в ознаці та перевизначте у класі case.


0

Якщо ви застрягли у старшій шкалі, де ви не можете замінити за замовчуванням, або ви не хочете додавати прапор компілятора, як показав @ mehmet-emre, і вам потрібен клас справи, ви можете зробити наступне:

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

0

Починаючи з 2020 року на Scala 2.13, наведений вище сценарій перевизначення методу застосування класу case із однаковим підписом працює абсолютно нормально.

case class A(val s: String)

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

наведений вище фрагмент компілюється та чудово працює в Scala 2.13 як у режимах REPL, так і в режимах, що не відносяться до REPL.


-2

Я думаю, це працює саме так, як ти вже хочеш. Ось моя сесія 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)

Для цього використовується Scala 2.8.1.final


3
Тут не працює, якщо я вкладу код у файл і спробую його скомпілювати.
Frank S. Thomas

Я вважаю, що я запропонував щось подібне у попередній відповіді, і хтось сказав, що це працює лише в repl завдяки способу роботи repl.
Бен Джексон,

5
REPL по суті створює новий обсяг з кожним рядком усередині попереднього. Ось чому деякі речі працюють не так, як очікувалося, вставляючи з REPL у ваш код. Отже, завжди перевіряйте обидва.
gregturn

1
Належним способом перевірити наведений вище код (який не працює) є використання: вставити в REPL, щоб переконатися, що і регістр, і об’єкт визначені разом.
StephenBoesch
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.