스칼라의 사례 클래스와 클래스의 차이점은 무엇입니까?


440

a case class와 a 의 차이점을 찾기 위해 Google을 검색했습니다 class. 모든 사람들은 클래스에서 패턴 일치를 원할 때 유스 케이스 클래스를 언급합니다. 그렇지 않으면 클래스를 사용하고 equals 및 hash 코드 재정의와 같은 추가 특권을 언급하십시오. 그러나 이것이 클래스 대신 케이스 클래스를 사용해야하는 유일한 이유입니까?

스칼라에는이 기능에 대한 몇 가지 중요한 이유가 있다고 생각합니다. 스칼라 사례 클래스에 대한 설명은 무엇입니까?

답변:


394

케이스 클래스는 생성자 인수에만 의존하는 단순하고 변경 불가능한 데이터 보유 오브젝트 로 볼 수 있습니다 .

이 기능 개념을 통해

  • 간단한 초기화 구문 사용 ( Node(1, Leaf(2), None)))
  • 패턴 일치를 사용하여 분해
  • 암시 적으로 정의 된 동등 비교

상속과 함께 case 클래스는 대수 데이터 유형 을 모방하는 데 사용됩니다 .

객체가 내부에서 상태 저장 계산을 수행하거나 다른 종류의 복잡한 동작을 나타내는 경우 일반 클래스 여야합니다.


11
@ Teja : 어떤 식 으로든. ADT는 일종의 매개 변수화 된 열거 형 이며 매우 강력하고 형식이 안전합니다.
Dario

8
봉인 된 사례 클래스는 대수 데이터 유형을 모방하는 데 사용됩니다. 그렇지 않으면 서브 클래스의 수가 제한되지 않습니다.
Thomas Jung

6
@Thomas : 올바르게 가능, 밀봉 추상 클래스로부터 모방 폐쇄 대수 데이터 유형을 도출 경우 클래스 ADT 달리 반면 열기 .
Dario

2
@Dario ... 및 형식이 달리 열려 있고 ADT가 아닙니다. :-)
Thomas Jung

1
@ 토마스 : 네, 그것은 단지 실존 적입니다;)
Dario

165

기술적으로 클래스와 케이스 클래스에는 차이가 없습니다. 컴파일러가 케이스 클래스를 사용할 때 일부 내용을 최적화하더라도 마찬가지입니다. 그러나 사례 클래스는 대수 데이터 유형을 구현하는 특정 패턴에 대한 보일러 플레이트를 제거하는 데 사용됩니다 .

이러한 유형의 매우 간단한 예는 나무입니다. 예를 들어 이진 트리는 다음과 같이 구현할 수 있습니다.

sealed abstract class Tree
case class Node(left: Tree, right: Tree) extends Tree
case class Leaf[A](value: A) extends Tree
case object EmptyLeaf extends Tree

이를 통해 다음을 수행 할 수 있습니다.

// DSL-like assignment:
val treeA = Node(EmptyLeaf, Leaf(5))
val treeB = Node(Node(Leaf(2), Leaf(3)), Leaf(5))

// On Scala 2.8, modification through cloning:
val treeC = treeA.copy(left = treeB.left)

// Pretty printing:
println("Tree A: "+treeA)
println("Tree B: "+treeB)
println("Tree C: "+treeC)

// Comparison:
println("Tree A == Tree B: %s" format (treeA == treeB).toString)
println("Tree B == Tree C: %s" format (treeB == treeC).toString)

// Pattern matching:
treeA match {
  case Node(EmptyLeaf, right) => println("Can be reduced to "+right)
  case Node(left, EmptyLeaf) => println("Can be reduced to "+left)
  case _ => println(treeA+" cannot be reduced")
}

// Pattern matches can be safely done, because the compiler warns about
// non-exaustive matches:
def checkTree(t: Tree) = t match {
  case Node(EmptyLeaf, Node(left, right)) =>
  // case Node(EmptyLeaf, Leaf(el)) =>
  case Node(Node(left, right), EmptyLeaf) =>
  case Node(Leaf(el), EmptyLeaf) =>
  case Node(Node(l1, r1), Node(l2, r2)) =>
  case Node(Leaf(e1), Leaf(e2)) =>
  case Node(Node(left, right), Leaf(el)) =>
  case Node(Leaf(el), Node(left, right)) =>
  // case Node(EmptyLeaf, EmptyLeaf) =>
  case Leaf(el) =>
  case EmptyLeaf =>
}

트리는 동일한 구문을 사용하여 패턴 일치를 통해 구성 및 해체합니다. 이는 동일한 방식으로 인쇄되는 방법입니다 (마이너스 공백).

또한 유효하고 안정적인 hashCode를 가지고 있기 때문에 해시 맵 또는 세트와 함께 사용할 수도 있습니다.


71
  • 케이스 클래스는 패턴 일치 가능
  • 케이스 클래스는 자동으로 해시 코드를 정의하고
  • 케이스 클래스는 생성자 인수에 대한 getter 메소드를 자동으로 정의합니다.

(당신은 이미 마지막 것을 제외한 모든 것을 언급했습니다).

이것들은 정규 수업과의 유일한 차이점입니다.


13
생성자 인수에 "var"을 지정하지 않으면 케이스 클래스에 대해 세터가 생성되지 않습니다.이 경우 일반 클래스와 동일한 게터 / 세터 생성을 얻습니다.
Mitch Blevins

1
@ 미치 : 사실, 내 나쁜. 지금 수정했습니다.
sepp2k

두 가지 차이점을 생략했습니다. 내 대답을 참조하십시오.
Shelby Moore III

@MitchBlevins, 일반 클래스 에는 항상 getter / setter 생성 이있는 것은 아닙니다 .
쉘비 무어 III

케이스 클래스는 적용되지 않는 메소드를 정의하므로 패턴 일치가 가능합니다.
Happy Torturer

30

사례 클래스도 인스턴스 Product이므로이 메소드를 상속 한다고 언급 한 사람은 없습니다 .

def productElement(n: Int): Any
def productArity: Int
def productIterator: Iterator[Any]

여기서, productArity클래스 매개 변수 수를 productElement(i)리턴하고 i 번째 매개 변수를 리턴하며 해당 매개 변수를 productIterator반복 할 수 있습니다.


2
그러나 Product1, Product2 등의 인스턴스는 아닙니다.
Jean-Philippe Pellet

27

케이스 클래스에 val생성자 매개 변수 가 있다고 언급 한 사람은 없지만 일반 클래스의 기본값이기도합니다 ( 스칼라 디자인에 불일치라고 생각합니다 ). 다리 오는 " 불변 " 이라고 언급 한 곳을 암시했다 .

var케이스 클래스에 대해 각 생성자 인수를 앞에 추가하여 기본값을 대체 할 수 있습니다 . 그러나 케이스 클래스를 변경 가능하게하면 메소드 equalshashCode메소드가 시간 변형됩니다. [1]

sepp2k는 이미 사례 클래스가 자동으로 생성 equals되고 hashCode메소드가 있다고 언급했습니다 .

또한 아무도 경우 클래스가 자동으로 동반자를 만드는 것이 언급되지 object들어있는 클래스와 같은 이름을 가진 applyunapply방법. 이 apply방법을 사용하면 앞에 추가하지 않고 인스턴스를 생성 할 수 있습니다 new. unapply추출 방법은 다른 언급하는 패턴 매칭을 가능하게한다.

또한 컴파일러는 속도를 최적화합니다 match-case 케이스 클래스 패턴 매칭 [2]가.

[1] 케이스 클래스는 멋지다

[2] 사례 분류 및 추출기, pg 15 .


12

Scala의 케이스 클래스 구성은 보일러 플레이트를 제거하기위한 편의로 볼 수도 있습니다.

케이스 클래스를 구성 할 때 Scala는 다음을 제공합니다.

  • 클래스와 동반자 객체를 만듭니다.
  • 동반 객체 apply는 팩토리 메소드로 사용할 수 있는 메소드를 구현합니다 . 새 키워드를 사용할 필요가없는 구문 설탕 이점을 얻을 수 있습니다.

클래스는 불변이므로 접근자는 클래스의 변수 또는 속성이지만 뮤 테이터는 없습니다 (변수를 변경할 수 없음). 생성자 매개 변수는 공개 읽기 전용 필드로 자동으로 사용 가능합니다. Java bean 구문보다 사용하기가 훨씬 좋습니다.

  • 당신은 또한 얻을 hashCode, equals그리고 toString기본적으로 방법과 equals방법은 구조적으로 객체를 비교합니다. copy방법 (일부 필드 방법에 제공된 새로운 값을 갖는) 개체를 복제 할 수 있도록 생성된다.

앞에서 언급했듯이 가장 큰 장점은 사례 클래스에서 패턴 일치를 할 수 있다는 것입니다. 그 이유 unapply는 케이스 클래스를 해체하여 필드를 추출 할 수있는 메소드를 가져 오기 때문 입니다.


본질적으로 케이스 클래스 (또는 클래스가 인수를 취하지 않으면 케이스 객체)를 만들 때 스칼라에서 얻는 것은 팩토리추출기 로 목적을 제공하는 단일 객체입니다 .


왜 불변 개체의 사본이 필요합니까?
Paŭlo Ebermann

@ PaŭloEbermann copy메소드가 필드를 수정할 수 있기 때문에val x = y.copy(foo="newValue")
Thilo

8

사람들이 이미 말한 것 외에도 class와 사이에는 몇 가지 기본적인 차이점이 있습니다.case class

1. Case Class명시 적으로 필요하지는 않지만 new클래스를 호출해야합니다.new

val classInst = new MyClass(...)  // For classes
val classInst = MyClass(..)       // For case class

2. 기본 생성자 매개 변수는에 비공개 class이지만 공개는case class

// For class
class MyClass(x:Int) { }
val classInst = new MyClass(10)

classInst.x   // FAILURE : can't access

// For caseClass
case class MyClass(x:Int) { }
val classInst = MyClass(10)

classInst.x   // SUCCESS

3. case class가치로 스스로 비교

// case Class
class MyClass(x:Int) { }

val classInst = new MyClass(10)
val classInst2 = new MyClass(10)

classInst == classInst2 // FALSE

// For Case Class
case class MyClass(x:Int) { }

val classInst = MyClass(10)
val classInst2 = MyClass(10)

classInst == classInst2 // TRUE

6

스칼라의 문서 에 따르면 :

사례 클래스는 다음과 같은 일반 클래스입니다.

  • 기본적으로 불변
  • 패턴 매칭을 통해 분해 가능
  • 참조가 아닌 구조적 평등으로 비교
  • 간결하고 작동하는 간결

case 키워드 의 또 다른 기능은 컴파일러가 Java의 익숙한 toString, equals 및 hashCode 메소드를 포함하여 몇 가지 메소드를 자동으로 생성한다는 것입니다.


5

수업:

scala> class Animal(name:String)
defined class Animal

scala> val an1 = new Animal("Padddington")
an1: Animal = Animal@748860cc

scala> an1.name
<console>:14: error: value name is not a member of Animal
       an1.name
           ^

그러나 동일한 코드를 사용하지만 사례 클래스를 사용하는 경우 :

scala> case class Animal(name:String)
defined class Animal

scala> val an2 = new Animal("Paddington")
an2: Animal = Animal(Paddington)

scala> an2.name
res12: String = Paddington


scala> an2 == Animal("fred")
res14: Boolean = false

scala> an2 == Animal("Paddington")
res15: Boolean = true

개인 수업 :

scala> case class Person(first:String,last:String,age:Int)
defined class Person

scala> val harry = new Person("Harry","Potter",30)
harry: Person = Person(Harry,Potter,30)

scala> harry
res16: Person = Person(Harry,Potter,30)
scala> harry.first = "Saily"
<console>:14: error: reassignment to val
       harry.first = "Saily"
                   ^
scala>val saily =  harry.copy(first="Saily")
res17: Person = Person(Saily,Potter,30)

scala> harry.copy(age = harry.age+1)
res18: Person = Person(Harry,Potter,31)

패턴 매칭 :

scala> harry match {
     | case Person("Harry",_,age) => println(age)
     | case _ => println("no match")
     | }
30

scala> res17 match {
     | case Person("Harry",_,age) => println(age)
     | case _ => println("no match")
     | }
no match

대상 : 싱글 톤 :

scala> case class Person(first :String,last:String,age:Int)
defined class Person

scala> object Fred extends Person("Fred","Jones",22)
defined object Fred

5

사례 클래스가 무엇인지 완전히 이해하려면 :

다음과 같은 케이스 클래스 정의를 가정 해 봅시다.

case class Foo(foo:String, bar: Int)

그런 다음 터미널에서 다음을 수행하십시오.

$ scalac -print src/main/scala/Foo.scala

스칼라 2.12.8은 다음을 출력합니다 :

...
case class Foo extends Object with Product with Serializable {

  <caseaccessor> <paramaccessor> private[this] val foo: String = _;

  <stable> <caseaccessor> <accessor> <paramaccessor> def foo(): String = Foo.this.foo;

  <caseaccessor> <paramaccessor> private[this] val bar: Int = _;

  <stable> <caseaccessor> <accessor> <paramaccessor> def bar(): Int = Foo.this.bar;

  <synthetic> def copy(foo: String, bar: Int): Foo = new Foo(foo, bar);

  <synthetic> def copy$default$1(): String = Foo.this.foo();

  <synthetic> def copy$default$2(): Int = Foo.this.bar();

  override <synthetic> def productPrefix(): String = "Foo";

  <synthetic> def productArity(): Int = 2;

  <synthetic> def productElement(x$1: Int): Object = {
    case <synthetic> val x1: Int = x$1;
        (x1: Int) match {
            case 0 => Foo.this.foo()
            case 1 => scala.Int.box(Foo.this.bar())
            case _ => throw new IndexOutOfBoundsException(scala.Int.box(x$1).toString())
        }
  };

  override <synthetic> def productIterator(): Iterator = scala.runtime.ScalaRunTime.typedProductIterator(Foo.this);

  <synthetic> def canEqual(x$1: Object): Boolean = x$1.$isInstanceOf[Foo]();

  override <synthetic> def hashCode(): Int = {
     <synthetic> var acc: Int = -889275714;
     acc = scala.runtime.Statics.mix(acc, scala.runtime.Statics.anyHash(Foo.this.foo()));
     acc = scala.runtime.Statics.mix(acc, Foo.this.bar());
     scala.runtime.Statics.finalizeHash(acc, 2)
  };

  override <synthetic> def toString(): String = scala.runtime.ScalaRunTime._toString(Foo.this);

  override <synthetic> def equals(x$1: Object): Boolean = Foo.this.eq(x$1).||({
      case <synthetic> val x1: Object = x$1;
        case5(){
          if (x1.$isInstanceOf[Foo]())
            matchEnd4(true)
          else
            case6()
        };
        case6(){
          matchEnd4(false)
        };
        matchEnd4(x: Boolean){
          x
        }
    }.&&({
      <synthetic> val Foo$1: Foo = x$1.$asInstanceOf[Foo]();
      Foo.this.foo().==(Foo$1.foo()).&&(Foo.this.bar().==(Foo$1.bar())).&&(Foo$1.canEqual(Foo.this))
  }));

  def <init>(foo: String, bar: Int): Foo = {
    Foo.this.foo = foo;
    Foo.this.bar = bar;
    Foo.super.<init>();
    Foo.super./*Product*/$init$();
    ()
  }
};

<synthetic> object Foo extends scala.runtime.AbstractFunction2 with Serializable {

  final override <synthetic> def toString(): String = "Foo";

  case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);

  case <synthetic> def unapply(x$0: Foo): Option =
     if (x$0.==(null))
        scala.None
     else
        new Some(new Tuple2(x$0.foo(), scala.Int.box(x$0.bar())));

  <synthetic> private def readResolve(): Object = Foo;

  case <synthetic> <bridge> <artifact> def apply(v1: Object, v2: Object): Object = Foo.this.apply(v1.$asInstanceOf[String](), scala.Int.unbox(v2));

  def <init>(): Foo.type = {
    Foo.super.<init>();
    ()
  }
}
...

보시다시피 스칼라 컴파일러는 일반 클래스 Foo와 companion-object를 생성 합니다 Foo.

컴파일 된 클래스를 살펴보고 우리가 얻은 것에 대해 의견을 나눕시다.

  • Foo불변 의 클래스 의 내부 상태
val foo: String
val bar: Int
  • 게터 :
def foo(): String
def bar(): Int
  • 복사 방법 :
def copy(foo: String, bar: Int): Foo
def copy$default$1(): String
def copy$default$2(): Int
  • scala.Product특성 구현 :
override def productPrefix(): String
def productArity(): Int
def productElement(x$1: Int): Object
override def productIterator(): Iterator
  • scala.Equals사례 클래스 인스턴스를 동등성에 대해 비교할 수 있도록 특성을 구현 합니다 ==.
def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
  • java.lang.Object.hashCodeequals-hashcode 계약을 준수하기위한 재정의 :
override <synthetic> def hashCode(): Int
  • 재정의 java.lang.Object.toString:
override def toString(): String
  • new키워드 로 인스턴스화하기위한 생성자 :
def <init>(foo: String, bar: Int): Foo 

Object Foo :- 키워드 apply없는 인스턴스화 방법 new:

case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
  • unupply패턴 일치에서 케이스 클래스 Foo를 사용 하기 위한 추출기 메소드 :
case <synthetic> def unapply(x$0: Foo): Option
  • 인스턴스를 하나 이상 생성하지 않도록 직렬화 해제로부터 객체를 싱글 톤으로 보호하는 방법 :
<synthetic> private def readResolve(): Object = Foo;
  • Foo scala.runtime.AbstractFunction2는 그러한 트릭을 수행하기 위해 확장 합니다.
scala> case class Foo(foo:String, bar: Int)
defined class Foo

scala> Foo.tupled
res1: ((String, Int)) => Foo = scala.Function2$$Lambda$224/1935637221@9ab310b

tupled from object는 2 개의 요소 튜플을 적용하여 새 Foo를 만드는 기능을 반환합니다.

따라서 사례 클래스는 단지 구문 설탕입니다.


4

클래스와 달리 사례 클래스는 데이터를 보유하는 데 사용됩니다.

케이스 클래스는 데이터 중심 애플리케이션에 유연하므로 케이스 클래스에서 데이터 필드를 정의하고 동반자 오브젝트에서 비즈니스 로직을 정의 할 수 있습니다. 이런 방식으로 비즈니스 로직에서 데이터를 분리합니다.

copy 메소드를 사용하면 소스에서 필요한 특성 중 일부 또는 전부를 상속하고 원하는대로 변경할 수 있습니다.


3

케이스 클래스 컴패니언 객체에는 tupled방어 유형이 있으며, 유형은 다음과 같습니다.

case class Person(name: String, age: Int)
//Person.tupled is def tupled: ((String, Int)) => Person

내가 찾을 수있는 유일한 유스 케이스는 튜플에서 사례 클래스를 생성 해야하는 경우입니다.

val bobAsTuple = ("bob", 14)
val bob = (Person.apply _).tupled(bobAsTuple) //bob: Person = Person(bob,14)

튜플없이 객체를 직접 생성하여 동일한 작업을 수행 할 수 있지만 arity 20 (튜플이 20 인 튜플) 인 튜플 목록으로 표현 된 데이터 집합이 튜플 링을 사용하는 경우가 가장 좋습니다.


3

경우 클래스는 함께 사용 할 수있는 클래스입니다 match/case문.

def isIdentityFun(term: Term): Boolean = term match {
  case Fun(x, Var(y)) if x == y => true
  case _ => false
}

당신은 그것을 참조 case 그 두번째 매개 변수 var에있다 클래스 재미의 인스턴스옵니다. 이것은 매우 훌륭하고 강력한 구문이지만 어떤 클래스의 인스턴스에서도 작동 할 수 없으므로 케이스 클래스에 대한 제한이 있습니다. 이러한 제한 사항을 준수하면 해시 코드와 등호를 자동으로 정의 할 수 있습니다.

"패턴 매칭을 통한 재귀 적 분해 메커니즘"이라는 애매한 문구는 단지 "와 작동한다"를 의미한다 case. (실제로 뒤 따르는 인스턴스는 뒤 따르는 인스턴스 match와 비교 (일치)합니다 case. 스칼라는 두 인스턴스를 모두 분해해야하며, 구성 요소를 재귀 적으로 분해해야합니다.)

어떤 사례 클래스 가 유용합니까? 대수 데이터 형식에 대한 위키 백과 문서 이 좋은 고전 예, 목록과 나무를 제공합니다. 현대 함수형 언어에서는 대수 데이터 형식 (비교 방법을 포함하여)을 지원해야합니다.

어떤 사례 클래스 가 유용 하지 않습니까? 일부 객체에는 상태가 있으며 코드와 같은 코드 connection.setConnectTimeout(connectTimeout)는 사례 클래스가 아닙니다.

이제 Scala 둘러보기 : 사례 클래스를 읽을 수 있습니다 .


2

나는 모든 답변이 클래스와 사례 클래스에 대한 의미 론적 설명을 제공했다고 생각합니다. 이는 매우 관련성이 있지만 스칼라의 모든 초보자는 사례 클래스를 만들 때 어떤 일이 발생하는지 알아야합니다. 나는 이것을 썼다 답변 하여 간단히 사례 클래스를 설명합니다.

모든 프로그래머는 사전 구축 된 기능을 사용하는 경우 비교적 적은 코드를 작성하므로 가장 최적화 된 코드를 작성할 수는 있지만 큰 책임이 따릅니다. 따라서 미리 작성된 기능을 매우주의해서 사용하십시오.

일부 개발자는 클래스 파일을 분해하여 볼 수있는 추가 20 개의 메소드로 인해 케이스 클래스 작성을 피합니다.

제발 당신이 경우 클래스 내부의 모든 방법을 확인하려면이 링크를 참조하십시오 .


1
  • 케이스 클래스는 apply 및 unapply 메소드로 compagnon 오브젝트를 정의합니다
  • 사례 클래스는 직렬화 가능
  • 케이스 클래스는 equals와 같은 코드를 정의합니다
  • 생성자의 모든 속성은 val (syntactic sugar)입니다.

1

주요 기능 중 일부 case classes는 다음과 같습니다.

  1. 케이스 클래스는 변경할 수 없습니다.
  2. new키워드 없이 사례 클래스를 인스턴스화 할 수 있습니다 .
  3. 케이스 클래스는 값으로 비교할 수 있습니다

스칼라 문서에서 가져온 스칼라 바이올린의 샘플 스칼라 코드.

https://scalafiddle.io/sf/34XEQyE/0

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