Apa perbedaan antara kelas kasus dan kelas Scala?


440

Saya mencari di Google untuk menemukan perbedaan antara a case classdan a class. Semua orang menyebutkan bahwa ketika Anda ingin melakukan pencocokan pola di kelas, gunakan kelas kasus. Selain itu gunakan kelas dan juga menyebutkan beberapa fasilitas tambahan seperti equals dan kode hash menimpa. Tetapi apakah ini satu-satunya alasan mengapa seseorang harus menggunakan kelas kasus alih-alih kelas?

Saya kira harus ada alasan yang sangat penting untuk fitur ini di Scala. Apa penjelasannya atau apakah ada sumber untuk mempelajari lebih lanjut tentang kelas kasus Scala?

Jawaban:


394

Kelas kasus dapat dilihat sebagai objek penampung data yang sederhana dan tidak dapat diubah yang harus bergantung pada argumen konstruktor secara eksklusif .

Konsep fungsional ini memungkinkan kita untuk

  • gunakan sintaks inisialisasi yang ringkas ( Node(1, Leaf(2), None)))
  • dekomposisi mereka menggunakan pencocokan pola
  • memiliki perbandingan kesetaraan yang didefinisikan secara implisit

Dalam kombinasi dengan pewarisan, kelas kasus digunakan untuk meniru tipe data aljabar .

Jika suatu objek melakukan perhitungan stateful di dalam atau menunjukkan jenis perilaku kompleks lainnya, itu harus kelas biasa.


11
@Teja: Dalam beberapa cara. ADT adalah enum yang agak parameter , sangat kuat dan aman.
Dario

8
Kelas kasus tertutup digunakan untuk meniru tipe data aljabar. Kalau tidak, jumlah subclass tidak terbatas.
Thomas Jung

6
@ Thomas: Diucapkan dengan benar, kelas kasus yang berasal dari kelas abstrak tersegel meniru tipe data aljabar tertutup sedangkan ADT terbuka .
Dario

2
@Dario ... dan jenisnya terbuka dan tidak dan ADT. :-)
Thomas Jung

1
@ Thomas: Ya, itu hanya eksistensial;)
Dario

165

Secara teknis, tidak ada perbedaan antara kelas dan kelas kasus - bahkan jika kompilator tidak mengoptimalkan beberapa hal saat menggunakan kelas kasus. Namun, kelas kasus digunakan untuk menghilangkan pelat ketel untuk pola tertentu, yang menerapkan tipe data aljabar .

Contoh yang sangat sederhana dari jenis tersebut adalah pohon. Pohon biner, misalnya, dapat diimplementasikan seperti ini:

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

Itu memungkinkan kami untuk melakukan hal berikut:

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

Perhatikan bahwa pohon membangun dan mendekonstruksi (melalui pencocokan pola) dengan sintaks yang sama, yang juga persis bagaimana mereka dicetak (spasi minus).

Dan mereka juga dapat digunakan dengan peta atau set hash, karena mereka memiliki kode hash yang valid dan stabil.


71
  • Kelas kasus dapat dicocokkan dengan pola
  • Kelas kasus secara otomatis mendefinisikan kode hash dan sama dengan
  • Kelas kasus secara otomatis menentukan metode pengambil untuk argumen konstruktor.

(Anda sudah menyebutkan semua kecuali yang terakhir).

Itulah satu-satunya perbedaan dengan kelas reguler.


13
Setter tidak dihasilkan untuk kelas kasus kecuali "var" ditentukan dalam argumen konstruktor, dalam hal ini Anda mendapatkan generasi pengambil / penyetel yang sama dengan kelas reguler.
Mitch Blevins

1
@ Nyonya: Benar, salahku. Diperbaiki sekarang
sepp2k

Anda menghilangkan 2 perbedaan, lihat jawaban saya.
Shelby Moore III

@MitchBlevins, kelas reguler tidak selalu memiliki generasi pengambil / penyetel.
Shelby Moore III

Kelas kasus mendefinisikan metode yang tidak berlaku itu sebabnya mereka dapat dicocokkan dengan pola.
Selamat Torturer

30

Tidak ada yang menyebutkan bahwa kelas kasus juga merupakan contoh Productdan dengan demikian mewarisi metode ini:

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

di mana productAritymengembalikan jumlah parameter kelas, productElement(i)mengembalikan parameter ke- i , dan memungkinkan iterasi melalui mereka.productIterator


2
Namun mereka bukan contoh dari Product1, Product2, dll.
Jean-Philippe Pellet

27

Tidak ada yang menyebutkan bahwa kelas kasus memiliki valparameter konstruktor namun ini juga merupakan standar untuk kelas reguler (yang saya pikir merupakan ketidakkonsistenan dalam desain Scala). Dario tersirat seperti itu di mana ia mencatat mereka " tidak berubah ".

Catatan Anda bisa mengganti default dengan menambahkan argumen setiap konstruktor dengan varuntuk kelas kasus. Namun, membuat kelas kasus bisa berubah menyebabkan mereka equalsdan hashCodemetode untuk waktu varian. [1]

sepp2k telah menyebutkan bahwa kelas kasus secara otomatis menghasilkan equalsdan hashCodemetode.

Juga tidak ada yang menyebutkan bahwa kelas kasus secara otomatis membuat pendamping objectdengan nama yang sama dengan kelas, yang berisi applydan unapplymetode. The applyMetode memungkinkan membangun contoh tanpa mengawali dengan new. The unapplyMetode extractor memungkinkan pencocokan pola yang lain disebutkan.

Juga compiler mengoptimalkan kecepatan match- casepencocokan pola untuk kelas kasus [2].

[1] Kelas Kasus Keren

[2] Kelas Kasus dan Ekstraktor, hal 15 .


12

Konstruksi case case di Scala juga dapat dilihat sebagai kenyamanan untuk menghapus beberapa boilerplate.

Saat membangun kelas kasus, Scala memberi Anda yang berikut.

  • Itu menciptakan kelas serta objek pendampingnya
  • Objek pendampingnya mengimplementasikan applymetode yang dapat Anda gunakan sebagai metode pabrik. Anda mendapatkan keuntungan gula sintaksis karena tidak harus menggunakan kata kunci baru.

Karena kelas tidak berubah Anda mendapatkan pengakses, yang hanya variabel (atau properti) dari kelas tetapi tidak ada mutator (jadi tidak ada kemampuan untuk mengubah variabel). Parameter konstruktor tersedia secara otomatis untuk Anda sebagai bidang hanya baca publik. Jauh lebih baik untuk digunakan daripada konstruk kacang Jawa.

  • Anda juga mendapatkan hashCode,, equalsdan toStringmetode secara default dan equalsmetode membandingkan objek secara struktural. Suatu copymetode dihasilkan untuk dapat mengkloning suatu objek (dengan beberapa bidang memiliki nilai baru yang disediakan untuk metode tersebut).

Keuntungan terbesar seperti yang telah disebutkan sebelumnya adalah fakta bahwa Anda dapat mencocokkan pola pada kelas kasus. Alasannya adalah karena Anda mendapatkan unapplymetode yang memungkinkan Anda mendekonstruksi kelas kasus untuk mengekstrak bidangnya.


Pada dasarnya apa yang Anda dapatkan dari Scala ketika membuat kelas kasus (atau objek kasus jika kelas Anda tidak berdebat) adalah objek tunggal yang berfungsi sebagai pabrik dan sebagai ekstraktor .


Mengapa Anda membutuhkan salinan objek yang tidak dapat diubah?
Paŭlo Ebermann

@ PaŭloEbermann Karena copymetode ini dapat memodifikasi bidang:val x = y.copy(foo="newValue")
Thilo

8

Terlepas dari apa yang telah dikatakan orang, ada beberapa perbedaan mendasar antara classdancase class

1. Case Classtidak perlu eksplisit new, sedangkan kelas perlu dipanggil dengannew

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

2. Dengan parameter konstruktor default bersifat privat class, sementara publik dalamcase 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 classmembandingkan diri mereka dengan nilai

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

Menurut dokumentasi Scala :

Kelas kasus adalah kelas biasa yang:

  • Tidak dapat diubah secara default
  • Dapat diurai melalui pencocokan pola
  • Dibandingkan dengan persamaan struktural, bukan dengan referensi
  • Singkat untuk instantiate dan beroperasi

Fitur lain dari kata kunci case adalah kompiler yang secara otomatis menghasilkan beberapa metode untuk kami, termasuk metode toString, equals, dan hashCode yang umum di Jawa.


5

Kelas:

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
           ^

Tetapi jika kita menggunakan kode yang sama tetapi menggunakan kelas kasus:

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

Kelas orang:

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)

Pencocokan Pola:

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

objek: 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

Untuk memiliki pemahaman akhir tentang apa itu kelas kasus:

mari kita asumsikan definisi kelas kasus berikut:

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

dan kemudian lakukan hal berikut di terminal:

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

Scala 2.12.8 akan menampilkan:

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

Seperti yang dapat kita lihat Scala compiler menghasilkan kelas reguler Foodan objek-pendamping Foo.

Mari kita melalui kelas yang dikompilasi dan mengomentari apa yang kita dapatkan:

  • keadaan internal Fookelas, tidak berubah:
val foo: String
val bar: Int
  • getter:
def foo(): String
def bar(): Int
  • metode penyalinan:
def copy(foo: String, bar: Int): Foo
def copy$default$1(): String
def copy$default$2(): Int
  • menerapkan scala.Productsifat:
override def productPrefix(): String
def productArity(): Int
def productElement(x$1: Int): Object
override def productIterator(): Iterator
  • menerapkan scala.Equalssifat untuk instance kelas kasus yang sebanding dengan kesetaraan dengan ==:
def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
  • override java.lang.Object.hashCodeuntuk mematuhi kontrak equals-kode hash:
override <synthetic> def hashCode(): Int
  • mengesampingkan java.lang.Object.toString:
override def toString(): String
  • constructor untuk instantiation dengan newkata kunci:
def <init>(foo: String, bar: Int): Foo 

Objek Foo: - metode applyuntuk instantiasi tanpa newkata kunci:

case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
  • metode ekstraktor unupplyuntuk menggunakan kelas kasus Foo dalam pencocokan pola:
case <synthetic> def unapply(x$0: Foo): Option
  • metode untuk melindungi objek sebagai singleton dari deserialisasi karena tidak membiarkan menghasilkan satu lagi contoh:
<synthetic> private def readResolve(): Object = Foo;
  • objek Foo diperluas scala.runtime.AbstractFunction2untuk melakukan trik seperti itu:
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 dari objek mengembalikan funtion untuk membuat Foo baru dengan menerapkan tuple 2 elemen.

Jadi kelas kasus hanyalah gula sintaksis.


4

Tidak seperti kelas, kelas kasus hanya digunakan untuk menyimpan data.

Kelas kasus fleksibel untuk aplikasi data-sentris, yang berarti Anda bisa mendefinisikan bidang data dalam kelas kasus dan mendefinisikan logika bisnis di objek pendamping. Dengan cara ini, Anda memisahkan data dari logika bisnis.

Dengan metode salin, Anda dapat mewarisi salah satu atau semua properti yang diperlukan dari sumber dan dapat mengubahnya sesuka Anda.


3

Tidak ada yang menyebutkan bahwa objek kelas pendamping kasus memiliki tupledpembelaan, yang memiliki tipe:

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

Satu-satunya use case yang dapat saya temukan adalah ketika Anda perlu membuat kelas case dari tuple, contoh:

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

Anda dapat melakukan hal yang sama, tanpa tupled, dengan membuat objek secara langsung, tetapi jika dataset Anda dinyatakan sebagai daftar tuple dengan arity 20 (tuple dengan 20 elemen), mungkin menggunakan tupled adalah pilihan Anda.


3

Kelas kasus adalah kelas yang dapat digunakan dengan match/casepernyataan.

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

Anda lihat bahwa casediikuti oleh instance dari Fun kelas yang parameter ke-2-nya adalah Var. Ini adalah sintaks yang sangat bagus dan kuat, tetapi tidak dapat bekerja dengan instance dari kelas apa pun, oleh karena itu ada beberapa batasan untuk kelas kasus. Dan jika batasan ini dipatuhi, adalah mungkin untuk secara otomatis mendefinisikan kode hash dan yang sederajat.

Ungkapan yang tidak jelas "mekanisme dekomposisi rekursif melalui pencocokan pola" berarti "bekerja dengan baik case". (Memang, instance diikuti oleh matchdibandingkan dengan (cocok dengan) instance yang mengikuticase , Scala harus membusuk mereka berdua, dan harus secara rekursif menguraikan apa yang mereka terbuat dari.)

Untuk apa kelas kasus berguna? The Wikipedia artikel tentang Jenis aljabar data memberikan dua baik klasik contoh, daftar dan pohon. Dukungan untuk tipe data aljabar (termasuk mengetahui bagaimana membandingkannya) adalah suatu keharusan untuk bahasa fungsional modern.

Kelas kasus apa yang tidak berguna? Beberapa objek memiliki status, kode seperti connection.setConnectTimeout(connectTimeout)ini bukan untuk kelas kasus.

Dan sekarang Anda dapat membaca A Tour of Scala: Class Case


2

Saya pikir secara keseluruhan semua jawaban telah memberikan penjelasan semantik tentang kelas dan kelas kasus. Ini bisa sangat relevan, tetapi setiap pemula di scala harus tahu apa yang terjadi ketika Anda membuat kelas kasus. Saya sudah menulis ini jawaban , yang menjelaskan kelas kasus secara singkat.

Setiap programmer harus tahu bahwa jika mereka menggunakan fungsi pre-built, maka mereka menulis kode yang relatif lebih sedikit, yang memungkinkan mereka dengan memberikan kekuatan untuk menulis kode yang paling optimal, tetapi kekuasaan datang dengan tanggung jawab besar. Jadi, gunakan fungsi prebuilt dengan sangat hati-hati.

Beberapa pengembang menghindari penulisan kelas kasus karena 20 metode tambahan, yang dapat Anda lihat dengan membongkar file kelas.

Silakan merujuk tautan ini jika Anda ingin memeriksa semua metode di dalam kelas kasus .


1
  • Kelas kasus mendefinisikan objek compagnon dengan metode yang berlaku dan tidak berlaku
  • Kelas kasus meluas Serializable
  • Kelas kasus mendefinisikan sama dengan kode hash dan metode salin
  • Semua atribut konstruktor adalah val (gula sintaksis)

1

Beberapa fitur utama case classestercantum di bawah ini

  1. kelas kasus tidak dapat diubah.
  2. Anda dapat membuat instance kelas kasus tanpa newkata kunci.
  3. kelas kasus dapat dibandingkan dengan nilai

Contoh kode scala pada scala fiddle, diambil dari scala docs.

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

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.