Scala 케이스 클래스 선언의 단점은 무엇입니까?


105

아름답고 변경 불가능한 데이터 구조를 많이 사용하는 코드를 작성하는 경우 케이스 클래스는 신의 선물로 보이며 단 하나의 키워드로 다음을 모두 무료로 제공합니다.

  • 기본적으로 변경 불가능한 모든 것
  • 게터 자동 정의
  • 괜찮은 toString () 구현
  • 준수 equals () 및 hashCode ()
  • 일치를위한 unapply () 메소드가있는 동반 객체

그러나 불변 데이터 구조를 케이스 클래스로 정의 할 때의 단점은 무엇입니까?

클래스 또는 클라이언트에 어떤 제한이 있습니까?

케이스가 아닌 클래스를 선호해야하는 상황이 있습니까?


관련 질문 참조 : stackoverflow.com/q/4635765/156410
David

18
왜 건설적이지 않습니까? 이 사이트의 모드는 너무 엄격합니다. 여기에는 유한 한 수의 사실적 답변이 있습니다.
Eloff

5
Eloff와 동의하십시오. 이 질문도 답변을 원했고 제공된 답변은 매우 유용하며 주관적이지 않습니다. 더 많은 토론과 의견을 이끌어내는 '코드 발췌 수정 방법'질문을 많이 보았습니다.
Herc

답변:


51

한 가지 큰 단점은 케이스 클래스가 케이스 클래스를 확장 할 수 없다는 것입니다. 그것이 제한입니다.

누락 된 기타 장점, 완전성을 위해 나열 : 준수 직렬화 / 역 직렬화, "new"키워드를 사용하여 만들 필요가 없습니다.

변경 가능한 상태, 개인 상태 또는 상태가없는 객체 (예 : 대부분의 싱글 톤 구성 요소)에 대해 케이스가 아닌 클래스를 선호합니다. 거의 모든 것에 대한 케이스 클래스.


48
케이스 클래스를 서브 클래 싱 할 수 있습니다. 하위 클래스도 케이스 클래스가 될 수 없습니다. 이것이 제한입니다.
Seth Tisue 2011 년

99

먼저 좋은 점 :

기본적으로 변경 불가능한 모든 것

예, var필요한 경우 재정의 (사용 ) 할 수도 있습니다.

게터 자동 정의

매개 변수에 접두사를 붙여 모든 클래스에서 가능 val

괜찮은 toString()구현

예, 매우 유용하지만 필요한 경우 모든 수업에서 직접 수행 할 수 있습니다.

준수 equals()hashCode()

쉬운 패턴 매칭과 결합되어 사람들이 케이스 클래스를 사용하는 주된 이유입니다.

unapply()일치하는 방법이있는 동반 개체

추출기를 사용하여 모든 클래스에서 손으로 할 수도 있습니다.

이 목록에는 Scala 2.8에 도입 될 최고의 기능 중 하나 인 강력한 복사 방법도 포함되어야합니다.


그렇다면 케이스 클래스에는 몇 가지 실제 제한 사항이 있습니다.

apply컴파일러에서 생성 한 메서드와 동일한 서명을 사용하여 컴패니언 개체에서 정의 할 수 없습니다.

그러나 실제로 이것은 거의 문제가되지 않습니다. 생성 된 적용 메서드의 동작을 변경하는 것은 사용자를 놀라게 할 수 있으므로 절대 권장하지 않습니다. 그렇게하는 유일한 이유는 입력 매개 변수의 유효성을 검사하는 것입니다. 이는 기본 생성자 본문에서 가장 잘 수행되는 작업입니다 (를 사용할 때 유효성 검사를 사용할 수도 있음 copy).

하위 클래스를 만들 수 없습니다.

사실이지만 케이스 클래스 자체가 자손 일 수는 있습니다. 한 가지 일반적인 패턴은 케이스 클래스를 트리의 리프 노드로 사용하여 트레이 트의 클래스 계층 구조를 구축하는 것입니다.

또한 sealed수정 자에 주목할 가치가 있습니다. 이 수정자가있는 특성의 모든 하위 클래스 는 동일한 파일에서 선언 합니다. 특성의 인스턴스에 대해 패턴 일치를 수행 할 때 가능한 모든 구체적인 하위 클래스를 확인하지 않은 경우 컴파일러에서 경고 할 수 있습니다. 케이스 클래스와 결합하면 경고없이 컴파일 될 경우 코드에 대해 매우 높은 수준의 신뢰성을 제공 할 수 있습니다.

Product의 하위 클래스로서 케이스 클래스는 22 개 이상의 매개 변수를 가질 수 없습니다.

이 많은 매개 변수로 클래스 남용을 중지하는 것을 제외하고는 실제 해결 방법이 없습니다. :)

또한...

때때로 언급되는 또 다른 제한은 Scala가 (현재) lazy params를 지원하지 않는다는 것입니다 ( lazy vals와 같지만 매개 변수로). 이에 대한 해결 방법은 by-name 매개 변수를 사용하고 생성자의 lazy val에 할당하는 것입니다. 불행히도 이름 별 매개 변수는 패턴 일치와 혼합되지 않으므로 컴파일러에서 생성 한 추출기를 손상시킬 때 케이스 클래스와 함께 사용되는 기술을 방지합니다.

이는 고기능의 지연 데이터 구조를 구현하려는 경우에 적합하며 향후 Scala 릴리스에 지연 매개 변수를 추가하여 해결 될 것입니다.


1
포괄적 인 답변에 감사드립니다. "You ca n't subclass"를 제외한 모든 예외가 조만간 나를 단계적으로 진행할 가능성은 거의 없다고 생각합니다.
Graham Lea 2011 년

15
케이스 클래스를 서브 클래 싱 할 수 있습니다. 하위 클래스도 케이스 클래스가 될 수 없습니다. 이것이 제한입니다.
Seth Tisue 2011 년

5
케이스 클래스에 대한 22 개 매개 변수 제한은 Scala 2.11에서 제거되었습니다. issues.scala-lang.org/browse/SI-7296
Jonathan Crosmer 2015-06-12

"컴파일러가 생성 한 메서드와 동일한 서명을 사용하여 컴패니언 개체에 적용을 정의 할 수 없습니다"라고 주장하는 것은 올바르지 않습니다. 그렇게하려면 몇 가지 후프를
거쳐야

저는 Scala 케이스 클래스를 광범위하게 사용하고 있으며 위에서 확인 된 여러 문제를 해결하는 데 도움이되는 "케이스 클래스 패턴"(결국 Scala 매크로로 끝날 것임)을 생각해 냈습니다
chaotic3quilibrium

10

여기에 TDD 원칙이 적용된다고 생각합니다. 과도하게 설계하지 마십시오. 무언가를으로 case class선언하면 많은 기능을 선언하는 것입니다. 그러면 향후 클래스 변경에 대한 유연성이 떨어집니다.

예를 들어, a case class에는 equals생성자 매개 변수에 대한 메서드가 있습니다. 처음 클래스를 작성할 때 신경 쓰지 않을 수도 있지만 후자는 평등이 이러한 매개 변수 중 일부를 무시하거나 약간 다른 작업을 수행하도록 결정할 수 있습니다. 그러나 클라이언트 코드는 case class평등에 의존하는 평균 시간에 작성 될 수 있습니다 .


4
나는 클라이언트 코드가 '같음'의 정확한 의미에 의존해야한다고 생각하지 않습니다. '같음'이 무엇을 의미하는지 결정하는 것은 학급에 달려 있습니다. 클래스 작성자는 'equals'구현을 자유롭게 변경할 수 있어야합니다.
pkaeding

8
@pkaeding 개인 방법에 의존하는 클라이언트 코드를 가지지 않아도됩니다. 공개 된 모든 것은 귀하가 동의 한 계약입니다.
Daniel C. Sobral

3
@ DanielC.Sobral 사실이지만 equals () (기반이되는 필드)의 정확한 구현이 반드시 계약에있는 것은 아닙니다. 적어도 처음 클래스를 작성할 때 계약에서 명시 적으로 제외 할 수 있습니다.
herman

2
@ DanielC.Sobral 당신은 자신과 모순되고 있습니다 : 사람들은 심지어 기본 equals 구현 (객체 신원을 비교하는)에 의존 할 것이라고 말합니다. 이것이 사실이고 나중에 다른 equals 구현을 작성하면 해당 코드도 중단됩니다. 어쨌든 사전 / 사후 조건과 불변을 지정하고 사람들이이를 무시한다면 그게 문제입니다.
herman

2
@herman 내가 말하는 것에는 모순이 없습니다. "그들의 문제"에 관해서는 그것이 당신의 문제 가되지 않는 한 확실 합니다 . 예를 들어, 그들이 스타트 업의 거대한 고객이기 때문이거나, 관리자가 변경하는 데 너무 많은 비용이 들기 때문에 변경 사항을 취소해야하거나 변경으로 인해 수백만 달러가 발생한다고 상위 경영진에게 설득했기 때문입니다. 버그 등으로 되돌아갑니다.하지만 취미 용 코드를 작성하고 사용자를 신경 쓰지 않는다면 계속 진행하세요.
Daniel C. Sobral 2013 년

7

케이스가 아닌 클래스를 선호해야하는 상황이 있습니까?

Martin Odersky는 클래스와 케이스 클래스 중에서 선택해야 할 때 사용할 수 있는 Scala의 Functional Programming Principles (강의 4.6-Pattern Matching) 과정에서 좋은 출발점을 제공 합니다. Scala By Example의 7 장 에는 동일한 예제 가 포함되어 있습니다.

산술 표현을위한 인터프리터를 작성하고 싶습니다. 처음에는 단순하게 유지하기 위해 숫자와 + 연산으로 만 제한합니다. 이러한 표현식은 루트로 추상 기본 클래스 Expr과 두 개의 하위 클래스 인 Number 및 Sum을 사용하여 클래스 계층 구조로 표현 될 수 있습니다. 그런 다음 식 1 + (3 + 7)은 다음과 같이 표현됩니다.

new Sum (new Number (1), new Sum (new Number (3), new Number (7)))

abstract class Expr {
  def eval: Int
}

class Number(n: Int) extends Expr {
  def eval: Int = n
}

class Sum(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval + e2.eval
}

또한 새 Prod 클래스를 추가해도 기존 코드가 변경되지 않습니다.

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}

반대로 새 메서드를 추가하려면 기존 클래스를 모두 수정해야합니다.

abstract class Expr { 
  def eval: Int 
  def print
} 

class Number(n: Int) extends Expr { 
  def eval: Int = n 
  def print { Console.print(n) }
}

class Sum(e1: Expr, e2: Expr) extends Expr { 
  def eval: Int = e1.eval + e2.eval
  def print { 
   Console.print("(")
   print(e1)
   Console.print("+")
   print(e2)
   Console.print(")")
  }
}

동일한 문제가 케이스 클래스로 해결되었습니다.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr

새 메소드 추가는 로컬 변경입니다.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
  }
}

새 Prod 클래스를 추가하려면 잠재적으로 모든 패턴 일치를 변경해야합니다.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
    case Prod(e1,e2) => e1.eval * e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
    case Prod(e1,e2) => ...
  }
}

동영상 강의 4.6 패턴 매칭 대본

이 두 디자인 모두 완벽하게 훌륭하며 둘 중 선택하는 것은 스타일의 문제이지만 그럼에도 불구하고 중요한 기준이 있습니다.

한 가지 기준은 새로운 표현의 하위 클래스를 더 자주 생성합니까, 아니면 새로운 메서드를 더 자주 생성합니까? 따라서 이는 시스템의 향후 확장 성과 가능한 확장 패스를 살펴 보는 기준입니다.

당신이하는 일이 대부분 새로운 서브 클래스를 만드는 것이라면, 실제로 객체 지향 분해 솔루션이 우세합니다. 그 이유는 eval 메서드를 사용하여 새 하위 클래스를 만드는 것이 매우 쉽고 매우 로컬 변경이기 때문입니다. . 기능적 솔루션에서와 같이 돌아가서 eval 메서드 내부의 코드를 변경하고 새 케이스를 추가해야합니다. 그것에.

반면 에 새로운 메소드를 많이 생성하는 것이지만 클래스 계층 자체가 비교적 안정적으로 유지되는 경우 패턴 일치가 실제로 유리합니다. 다시 말하지만, 패턴 매칭 솔루션의 각각의 새로운 메서드 는 기본 클래스에 넣든 클래스 계층 외부에 넣든 상관없이 로컬 변경 일뿐 입니다. 객체 지향 분해에서 show와 같은 새로운 방법은 새로운 증가가 필요하지만 각 하위 클래스입니다. 그래서 더 많은 부분을 만져야합니다.

따라서 계층 구조에 새 클래스를 추가하거나 새 메서드를 추가하거나 둘 다 추가 할 수있는 2 차원 확장 성의 문제를 expression problem 이라고 명명 했습니다 .

기억하십시오 : 우리는 이것을 유일한 기준이 아닌 시작점으로 사용해야합니다.

여기에 이미지 설명 입력


0

나는에서이 말을 인용하고있다 Scala cookbook하여 Alvin Alexander제 6 장 : objects.

이것은 제가이 책에서 흥미롭게 발견 한 많은 것 중 하나입니다.

케이스 클래스에 여러 생성자를 제공하려면 케이스 클래스 선언이 실제로 무엇을하는지 아는 것이 중요합니다.

case class Person (var name: String)

케이스 클래스 예제를 위해 Scala 컴파일러가 생성하는 코드를 보면 두 개의 출력 파일 인 Person $ .class와 Person.class가 생성되는 것을 볼 수 있습니다. javap 명령을 사용하여 Person $ .class를 디스 어셈블하면 다른 많은 것과 함께 apply 메소드가 포함되어 있음을 알 수 있습니다.

$ javap Person$
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(java.lang.String); // the apply method (returns a Person) public java.lang.Object readResolve();
        public java.lang.Object apply(java.lang.Object);
    }

Person.class를 분해하여 포함 된 내용을 볼 수도 있습니다. 이와 같은 간단한 클래스의 경우 추가로 20 개의 메서드가 포함됩니다. 이 숨겨진 팽창은 일부 개발자가 케이스 클래스를 좋아하지 않는 이유 중 하나입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.