Jaka jest różnica między klasą przypadków i klasą Scali?


440

Szukałem w Google, aby znaleźć różnice między case classa a class. Wszyscy wspominają, że jeśli chcesz wykonać dopasowanie wzorców w klasie, użyj klasy case. W przeciwnym razie użyj klas, a także wspominając o dodatkowych korzyściach, takich jak równość i zastępowanie kodu skrótu. Ale czy są to jedyne powody, dla których należy używać klasy case zamiast klasy?

Myślę, że powinien istnieć jakiś bardzo ważny powód tej funkcji w Scali. Jakie jest wyjaśnienie lub czy istnieje zasób, z którego można dowiedzieć się więcej o klasach przypadków Scala?

Odpowiedzi:


394

Klasy przypadków można postrzegać jako zwykłe i niezmienne obiekty przechowujące dane, które powinny zależeć wyłącznie od ich argumentów konstruktora .

Ta funkcjonalna koncepcja pozwala nam na to

  • użyj kompaktowej składni inicjalizacyjnej ( Node(1, Leaf(2), None)))
  • rozłóż je za pomocą dopasowania wzorca
  • mają niejawnie zdefiniowane porównania równości

W połączeniu z dziedziczeniem klasy przypadków są używane do naśladowania algebraicznych typów danych .

Jeśli obiekt wykonuje obliczenia stanowe od wewnątrz lub wykazuje inne złożone zachowania, powinna to być zwykła klasa.


11
@Teja: W pewien sposób. ADT'S trochę parametryzowane stałe teksty , niezwykle wydajne i typesafe.
Dario

8
Zapieczętowane klasy przypadków są używane do naśladowania algebraicznych typów danych. W przeciwnym razie liczba podklas nie jest ograniczona.
Thomas Jung,

6
@Thomas: Prawidłowo wypowiedziane, klasy przypadków wywodzące się z zapieczętowanych klas abstrakcyjnych naśladują zamknięte typy algebraiczne, podczas gdy ADT jest inaczej otwarty .
Dario

2
@Dario ... a typ jest inaczej otwarty, a nie ADT. :-)
Thomas Jung,

1
@Thomas: Tak, to tylko egzystencjalny;)
Dario

165

Technicznie nie ma różnicy między klasą a klasą spraw - nawet jeśli kompilator optymalizuje niektóre rzeczy podczas używania klas spraw. Jednak klasa przypadków służy do usuwania płyty kotła dla określonego wzorca, który implementuje algebraiczne typy danych .

Bardzo prostym przykładem takich typów są drzewa. Na przykład drzewo binarne można zaimplementować w następujący sposób:

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

Dzięki temu możemy wykonać następujące czynności:

// 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 =>
}

Zauważ, że drzewa konstruują i dekonstruują (poprzez dopasowanie wzorca) przy użyciu tej samej składni, co jest dokładnie tak, jak są drukowane (bez spacji).

Można ich również używać z mapami lub zestawami skrótów, ponieważ mają one prawidłowy, stabilny kod skrótu.


71
  • Klasy spraw można dopasować do wzorca
  • Klasy spraw automatycznie definiują kod skrótu i ​​są równe
  • Klasy spraw automatycznie definiują metody pobierające dla argumentów konstruktora.

(Wspomniałeś już o wszystkich oprócz ostatniego).

To jedyne różnice w stosunku do zwykłych klas.


13
Settery nie są generowane dla klas obserwacji, chyba że w argumencie konstruktora podano „var”, w którym to przypadku generowane jest to samo generowanie gettera / settera, co zwykłe klasy.
Mitch Blevins

1
@Mitch: Prawda, mój zły. Naprawiono teraz.
sepp2k

Pominąłeś dwie różnice, patrz moja odpowiedź.
Shelby Moore III

@MitchBlevins, regularne klasy nie zawsze mają generowanie getter / setter.
Shelby Moore III,

Klasy przypadków definiują niepoprawną metodę, dlatego można je dopasować do wzorca.
Szczęśliwy oprawca

30

Nikt nie wspominał, że klasy przypadków są również instancjami Producti dlatego dziedziczą następujące metody:

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

gdzie productArityzwraca liczbę parametrów klasy, productElement(i)zwraca i- ty parametr i productIteratorumożliwia iterację przez nie.


2
Nie są to jednak wystąpienia Product1, Product2 itp.
Jean-Philippe Pellet,

27

Nikt nie wspominał, że klasy przypadków mają valparametry konstruktora, ale jest to również domyślne dla klas regularnych (co moim zdaniem jest niespójne w projektowaniu Scali). Dario sugerował, że tam, gdzie zauważył, są „ niezmienni ”.

Uwaga: możesz zastąpić wartość domyślną, wstawiając argument każdego konstruktora vardla klas spraw. Jednak, dzięki czemu zajęcia przypadków zmienny powoduje ich equalsand hashCodemetod być wariant czas. [1]

sepp2k już wspomniał, że klasy spraw automatycznie generują equalsi hashCodemetody.

Nikt też nie wspomniał, że klasy przypadków automatycznie tworzą towarzysza objecto tej samej nazwie co klasa, która zawiera applyi unapplymetody. Ta applymetoda umożliwia konstruowanie instancji bez konieczności wcześniejszego korzystania z nich new. Metoda unapplyekstraktora umożliwia dopasowanie wzorca, o którym wspominali inni.

Również kompilator optymalizuje prędkość match- casedopasowanie wzorca dla klas przypadków [2].

[1] Klasy przypadków są fajne

[2] Klasy przypadków i ekstraktory, str . 15 .


12

Konstrukcja klasy skrzynek w Scali może być również postrzegana jako wygoda do usunięcia płyty kotłowej.

Podczas konstruowania klasy skrzynek Scala daje następujące informacje.

  • Tworzy klasę, a także obiekt towarzyszący
  • Jego obiekt towarzyszący implementuje applymetodę, której można użyć jako metody fabrycznej. Zaletą cukru syntaktycznego jest brak konieczności używania nowego słowa kluczowego.

Ponieważ klasa jest niezmienna, dostajesz akcesoria, które są tylko zmiennymi (lub właściwościami) klasy, ale nie mają mutatorów (więc nie ma możliwości zmiany zmiennych). Parametry konstruktora są automatycznie dostępne jako publiczne pola tylko do odczytu. O wiele ładniejszy w użyciu niż Java Bean.

  • Można również uzyskać hashCode, equalsoraz toStringmetody domyślnie i equalsmetoda porównuje obiektu strukturalnie. copyMetoda jest generowany, aby móc sklonować obiektu (z niektóre pola posiadające nowe wartości podane metody).

Największą zaletą, jak wspomniano wcześniej, jest fakt, że można dopasowywać wzorce do klas przypadków. Powodem tego jest to, że otrzymujesz unapplymetodę, która pozwala zdekonstruować klasę przypadków w celu wyodrębnienia jej pól.


Zasadniczo to, co otrzymujesz od Scali podczas tworzenia klasy sprawy (lub obiektu sprawy, jeśli twoja klasa nie przyjmuje argumentów), jest obiektem singleton, który służy temu celowi jako fabryka i jako ekstraktor .


Dlaczego potrzebujesz kopii niezmiennego obiektu?
Paŭlo Ebermann

@ PaŭloEbermann Ponieważ copymetoda może modyfikować pola:val x = y.copy(foo="newValue")
Thilo

8

Oprócz tego, co ludzie już powiedzieli, istnieją pewne podstawowe różnice między classicase class

1. Case Classnie wymaga jawnego new, podczas gdy klasa musi być wywoływana za pomocąnew

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

2. Domyślne parametry konstruktora są prywatne w class, a jego publiczne wcase 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 classporównać się pod względem wartości

// 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

Zgodnie z dokumentacją Scali :

Klasy przypadków to tylko zwykłe klasy, które są:

  • Domyślnie niezmienne
  • Rozkładalny poprzez dopasowanie wzoru
  • Porównywany przez równość strukturalną zamiast przez odniesienie
  • Zwięzły, aby utworzyć wystąpienie i działać dalej

Inną cechą słowa kluczowego case jest to, że kompilator automatycznie generuje dla nas kilka metod, w tym znane metody toString, equals i hashCode w Javie.


5

Klasa:

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
           ^

Ale jeśli używamy tego samego kodu, ale używamy klasy przypadku:

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

Klasa osoby:

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)

Dopasowanie wzoru:

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

obiekt: singleton:

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

Aby uzyskać maksymalne zrozumienie klasy przypadków:

załóżmy następującą definicję klasy przypadku:

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

a następnie wykonaj następujące czynności w terminalu:

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

Scala 2.12.8 wyświetli:

...
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>();
    ()
  }
}
...

Jak widzimy, kompilator Scala produkuje zwykłą klasę Fooi obiekt towarzyszący Foo.

Przejdźmy przez skompilowaną klasę i skomentujmy, co mamy:

  • wewnętrzny stan Fooklasy, niezmienny:
val foo: String
val bar: Int
  • pobierający:
def foo(): String
def bar(): Int
  • metody kopiowania:
def copy(foo: String, bar: Int): Foo
def copy$default$1(): String
def copy$default$2(): Int
  • implementacja scala.Productcechy:
override def productPrefix(): String
def productArity(): Int
def productElement(x$1: Int): Object
override def productIterator(): Iterator
  • implementacja scala.Equalscechy sprawiającej, że instancje klas spraw są porównywalne pod względem równości poprzez ==:
def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
  • przesłanianie java.lang.Object.hashCodeza przestrzeganie umowy equals-hashcode:
override <synthetic> def hashCode(): Int
  • zastępowanie java.lang.Object.toString:
override def toString(): String
  • konstruktor tworzenia instancji według newsłowa kluczowego:
def <init>(foo: String, bar: Int): Foo 

Object Foo: - metoda applytworzenia instancji bez newsłowa kluczowego:

case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
  • Metoda ekstraktora unupplydo użycia klasy przypadków Foo w dopasowaniu wzorca:
case <synthetic> def unapply(x$0: Foo): Option
  • metoda ochrony obiektu jako singletonu przed deserializacją za niedopuszczenie do wygenerowania jeszcze jednej instancji:
<synthetic> private def readResolve(): Object = Foo;
  • obiekt Foo rozszerza się scala.runtime.AbstractFunction2o wykonanie takiej sztuczki:
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 z obiektu zwraca funkcję tworzenia nowego Foo przez zastosowanie krotki 2 elementów.

Tak więc klasa przypadków to po prostu cukier składniowy.


4

W przeciwieństwie do klas, klasy przypadków służą tylko do przechowywania danych.

Klasy spraw są elastyczne dla aplikacji skoncentrowanych na danych, co oznacza, że ​​możesz definiować pola danych w klasie spraw i definiować logikę biznesową w obiekcie towarzyszącym. W ten sposób oddzielasz dane od logiki biznesowej.

Za pomocą metody kopiowania możesz odziedziczyć dowolne lub wszystkie wymagane właściwości ze źródła i możesz je dowolnie zmieniać.


3

Nikt nie wspominał, że obiekt towarzyszący klasie przypadków ma tupledobronę, która ma typ:

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

Jedyny przypadek użycia, jaki mogę znaleźć, to to, kiedy musisz zbudować klasę sprawy z krotki, przykład:

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

Możesz zrobić to samo, bez krotek, tworząc obiekt bezpośrednio, ale jeśli twoje zbiory danych wyrażone jako lista krotek z arity 20 (krotka z 20 elementami), możesz użyć krotek to twój wybór.


3

Przypadek klasa to klasa, która może być używana z match/caseoświadczeniem.

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

Po tym casenastępuje instancja klasy Fun, której drugim parametrem jest Var. Jest to bardzo ładna i potężna składnia, ale nie może działać z instancjami żadnej klasy, dlatego istnieją pewne ograniczenia dotyczące klas spraw. A jeśli te ograniczenia są przestrzegane, możliwe jest automatyczne zdefiniowanie kodu skrótu i ​​równe.

Niejasne wyrażenie „rekurencyjny mechanizm rozkładu poprzez dopasowanie wzorca” oznacza po prostu „działa z case”. (Rzeczywiście, instancja, po której następuje, matchjest porównywana z (dopasowaną do) instancją, która następuje case, Scala musi je rozłożyć i musi rekurencyjnie rozłożyć to, z czego są wykonane.)

Do jakich klas spraw są przydatne? Artykuł w Wikipedii na temat Algebraicznych typów danych podaje dwa dobre klasyczne przykłady, listy i drzewa. Obsługa algebraicznych typów danych (w tym umiejętność ich porównywania) jest niezbędna w każdym nowoczesnym języku funkcjonalnym.

W jakich klasach przypadków nie są przydatne? Niektóre obiekty mają stan, podobnie jak kod connection.setConnectTimeout(connectTimeout)nie dotyczy klas wielkości liter.

A teraz możesz przeczytać A Tour of Scala: Case Classes


2

Myślę, że ogólnie wszystkie odpowiedzi zawierają semantyczne wyjaśnienia dotyczące klas i klas przypadków. Może to być bardzo istotne, ale każdy początkujący w scala powinien wiedzieć, co się stanie, gdy utworzysz klasę przypadków. Napisałem odpowiedź, która w skrócie wyjaśnia klasę przypadków.

Każdy programista powinien wiedzieć, że jeśli korzysta z gotowych funkcji, to pisze stosunkowo mniej kodu, co umożliwia im pisanie najbardziej zoptymalizowanego kodu, ale wiąże się to z wielką odpowiedzialnością. Dlatego używaj gotowych funkcji z dużą ostrożnością.

Niektórzy programiści unikają pisania klas przypadków ze względu na dodatkowe 20 metod, które można zobaczyć po rozmontowaniu pliku klasy.

Odwołaj się do tego linku, jeśli chcesz sprawdzić wszystkie metody w klasie przypadków .


1
  • Klasy przypadków definiują obiekt Compagnon za pomocą metod Apply i Unapply
  • Klasy przypadków rozszerzają Serializable
  • Klasy przypadków definiują równe metody hashCode i metody kopiowania
  • Wszystkie atrybuty konstruktora to val (cukier syntaktyczny)

1

Niektóre z kluczowych funkcji case classessą wymienione poniżej

  1. klasy przypadków są niezmienne.
  2. Można utworzyć instancję klas spraw bez newsłowa kluczowego.
  3. klasy przypadków można porównać pod względem wartości

Przykładowy kod Scala na skrzypcach Scala, zaczerpnięty z dokumentów Scala.

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.