В чем разница между классом дел Скалы и классом?


440

Я искал в Google, чтобы найти различия между а case classи а class. Все упоминают, что когда вы хотите выполнить сопоставление с образцом в классе, используйте case case. В противном случае используйте классы, а также упомяните некоторые дополнительные привилегии, такие как equals и переопределение хеш-кода. Но являются ли это единственными причинами, по которым следует использовать класс case вместо класса?

Я думаю, что в Scala должна быть какая-то очень важная причина. Каково объяснение или есть ресурс, чтобы узнать больше о тематических классах Scala?

Ответы:


394

Классы case можно рассматривать как простые и неизменные объекты, хранящие данные, которые должны зависеть исключительно от аргументов своего конструктора .

Эта функциональная концепция позволяет нам

  • использовать компактный синтаксис инициализации ( Node(1, Leaf(2), None)))
  • разложить их с помощью сопоставления с образцом
  • иметь сравнения равенства неявно определены

В сочетании с наследованием классы падежей используются для имитации алгебраических типов данных .

Если объект выполняет вычисления с состоянием изнутри или демонстрирует другие виды сложного поведения, это должен быть обычный класс.


11
@Teja: в некотором роде. ADT являются параметризованными перечислениями , чрезвычайно мощными и безопасными.
Дарио

8
Классы запечатанных падежей используются для имитации алгебраических типов данных. В противном случае количество подклассов не ограничено.
Томас Юнг

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

2
@Dario ... и тип в противном случае открыт, а не и ADT. :-)
Томас Юнг

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

165

Технически, нет никакой разницы между классом и классом case - даже если компилятор оптимизирует некоторые вещи при использовании 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
  • Классы дел могут быть сопоставлены с шаблоном
  • Классы дел автоматически определяют хеш-код и равно
  • Классы case автоматически определяют методы получения для аргументов конструктора.

(Вы уже упомянули все, кроме последнего).

Это единственные отличия от обычных классов.


13
Сеттеры не генерируются для классов case, если в аргументе конструктора не указано «var», и в этом случае вы получаете то же поколение getter / setter, что и в обычных классах.
Митч Блевинс

1
@ Митч: Правда, мой плохой. Исправлено сейчас.
sepp2k

Вы пропустили 2 различия, смотрите мой ответ.
Шелби Мур III

@MitchBlevins, обычные классы не всегда имеют генерацию геттера / сеттера.
Шелби Мур III

Классы Case определяют метод неприменить, поэтому они могут быть сопоставлены с шаблоном.
Happy Torturer

30

Никто не упомянул, что case-классы также являются экземплярами Productи таким образом наследуют эти методы:

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

где productArityвозвращает количество параметров класса, productElement(i)возвращает i- й параметр и productIteratorпозволяет перебирать их.


2
Однако они не являются экземплярами Product1, Product2 и т. Д.
Жан-Филипп Пелле

27

Никто не упомянул, что case-классы имеют valпараметры конструктора, но это также по умолчанию для обычных классов (что я считаю несоответствием в дизайне Scala). Дарио подразумевал такие, где он отметил, что они « неизменны ».

Обратите внимание, что вы можете переопределить значение по умолчанию, добавив каждый аргумент конструктора varк классам case. Однако, делая изменяемые классы case приводит к тому, что их equalsи hashCodeметоды изменяются во времени.

sepp2k уже упоминал, что case-классы автоматически генерируют equalsи hashCodeметоды.

Также никто не упомянул, что case-классы автоматически создают компаньона objectс тем же именем, что и класс, который содержит applyи unapplyметоды. applyМетод позволяет строить экземпляры без предваряя с new. Метод unapplyэкстрактора обеспечивает сопоставление с шаблоном, упомянутым другими.

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

[1] Классные кейсы классные

[2] Классы дел и экстракторы, стр. 15 .


12

Конструкцию case-класса в Scala также можно рассматривать как удобство для удаления некоторого шаблона.

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

  • Он создает класс, а также его сопутствующий объект
  • Его сопутствующий объект реализует applyметод, который вы можете использовать как фабричный метод. Вы получаете преимущество синтаксического сахара в том, что вам не нужно использовать новое ключевое слово.

Поскольку класс является неизменным, вы получаете методы доступа, которые являются просто переменными (или свойствами) класса, но не имеют мутаторов (поэтому нет возможности изменять переменные). Параметры конструктора автоматически доступны для вас как общедоступные поля только для чтения. Гораздо приятнее в использовании, чем конструкция Java bean.

  • Вы также получаете hashCode, equalsи toStringметоды по умолчанию, и equalsметод сравнивает объект структурно. copyМетод генерируется , чтобы иметь возможность клонировать объект (с некоторыми полями , имеющими новые значения , предоставленных методом).

Самым большим преимуществом, как уже упоминалось ранее, является тот факт, что вы можете использовать сопоставление с образцом в классах case. Причина этого в том, что вы получаете unapplyметод, который позволяет деконструировать класс case для извлечения его полей.


По сути, то, что вы получаете от Scala при создании класса case (или объекта case, если ваш класс не принимает аргументов), является одноэлементным объектом, который служит цели как фабрика и как экстрактор .


Зачем вам нужна копия неизменяемого объекта?
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

Согласно документации Scala :

Классы Case - это обычные классы, которые:

  • Неизменный по умолчанию
  • Разлагается через сопоставление с образцом
  • По сравнению структурным равенством вместо ссылки
  • Достаточно, чтобы создать экземпляр и оперировать

Еще одной особенностью ключевого слова 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
           ^

Но если мы используем тот же код, но используем case case:

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черты для экземпляров класса make case, сравнимых по равенству ==:
def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
  • переопределение java.lang.Object.hashCodeдля выполнения контракта equals-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для использования класса 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 возвращает функцию для создания нового Foo, применяя кортеж из 2 элементов.

Так что класс case - это просто синтаксический сахар.


4

В отличие от классов, case-классы используются только для хранения данных.

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

С помощью метода копирования вы можете наследовать любые или все необходимые свойства из источника и изменять их по своему усмотрению.


3

Никто не упомянул, что сопутствующий объект case-класса имеет tupleddefention, который имеет тип:

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

Единственный вариант использования, который я могу найти, это когда вам нужно создать класс case из кортежа, например:

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)не для классов case.

И теперь вы можете прочитать Тур по Scala: Case Classes


2

Я думаю, что в целом все ответы дали семантическое объяснение о классах и тематических классах. Это может быть очень актуально, но каждый новичок в Scala должен знать, что происходит, когда вы создаете кейс-класс. Я написал этот ответ, который объясняет класс случая в двух словах.

Каждый программист должен знать, что если они используют какие-либо предварительно созданные функции, то они пишут сравнительно меньше кода, что дает им возможность писать наиболее оптимизированный код, но это связано с большими обязанностями. Поэтому используйте готовые функции с очень большой осторожностью.

Некоторые разработчики избегают писать классы case из-за дополнительных 20 методов, которые вы можете увидеть, разобрав файл class.

Пожалуйста, обратитесь по этой ссылке, если вы хотите проверить все методы внутри класса case .


1
  • Классы Case определяют объект Compagnon с методами apply и unapply
  • Классы Case расширяют Serializable
  • Классы case определяют методы hashCode и copy
  • Все атрибуты конструктора - val (синтаксический сахар)

1

Некоторые из ключевых особенностей case classesперечислены ниже

  1. тематические классы неизменны.
  2. Вы можете создавать экземпляры классов без newключевого слова.
  3. тематические классы можно сравнить по значению

Пример кода Scala на скрипке Scala, взятый из документов Scala.

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.