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.