Яка різниця між класом випадку Scala та класом?


440

Я шукав в Google, щоб знайти відмінності між a case classі a class. Усі зазначають, що, коли ви хочете виконати відповідність шаблонів у класі, використовуйте клас регістру. В іншому випадку використовуйте класи, а також згадуйте про деякі додаткові переваги, наприклад, рівне і переосмислення хеш-коду. Але це єдині причини, чому слід використовувати клас справ замість класу?

Я думаю, що у Scala має бути якась дуже важлива причина цієї функції. Яке пояснення чи є ресурс, щоб дізнатися більше про класи випадків Scala?

Відповіді:


394

Класи справ можуть розглядатися як звичайні та незмінні об'єкти, що зберігають дані, які повинні залежати виключно від їх аргументів конструктора .

Ця функціональна концепція дозволяє нам

  • використовувати компактний синтаксис ініціалізації ( Node(1, Leaf(2), None)))
  • розкласти їх, використовуючи відповідність шаблонів
  • мають порівняння рівності неявно визначені

У поєднанні з успадкуванням класи кейсів використовуються для імітації алгебраїчних типів даних .

Якщо об’єкт виконує стаціонарні обчислення зсередини або демонструє інші види складної поведінки, це повинен бути звичайний клас.


11
@Teja: У чомусь. ADT - це якісь параметризовані переліки , надзвичайно потужні та безпечні.
Даріо

8
Запечатані класи справ використовуються для імітації алгебраїчних типів даних. Інакше кількість підкласів не обмежена.
Томас Юнг

6
@Thomas: Правильно кажучи, класи класів, що походять від запечатаних абстрактних класів, імітують закриті алгебраїчні типи даних, тоді як ADT в іншому випадку відкритий .
Даріо

2
@Dario ... і тип інакше відкритий, а не і ADT. :-)
Томас Юнг

1
@Thomas: Так, це просто екзистенція;)
Даріо,

165

Технічно немає різниці між класом і класом регістру - навіть якщо компілятор оптимізує деякі речі при використанні класів case. Однак клас корпусу використовується для усунення пластини котла за певною схемою, яка реалізує алгебраїчні типи даних .

Дуже простий приклад таких типів - дерева. Наприклад, бінарне дерево може бути реалізовано так:

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

Зауважте, що дерева будують та деконструюють (через відповідність візерунка) з однаковим синтаксисом, який також точно друкується (мінус пробіли).

А також їх можна використовувати з хеш-картами або наборами, оскільки вони мають дійсний стабільний хеш-код.


71
  • Класи кейсів можуть бути узорними
  • Класи справ автоматично визначають хеш-код і дорівнюють
  • Класи справ автоматично визначають методи getter для аргументів конструктора.

(Ви вже згадали всіх, крім останнього).

Це єдині відмінності від звичайних занять.


13
Setters не генерується для класів випадків, якщо в аргументі конструктора не вказано "var", і в цьому випадку ви отримуєте те саме покоління геттера / сеттера, що і звичайні класи.
Мітч Блевінс

1
@Mitch: Правда, мені погано. Виправлено зараз.
sepp2k

Ви пропустили 2 відмінності, дивіться мою відповідь.
Шелбі Мур III

@MitchBlevins, регулярні заняття не завжди мають генерацію геттера / сетера.
Шелбі Мур III,

Класи кейсів визначають метод непридатності, тому їх можна узгодити за шаблоном.
Щасливий катувач

30

Ніхто не згадував, що класи випадків також є примірниками Productі, таким чином, успадковують ці методи:

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

де productArityповертає кількість параметрів класу, productElement(i)повертає i- й параметр і productIteratorдозволяє повторювати через них.


2
Однак вони не є примірниками Product1, Product2 тощо.
Жан-Філіп Пеллет

27

Ніхто не згадував, що класи класів мають valпараметри конструктора, але це також є типовим для звичайних класів (що, на мою думку, є непослідовністю в дизайні Scala). Даріо мав на увазі таке, де зазначив, що вони " незмінні ".

Зауважте, що ви можете змінити типовий параметр, попередньо додавши до кожного аргументу конструктора varдля класів справ. Однак, внесення змін до класів випадків призводить до того, що їх equalsта hashCodeметоди є часовими варіантами. [1]

sepp2k вже згадував, що класи випадків автоматично генеруються equalsта hashCodeметоди.

Також ніхто не згадував, що класи справ автоматично створюють супутник objectз тим же ім'ям, що і клас, який містить applyі unapplyметоди. applyМетод дозволяє будувати екземпляри без випереджаючи з new. Метод unapplyекстрактора дозволяє узгоджувати візерунок, який згадували інші.

Крім того , компілятор оптимізує швидкість match- caseзіставлення з зразком для випадку класів [2].

[1] Класи кейсів класні

[2] Класи корпусу та витяжки, стор. 15 .


12

Конструкція корпусного класу в Scala також може розглядатися як зручність для видалення котлоагрегату.

При конструюванні кейсового класу Scala дає вам наступне.

  • Він створює клас, а також його супутниковий об’єкт
  • Об'єкт-супутник реалізує applyметод, який ви можете використовувати як заводський метод. Ви отримуєте перевагу синтаксичного цукру в тому, що не потрібно використовувати нове ключове слово.

Оскільки клас є незмінним, ви отримуєте аксесуари, які є лише змінними (або властивостями) класу, але не мутаторами (тому немає можливості змінювати змінні). Параметри конструктора автоматично доступні вам як публічні поля лише для читання. Набагато приємніше використовувати, ніж конструкція Java Bean.

  • Ви також отримуєте hashCode, equalsі, toStringметоди за замовчуванням, і equalsметод структурно порівнює об'єкт. Створений copyметод, щоб можна було клонувати об'єкт (деякі поля мають нові значення, надані методу).

Найбільшою перевагою, як уже згадувалося раніше, є той факт, що ви можете визначати відповідність на класи кейсів. Причиною цього є те, що ви отримуєте unapplyметод, який дозволяє деконструювати клас справи для вилучення його полів.


По суті, те, що ви отримуєте від Scala при створенні класу case (або об'єкта case, якщо ваш клас не бере аргументів), є однотонним об'єктом, який служить цілі як фабрики, так і як витяжки .


Навіщо вам потрібна копія незмінного об’єкта?
Paŭlo Ebermann

@ PaŭloEbermann Оскільки copyметод може модифікувати поля:val x = y.copy(foo="newValue")
Тіло

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

Відповідно до документації Scala :

Класи кейсів - це лише звичайні заняття, які є:

  • Зміна за замовчуванням
  • Розбирається через відповідність шаблонів
  • У порівнянні з структурною рівністю замість посилання
  • Хороша інстанція і діяти далі

Ще одна особливість ключового слова case - компілятор автоматично генерує декілька методів для нас, включаючи знайомі способи toString, equals та hashCode на Java.


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

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

Як ми бачимо, компілятор Scala виробляє звичайний клас Fooта супутник-об’єкт 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.hashCodeдля виконання договору рівного хеш-коду:
override <synthetic> def hashCode(): Int
  • головне java.lang.Object.toString:
override def toString(): String
  • конструктор для інстанції за newключовим словом:
def <init>(foo: String, bar: Int): Foo 

Об'єкт Foo: - метод applyекземпляра без newключового слова:

case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
  • метод екстрактора unupplyдля використання класу Case 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 повертає функцію для створення нового Foo, застосовуючи кортеж з 2-х елементів.

Тож кейс-клас - це просто синтаксичний цукор.


4

На відміну від класів, типові класи просто використовуються для зберігання даних.

Класи справ є гнучкими для додатків, орієнтованих на дані, а це означає, що ви можете визначити поля даних у класі випадку та визначити бізнес-логіку в супутньому об’єкті. Таким чином ви відокремлюєте дані від бізнес-логіки.

За допомогою методу копіювання ви можете успадкувати будь-які або всі необхідні властивості від джерела та можете змінити їх як завгодно.


3

Ніхто не згадував, що об’єкт супутника класу випадку має значення tupleddefenction, яке має тип:

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)

Ви можете зробити те ж саме, без кортежу, створивши об'єкт безпосередньо, але якщо ваші набори даних, виражені у вигляді списку кортежу з сукупністю 20 (кортеж з 20 елементами), можливо, ви використовуєте кортеж - це ваш вибір.


3

Клас випадку це клас , який може бути використаний з match/caseзаявою.

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

Ви бачите, що caseслідує екземпляр класу Fun, другий параметр якого - Var. Це симпатичний і потужний синтаксис, але він не може працювати з екземплярами будь-якого класу, тому існують деякі обмеження для класів case. І якщо ці обмеження будуть дотримані, можна автоматично визначити хеш-код і дорівнює.

Неясна фраза "рекурсивний механізм розкладання через узгодження зразка" означає просто "це працює case". (Дійсно, екземпляр, за яким слідує, matchпорівнюється з (збігається з) екземпляром, що випливає case, Scala повинен розкласти їх обоє і повинен рекурсивно розкласти те, з чого вони зроблені.)

Для чого корисні заняття ? Стаття у Вікіпедії про алгебраїчні типи даних дає два хороших класичні приклади, списки та дерева. Підтримка алгебраїчних типів даних (включаючи знання їх порівняння) є необхідною для будь-якої сучасної функціональної мови.

Які класи випадків є НЕ корисно? Деякі об'єкти мають стан, подібний код connection.setConnectTimeout(connectTimeout)не для класів випадків.

А тепер ви можете прочитати екскурсію по Scala: Класи кейсів


2

Я думаю, що загалом усі відповіді дали смислове пояснення щодо класів та класів справ. Це може бути дуже актуальним, але кожен новачок в масштабі повинен знати, що відбувається під час створення класу справ. Я написав цю відповідь, яка пояснює клас випадків у двох словах.

Кожен програміст повинен знати, що якщо вони використовують будь-які заздалегідь вбудовані функції, вони пишуть порівняно менше коду, що дозволяє їм, даючи можливість писати найбільш оптимізований код, але влада покладається на великі обов'язки. Отже, використовуйте попередньо вбудовані функції з дуже обережністю.

Деякі розробники уникають написання класів випадків завдяки додатковим 20 методам, які ви можете побачити, розбираючи файл класу.

Будь ласка, перейдіть за цим посиланням, якщо ви хочете перевірити всі методи в класі класів .


1
  • Класи кейсів визначають об'єкт Compagnon з методами застосовувати і не застосовувати
  • Класи кейсів розширюють серіалізацію
  • Класи випадків визначають рівняння методів hashCode та копіювання
  • Усі атрибути конструктора - val (синтаксичний цукор)

1

Нижче case classesнаведено деякі основні особливості

  1. кейсові класи незмінні.
  2. Ви можете інстанціювати регістри класів без newключового слова.
  3. класи класів можна порівняти за значенням

Зразок коду scala на скрипті scala, взятому з документів scla.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.