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

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

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

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

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

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



답변

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

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

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


답변

먼저 좋은 점 :

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

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

게터 자동 정의

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

괜찮은 toString()구현

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

준수 equals()hashCode()

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

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

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

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


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

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

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

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

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

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

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

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

또한…

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

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


답변

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

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


답변

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

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 이라고 명명 했습니다 .

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

여기에 이미지 설명 입력


답변

나는에서이 말을 인용하고있다 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 개의 메서드가 포함됩니다. 이 숨겨진 팽창은 일부 개발자가 케이스 클래스를 좋아하지 않는 이유 중 하나입니다.


답변