В чем разница между классом дел Скалы и классом?
Я искал в Google, чтобы найти различия между case class
и class
, Все упоминают, что когда вы хотите сделать сопоставление с образцом в классе, используйте case case. В противном случае используйте классы, а также упомяните некоторые дополнительные привилегии, такие как equals и переопределение хеш-кода. Но являются ли это единственными причинами, по которым следует использовать класс case вместо класса?
Я думаю, что в Scala должна быть какая-то очень важная причина. Каково объяснение или есть ресурс, чтобы узнать больше о тематических классах Scala?
17 ответов
Классы case можно рассматривать как простые и неизменные объекты, содержащие данные, которые должны зависеть исключительно от аргументов своего конструктора.
Эта функциональная концепция позволяет нам
- использовать компактный синтаксис инициализации (
Node(1, Leaf(2), None))
) - разложить их с помощью сопоставления с образцом
- иметь сравнения равенства неявно определены
В сочетании с наследованием классы падежей используются для имитации алгебраических типов данных.
Если объект выполняет вычисления с состоянием изнутри или демонстрирует другие виды сложного поведения, это должен быть обычный класс.
Технически, нет никакой разницы между классом и классом 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 =>
}
Обратите внимание, что деревья конструируют и деконструируют (посредством сопоставления с образцом) с одинаковым синтаксисом, который также является точным способом их печати (минус пробелы).
И их также можно использовать с хэш-картами или наборами, поскольку они имеют действительный, стабильный хэш-код.
- Классы дел могут быть сопоставлены с шаблоном
- Классы дел автоматически определяют хеш-код и равно
- Классы case автоматически определяют методы получения для аргументов конструктора.
(Вы уже упомянули все, кроме последнего).
Это единственные отличия от обычных классов.
Никто не упомянул, что тематические классы имеют val
Параметры конструктора, но это также по умолчанию для обычных классов (что я считаю несоответствием в дизайне Scala). Дарио подразумевал такие, где он отметил, что они "неизменны".
Обратите внимание, что вы можете переопределить значение по умолчанию, добавив каждый аргумент конструктора var
для тематических классов. Тем не менее, делая изменчивыми классы дела вызывает их equals
а также hashCode
методы, чтобы быть вариантом времени. [1]
sepp2k уже упоминал, что case-классы автоматически генерируют equals
а также hashCode
методы.
Также никто не упомянул, что case-классы автоматически создают компаньона object
с тем же именем, что и класс, который содержит apply
а также unapply
методы. apply
метод позволяет создавать экземпляры без предварительной new
, unapply
Метод Extractor позволяет сопоставить шаблон с другими.
Также компилятор оптимизирует скорость match
-case
сопоставление с образцом для case-классов [2].
Никто не упомянул, что case-классы также являются экземплярами Product
и таким образом наследовать эти методы:
def productElement(n: Int): Any
def productArity: Int
def productIterator: Iterator[Any]
где productArity
возвращает количество параметров класса, productElement(i)
возвращает i-й параметр и productIterator
позволяет перебирать их.
Конструкцию case-класса в Scala также можно рассматривать как удобство для удаления некоторого шаблона.
При построении кейса класс Scala дает вам следующее.
- Он создает класс, а также его сопутствующий объект
- Его сопутствующий объект реализует
apply
метод, который вы можете использовать в качестве заводского метода. Вы получаете преимущество синтаксического сахара в том, что вам не нужно использовать новое ключевое слово.
Поскольку класс является неизменным, вы получаете методы доступа, которые являются просто переменными (или свойствами) класса, но не имеют мутаторов (поэтому нет возможности изменять переменные). Параметры конструктора автоматически доступны для вас как общедоступные поля только для чтения. Гораздо приятнее в использовании, чем конструкция Java bean.
- Вы также получаете
hashCode
,equals
, а такжеtoString
методы по умолчанию иequals
Метод сравнивает объект структурно.copy
Метод генерируется, чтобы иметь возможность клонировать объект.
Самым большим преимуществом, как уже упоминалось ранее, является тот факт, что вы можете использовать сопоставление с образцом в классах case. Причина в том, что вы получаете unapply
метод, который позволяет деконструировать класс case для извлечения его полей.
По сути, то, что вы получаете от Scala при создании класса case (или объекта case, если ваш класс не принимает аргументов), является одноэлементным объектом, который служит цели как фабрика и как экстрактор.
Помимо того, что люди уже сказали, есть еще несколько основных различий между 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
Чтобы иметь полное представление о том, что такое кейс-класс:
давайте предположим следующее определение класса дела:
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
Объект 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 - это просто синтаксический сахар.
Согласно документации Scala:
Классы Case - это обычные классы, которые:
- Неизменный по умолчанию
- Разлагается через сопоставление с образцом
- По сравнению структурным равенством вместо ссылки
- Достаточно, чтобы создать экземпляр и оперировать
Еще одной особенностью ключевого слова case является то, что компилятор автоматически генерирует для нас несколько методов, включая знакомые методы toString, equals и hashCode в Java.
Учебный класс:
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
В отличие от классов, case-классы используются только для хранения данных.
Классы дел являются гибкими для приложений, ориентированных на данные, что означает, что вы можете определять поля данных в классе дел и определять бизнес-логику в сопутствующем объекте. Таким образом, вы отделяете данные от бизнес-логики.
С помощью метода copy вы можете наследовать любые или все необходимые свойства из источника и можете изменять их по своему усмотрению.
Никто не упомянул, что объект сопутствующего класса tupled
defention, который имеет тип:
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 элементами), могут использовать тьюплы - ваш выбор.
Класс case - это класс, который может использоваться с 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: Case Classes
Я думаю, что в целом все ответы дали семантическое объяснение о классах и классах случаев. Это может быть очень актуально, но каждый новичок в Scala должен знать, что происходит, когда вы создаете кейс-класс. Я написал этот ответ, который объясняет класс случая в двух словах.
Каждый программист должен знать, что если он использует какие-либо предварительно созданные функции, то он пишет сравнительно меньше кода, что дает им возможность писать наиболее оптимизированный код, но это связано с большими обязанностями. Поэтому используйте готовые функции с очень большой осторожностью.
Некоторые разработчики избегают написания case-классов из-за дополнительных 20 методов, которые вы можете увидеть, разобрав файл class.
Пожалуйста, обратитесь по этой ссылке, если вы хотите проверить все методы внутри класса case.
Один из важных вопросов, не упомянутых в предыдущих ответах, - это вопрос идентичности. Объекты обычных классов имеют идентичность, поэтому, даже если два объекта имеют одинаковые значения для всех своих полей, они все равно являются разными объектами. Однако для экземпляров класса case равенство определяется исключительно в терминах значений полей объекта.
Некоторые из ключевых особенностей case classes
перечислены ниже
- case-классы неизменяемы.
- Вы можете создавать экземпляры классов case без
new
ключевое слово. - case-классы можно сравнивать по значению
Пример кода scala на скрипте scala, взятый из документации scala.
- Классы Case определяют объект Compagnon с методами apply и unapply
- Классы Case расширяют Serializable
- Классы case определяют методы hashCode и copy
- Все атрибуты конструктора являются val (синтаксический сахар)