케이스 클래스 컴패니언에서 적용을 재정의하는 방법


84

그래서 여기에 상황이 있습니다. 다음과 같이 케이스 클래스를 정의하고 싶습니다.

case class A(val s: String)

클래스의 인스턴스를 만들 때 's'의 값이 항상 대문자가되도록 개체를 정의하고 싶습니다.

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

그러나 Scala가 apply (s : String) 메서드가 두 번 정의되어 있다고 불평하기 때문에 이것은 작동하지 않습니다. 케이스 클래스 구문이이를 자동으로 정의한다는 것을 이해하지만이를 달성 할 수있는 다른 방법이 없습니까? 패턴 매칭에 사용하고 싶기 때문에 케이스 클래스를 고수하고 싶습니다.


3
제목을 "케이스 클래스 컴패니언에서 적용을 재정의하는 방법"으로 변경할 수 있습니다.
ziggystar 2011-04-29

1
설탕이 원하는대로되지 않으면 사용하지 마십시오 ...
Raphael

7
@Raphael 만약 당신이 흑설탕을 원한다면, 즉 우리는 특별한 속성을 가진 설탕을 원합니다. 나는 OP와 정확히 같은 질문을 가지고 있습니다 : 케이스 클래스는 유용하지만 컴패니언 객체를 장식하고 싶은 충분히 일반적인 사용 사례입니다. 추가 적용.
StephenBoesch 2014-08-24

참고로 이것은 scala 2.12+에서 수정되었습니다. 컴패니언에서 다른 방식으로 혼동되는 적용 메서드를 정의하면 기본 적용 메서드가 생성되지 않습니다.
stewSquared

답변:


90

충돌의 원인은 케이스 클래스가 똑같은 apply () 메서드 (동일한 서명)를 제공하기 때문입니다.

먼저 다음을 사용하는 것이 좋습니다.

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

보시다시피 A는 더 이상 case class. "case class"라는 이름은 match.


5
toCharArray호출이 필요하지 않습니다, 당신은 또한 쓸 수 있습니다 s.exists(_.isLower).
Frank S. Thomas

4
BTW s.forall(_.isUpper)보다 이해하기 쉽다고 생각 !s.exists(_.isLower)합니다.
Frank S. Thomas

감사! 이것은 확실히 내 필요에 적합합니다. @Frank, s.forall(_isupper)더 읽기 쉽다는 데 동의합니다 . @olle의 제안과 함께 사용하겠습니다.
John S

4
""케이스 클래스 "라는 이름의 +1은를 사용하여 (수정되지 않은) 생성자 인수를 추출 할 수 있어야 함을 의미합니다 match."
Eugen Labun 2013

2
@ollekullberg OP의 원하는 효과를 얻기 위해 케이스 클래스를 사용하지 않고 케이스 케이스 클래스가 기본적으로 제공하는 모든 추가 혜택을 잃을 필요가 없습니다. 두 가지 수정을하면 케이스 클래스를 만들어 먹을 수도 있습니다! A) 케이스 클래스를 추상으로 표시하고 B) 케이스 클래스 생성자를 private [A]로 표시합니다 (단지 private이 아니라). 이 기술을 사용하여 케이스 클래스를 확장하는 것과 관련된 다른 미묘한 문제가 있습니다. 자세한 내용은 게시 한 답변을 참조하십시오 : stackoverflow.com/a/25538287/501113
chaotic3quilibrium

28

업데이트 2016/02/25 :
아래에 작성한 답변은 충분하지만 케이스 클래스의 동반자 객체와 관련하여 이에 대한 다른 관련 답변을 참조하는 것도 가치가 있습니다. 즉, 케이스 클래스 자체 만 정의 할 때 발생 하는 컴파일러 생성 암시 적 컴패니언 객체정확히 어떻게 재현합니까 ? 나에게는 직관적이지 않은 것으로 판명되었습니다.


요약 :
케이스 클래스 매개 변수가 유효한 (정해진) ADT (Abstract Data Type)를 유지하면서 케이스 클래스에 저장되기 전에 케이스 클래스 매개 변수의 값을 변경할 수 있습니다. 해결책은 비교적 간단했지만 세부 사항을 발견하는 것은 훨씬 더 어려웠습니다.

세부 정보 :
ADT (Abstract Data Type) 뒤에 필수적인 가정 인 케이스 클래스의 유효한 인스턴스 만 인스턴스화 할 수 있도록하려면 수행해야하는 여러 가지가 있습니다.

예를 들어 컴파일러 생성 copy메서드는 케이스 클래스에서 기본적으로 제공됩니다. 따라서 인스턴스 apply만 대문자 값만 포함 할 수 있음을 보장 하는 명시 적 컴패니언 객체의 메서드를 통해 생성되도록 매우 신중한 경우에도 다음 코드는 소문자 값을 가진 케이스 클래스 인스턴스를 생성합니다.

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

또한 케이스 클래스는 java.io.Serializable. 즉, 간단한 텍스트 편집기와 역 직렬화로 대문자 인스턴스 만 사용하려는 신중한 전략을 뒤집을 수 있습니다.

따라서 케이스 클래스를 사용할 수있는 모든 다양한 방법 (자비 롭거나 악의적으로)에 대해 취해야 할 조치는 다음과 같습니다.

  1. 명시 적 컴패니언 객체의 경우 :
    1. 케이스 클래스와 정확히 동일한 이름을 사용하여 작성하십시오.
      • 이것은 케이스 클래스의 개인 부분에 액세스 할 수 있습니다.
    2. apply케이스 클래스의 기본 생성자와 정확히 동일한 서명을 사용하여 메서드를 만듭니다.
      • 2.1 단계가 완료되면 성공적으로 컴파일됩니다.
    3. new연산자를 사용하여 케이스 클래스의 인스턴스를 가져 오고 빈 구현을 제공하는 구현을 제공합니다.{}
      • 이제 조건에 따라 케이스 클래스를 인스턴스화합니다.
      • {}케이스 클래스가 선언되었으므로 빈 구현 을 제공해야합니다 abstract(2.1 단계 참조).
  2. 케이스 클래스 :
    1. 선언 abstract
      • Scala 컴파일러 apply가 "method is twice ..."컴파일 오류를 일으키는 동반 객체에서 메소드 를 생성하지 못하도록합니다 (위의 1.2 단계).
    2. 기본 생성자를 다음과 같이 표시하십시오. private[A]
      • 기본 생성자는 이제 케이스 클래스 자체와 동반 객체 (위의 1.1 단계에서 정의한 객체)에서만 사용할 수 있습니다.
    3. readResolve방법 만들기
      1. 적용 방법을 사용하여 구현 제공 (위의 1.2 단계)
    4. copy방법 만들기
      1. 케이스 클래스의 기본 생성자와 정확히 동일한 서명을 갖도록 정의하십시오.
      2. 각 매개 변수에 대해 동일한 매개 변수 이름을 사용하여 기본 값을 추가 (예를 : s: String = s)
      3. 적용 방법을 사용하여 구현 제공 (아래 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)
}

그리고 다음은 요구 사항을 구현하고 (@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 interop을 통해 사용될 경우이 버전은 더 안전하고 견고합니다 (케이스 클래스를 구현으로 숨기고 파생을 방지하는 최종 클래스를 만듭니다).

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 구현 없이도 작동하기 때문입니다.
mogli

별도의 질문을 게시했습니다 : stackoverflow.com/questions/32236594/…
mogli aug

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 : 기본 케이스 클래스 생성자를 재정의 할 수 있습니까?


고마워요-저도 같은 생각을하고 있었지만 몰랐습니다 Proxy! 그래도 s.toUpperCase 한 번 더 나을 수도 있습니다 .
Ben Jackson

@Ben 나는 toUpperCase두 번 이상 호출 되는 곳을 보지 못합니다 .
프랭크 S. 토마스

당신은 확실히 오른쪽있어 val self,하지 def self. 방금 뇌에 C ++가 있습니다.
Ben Jackson

6

2017 년 4 월 이후이 글을 읽는 사람들을 위해 : Scala 2.12.2 이상부터 Scala 는 기본적으로 적용 및 적용 취소를 재정의 할 수 있습니다 . -Xsource:2.12Scala 2.11.11+에서도 컴파일러에 옵션을 제공하여이 동작을 얻을 수 있습니다 .


1
이것은 무엇을 의미 하는가? 이 지식을 솔루션에 어떻게 적용 할 수 있습니까? 예를 들어 줄 수 있습니까?
k0pernikus

적용 취소가 꽤 쓸모가 (당신이 경우 무시하게 패턴 매칭의 경우 클래스에 사용되지 않도록주의 문 당신이 그것을 사용하지 않는 것을 볼 수 있습니다). -Xprintmatch
J Cracknell

5

var 변수와 함께 작동합니다.

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

이 방법은 다른 생성자를 정의하는 대신 케이스 클래스에서 권장됩니다. 여길 봐. . 개체를 복사 할 때도 동일한 수정 사항이 유지됩니다.


4

케이스 클래스를 유지하고 암시 적 정의 나 다른 생성자가없는 또 다른 아이디어는 서명을 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 사용). 마침내 그가 명시 적 컴패니언 객체에 대해 자신의 적용 방법을 제공 할 수없는 이유를 알아 냈습니다. 난 그냥 게시 된 질문에 대해 답을 설명했다 : 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)
}

그리고 메서드가 필요하면 트레이 트에서 정의하고 케이스 클래스에서 재정의하십시오.


0

기본적으로 재정의 할 수없는 이전 스칼라를 사용하고 있거나 @ mehmet-emre가 표시 한대로 컴파일러 플래그를 추가하고 싶지 않고 케이스 클래스가 필요한 경우 다음을 수행 할 수 있습니다.

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

0

2020 년부터 Scala 2.13에서 동일한 서명으로 케이스 클래스 적용 메서드를 재정의하는 위의 시나리오는 완전히 잘 작동합니다.

case class A(val s: String)

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

위의 스 니펫은 REPL 및 non-REPL 모드 모두에서 Scala 2.13에서 잘 컴파일되고 실행됩니다.


-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에서만 작동한다고 말했습니다.
Ben Jackson

5
REPL은 기본적으로 이전 행 안에 각 행으로 새 범위를 만듭니다. 그렇기 때문에 REPL에서 코드로 붙여 넣을 때 일부 항목이 예상대로 작동하지 않습니다. 따라서 항상 둘 다 확인하십시오.
gregturn

1
위의 코드 (작동하지 않음)를 테스트하는 적절한 방법은 REPL에서 : paste를 사용하여 대소 문자와 객체가 함께 정의되었는지 확인하는 것입니다.
StephenBoesch
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.