program tip

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

radiobox 2020. 8. 15. 09:02
반응형

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선언하면 많은 기능을 선언하는 것입니다. 그러면 향후 클래스 변경에 대한 유연성이 떨어집니다.

For example, a case class has an equals method over the constructor parameters. You may not care about that when you first write your class, but, latter, may decide you want equality to ignore some of these parameters, or do something a bit different. However, client code may be written in the mean time that depends on case class equality.


Are there situations where you should prefer a non-case class?

Martin Odersky gives us a good starting point in his course Functional Programming Principles in Scala (Lecture 4.6 - Pattern Matching) that we could use when we must choose between class and case class. The chapter 7 of Scala By Example contains the same example.

Say, we want to write an interpreter for arithmetic expressions. To keep things simple initially, we restrict ourselves to just numbers and + operations. Such expres- sions can be represented as a class hierarchy, with an abstract base class Expr as the root, and two subclasses Number and Sum. Then, an expression 1 + (3 + 7) would be represented as

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
}

Furthermore, adding a new Prod class does not entail any changes to existing code:

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

In contrast, add a new method requires modification of all existing classes.

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

The same problem solved with case classes.

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

Adding a new method is a local change.

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

Adding a new Prod class requires potentially change all pattern matching.

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

Transcript from the videolecture 4.6 Pattern Matching

Both of these designs are perfectly fine and choosing between them is sometimes a matter of style, but then nevertheless there are some criteria that are important.

One criteria could be, are you more often creating new sub-classes of expression or are you more often creating new methods? So it's a criterion that looks at the future extensibility and the possible extension pass of your system.

If what you do is mostly creating new subclasses, then actually the object oriented decomposition solution has the upper hand. The reason is that it's very easy and a very local change to just create a new subclass with an eval method, where as in the functional solution, you'd have to go back and change the code inside the eval method and add a new case to it.

On the other hand, if what you do will be create lots of new methods, but the class hierarchy itself will be kept relatively stable, then pattern matching is actually advantageous. Because, again, each new method in the pattern matching solution is just a local change, whether you put it in the base class, or maybe even outside the class hierarchy. Whereas a new method such as show in the object oriented decomposition would require a new incrementation is each sub class. So there would be more parts, That you have to touch.

So the problematic of this extensibility in two dimensions, where you might want to add new classes to a hierarchy, or you might want to add new methods, or maybe both, has been named the expression problem.

Remember: we must use this like a starting point and not like the only criteria.

enter image description here


I am quoting this from Scala cookbook by Alvin Alexander chapter 6: objects.

This is one of the many things that I found interesting in this book.

To provide multiple constructors for a case class, it’s important to know what the case class declaration actually does.

case class Person (var name: String)

If you look at the code the Scala compiler generates for the case class example, you’ll see that see it creates two output files, Person$.class and Person.class. If you disassemble Person$.class with the javap command, you’ll see that it contains an apply method, along with many others:

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

You can also disassemble Person.class to see what it contains. For a simple class like this, it contains an additional 20 methods; this hidden bloat is one reason some developers don’t like case classes.

참고URL : https://stackoverflow.com/questions/4653424/what-are-the-disadvantages-to-declaring-scala-case-classes

반응형