Qual é a diferença entre a classe de caso do Scala e a classe?


439

Pesquisei no Google para encontrar as diferenças entre a case classe a class. Todo mundo menciona que, quando você deseja fazer a correspondência de padrões na classe, use a classe de caso. Caso contrário, use classes e também mencione algumas vantagens extras, como iguais e substituição de código de hash. Mas essas são as únicas razões pelas quais se deve usar uma classe de caso em vez de classe?

Acho que deve haver uma razão muito importante para esse recurso no Scala. Qual é a explicação ou existe um recurso para aprender mais sobre as classes de caso Scala?

Respostas:


393

As classes de caso podem ser vistas como objetos de retenção de dados simples e imutáveis ​​que devem depender exclusivamente de seus argumentos construtores .

Esse conceito funcional nos permite

  • use uma sintaxe de inicialização compacta ( Node(1, Leaf(2), None)))
  • decomponha-os usando a correspondência de padrões
  • ter comparações de igualdade definidas implicitamente

Em combinação com a herança, as classes de caso são usadas para imitar tipos de dados algébricos .

Se um objeto executa cálculos com estado no interior ou exibe outros tipos de comportamento complexo, deve ser uma classe comum.


11
@ Teja: De alguma forma. Os ADTs são meio enumerados em parâmetros , extremamente poderosos e seguros.
Dario

8
Classes de caixa selada são usadas para imitar tipos de dados algébricos. Caso contrário, o número de subclasses não é limitado.
Thomas Jung

6
@ Thomas: Corretamente faladas, as classes de caso derivadas de classes abstratas seladas imitam tipos de dados algébricos fechados, enquanto o ADT está aberto de outra forma .
Dario

2
@Dario ... e o tipo está aberto e não e um ADT. :-)
Thomas Jung

1
@ Thomas: Sim, é apenas um existencial;)
Dario

165

Tecnicamente, não há diferença entre uma classe e uma classe de caso - mesmo que o compilador otimize algumas coisas ao usar classes de caso. No entanto, uma classe de caso é usada para eliminar a placa da caldeira para um padrão específico, que está implementando tipos de dados algébricos .

Um exemplo muito simples de tais tipos são as árvores. Uma árvore binária, por exemplo, pode ser implementada assim:

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

Isso nos permite fazer o seguinte:

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

Observe que as árvores constroem e desconstroem (através da correspondência de padrões) com a mesma sintaxe, que também é exatamente como elas são impressas (menos espaços).

E eles também podem ser usados ​​com mapas ou conjuntos de hash, pois eles têm um hashCode válido e estável.


71
  • As classes de caso podem ser correspondidas com padrão
  • As classes de caso definem automaticamente hashcode e iguais
  • As classes de caso definem automaticamente métodos getter para os argumentos do construtor.

(Você já mencionou tudo, exceto o último).

Essas são as únicas diferenças para as classes regulares.


13
Os setters não são gerados para as classes de casos, a menos que "var" seja especificado no argumento do construtor; nesse caso, você obtém a mesma geração de getter / setter que as classes regulares.
Mitch Blevins

1
@ Mitch: Verdade, meu mal. Corrigido agora.
sepp2k

Você omitiu 2 diferenças, veja minha resposta.
Shelby Moore III

@MitchBlevins, as classes regulares nem sempre têm geração de getter / setter.
Shelby Moore III

As classes de caso definem o método de não aplicar e é por isso que elas podem ser correspondidas com padrão.
Happy Torturer

30

Ninguém mencionou que as classes de casos também são instâncias Producte, portanto, herdam esses métodos:

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

onde the productArityretorna o número de parâmetros de classe, productElement(i)retorna o i- ésimo parâmetro e productIteratorpermite iterar através deles.


2
No entanto, não são instâncias do Produto1, Produto2, etc.
Jean-Philippe Pellet

27

Ninguém mencionou que as classes de caso têm valparâmetros construtores, mas esse também é o padrão para as classes regulares (o que eu acho que é uma inconsistência no design do Scala). Dario sugeriu isso onde ele notou que eles são " imutáveis ".

Observe que você pode substituir o padrão acrescentando um argumento a cada construtor varpara classes de caso. No entanto, fazendo classes case mutável faz com que os seus equalse hashCodemétodos para ser variante tempo. [1]

O sepp2k já mencionou que as classes de caso geram equalse hashCodemétodos automaticamente .

Além disso, ninguém mencionou que as classes de caso criam automaticamente um complemento objectcom o mesmo nome da classe, que contém applye unapplymétodos. O applymétodo permite construir instâncias sem preceder com new. O unapplymétodo extrator permite a correspondência de padrões mencionada por outros.

Além disso, o compilador otimiza a velocidade de match- casecorrespondência de padrão para as classes de casos [2].

[1] Classes de casos são legais

[2] Classes de casos e extratores, página 15 .


12

A construção de classe de caso no Scala também pode ser vista como uma conveniência para remover alguns clichês.

Ao construir uma classe de caso, o Scala fornece o seguinte.

  • Ele cria uma classe e seu objeto complementar
  • Seu objeto complementar implementa o applymétodo que você pode usar como método de fábrica. Você obtém a vantagem sintática do açúcar por não precisar usar a nova palavra-chave.

Como a classe é imutável, você obtém acessadores, que são apenas as variáveis ​​(ou propriedades) da classe, mas sem mutadores (portanto, não há capacidade de alterar as variáveis). Os parâmetros do construtor estão automaticamente disponíveis para você como campos públicos somente leitura. Muito mais agradável de usar do que a construção do Java bean.

  • Você também terá hashCode, equalse toStringmétodos, por padrão eo equalsmétodo compara um objeto estruturalmente. Um copymétodo é gerado para poder clonar um objeto (com alguns campos tendo novos valores fornecidos ao método).

A maior vantagem, como mencionado anteriormente, é o fato de que você pode padronizar a correspondência nas classes de caso. A razão para isso é porque você obtém o unapplymétodo que permite desconstruir uma classe de caso para extrair seus campos.


Em essência, o que você obtém do Scala ao criar uma classe de caso (ou um objeto de caso, se sua classe não aceita argumentos) é um objeto único que serve ao propósito de fábrica e de extrator .


Por que você precisaria de uma cópia de um objeto imutável?
Pa Elo Ebermann

@ PaŭloEbermann Como o copymétodo pode modificar os campos:val x = y.copy(foo="newValue")
Thilo

8

Além do que as pessoas já disseram, existem algumas diferenças mais básicas entre classecase class

1. Case Classnão precisa explícito new, enquanto a classe precisa ser chamada comnew

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

2.Por parâmetros de construtores padrão são privados class, enquanto seu público emcase 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 classcomparar-se por valor

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

De acordo com a documentação da Scala :

As classes de caso são apenas classes regulares que são:

  • Imutável por padrão
  • Decomponível através da correspondência de padrões
  • Comparado por igualdade estrutural em vez de por referência
  • Sucinto para instanciar e operar

Outro recurso da palavra-chave case é que o compilador gera automaticamente vários métodos para nós, incluindo os métodos familiares toString, equals e hashCode em Java.


5

Classe:

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
           ^

Mas se usarmos o mesmo código, mas usarmos a classe de caso:

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

Classe de pessoa:

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)

Correspondência de padrões:

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

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

Para ter o melhor entendimento do que é uma classe de caso:

vamos assumir a seguinte definição de classe de caso:

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

e faça o seguinte no terminal:

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

O Scala 2.12.8 produzirá:

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

Como podemos ver, o compilador Scala produz uma classe regular Fooe um objeto complementar Foo.

Vamos percorrer a classe compilada e comentar o que temos:

  • o estado interno da Fooclasse, imutável:
val foo: String
val bar: Int
  • getters:
def foo(): String
def bar(): Int
  • métodos de cópia:
def copy(foo: String, bar: Int): Foo
def copy$default$1(): String
def copy$default$2(): Int
  • scala.Productcaracterística de implementação :
override def productPrefix(): String
def productArity(): Int
def productElement(x$1: Int): Object
override def productIterator(): Iterator
  • implementação de scala.Equalscaracterística para tornar instâncias de classe de caso comparáveis ​​à igualdade por ==:
def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
  • substituindo java.lang.Object.hashCodepor obedecer ao contrato equals-hashcode:
override <synthetic> def hashCode(): Int
  • substituição java.lang.Object.toString:
override def toString(): String
  • construtor para instanciação por newpalavra-chave:
def <init>(foo: String, bar: Int): Foo 

Objeto Foo: - método applypara instanciação sem newpalavra-chave:

case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
  • método extrator unupplypara usar a classe de caso Foo na correspondência de padrões:
case <synthetic> def unapply(x$0: Foo): Option
  • método para proteger o objeto como singleton da desserialização por não deixar produzir mais uma instância:
<synthetic> private def readResolve(): Object = Foo;
  • O objeto Foo se estende scala.runtime.AbstractFunction2para fazer esse truque:
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 retorna uma função para criar um novo Foo aplicando uma tupla de 2 elementos.

Portanto, a aula de caso é apenas açúcar sintático.


4

Diferentemente das classes, as classes de caso são usadas apenas para armazenar dados.

As classes de caso são flexíveis para aplicativos centrados em dados, o que significa que você pode definir campos de dados na classe de caso e definir lógica de negócios em um objeto complementar. Dessa forma, você está separando os dados da lógica de negócios.

Com o método de cópia, você pode herdar uma ou todas as propriedades necessárias da origem e alterá-las conforme desejar.


3

Ninguém mencionou que o objeto complementar da classe de caso tem tupleddefesa, que possui um tipo:

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

O único caso de uso que posso encontrar é quando você precisa construir uma classe de caso a partir da tupla, por exemplo:

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

Você pode fazer o mesmo, sem tupla, criando objeto diretamente, mas se seus conjuntos de dados expressos como lista de tupla com aridade 20 (tupla com 20 elementos), pode estar usando tupla, é sua escolha.


3

Uma classe de caso é uma classe que pode ser usada com a match/caseinstrução

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

Você vê que caseé seguido por uma instância da classe Fun cujo segundo parâmetro é um Var. Essa é uma sintaxe muito agradável e poderosa, mas não pode funcionar com instâncias de nenhuma classe; portanto, existem algumas restrições para as classes de caso. E se essas restrições forem obedecidas, é possível definir automaticamente código hash e iguais.

A frase vaga "um mecanismo de decomposição recursiva via correspondência de padrões" significa apenas "ele trabalha com case". (De fato, a instância seguida por matché comparada com (comparada com) a instância a seguir case, Scala precisa decompor os dois e decompor recursivamente do que eles são feitos.)

Para que classes de casos são úteis? O artigo da Wikipedia sobre tipos de dados algébricos fornece dois bons exemplos clássicos, listas e árvores. O suporte para tipos de dados algébricos (incluindo saber como compará-los) é obrigatório para qualquer linguagem funcional moderna.

Para quais classes de caso não são úteis? Alguns objetos têm estado, o código como connection.setConnectTimeout(connectTimeout)não é para classes de caso.

E agora você pode ler A Tour of Scala: Case Classes


2

Eu acho que no geral todas as respostas deram uma explicação semântica sobre classes e classes de casos. Isso pode ser muito relevante, mas todo iniciante no scala deve saber o que acontece quando você cria uma classe de caso. Eu escrevi esta resposta, que explica a classe de caso em poucas palavras.

Todo programador deve saber que, se estiver usando alguma função pré-criada, estará escrevendo um código comparativamente menos, o que lhes permitirá dar o poder de escrever o código mais otimizado, mas o poder vem com grandes responsabilidades. Portanto, use funções pré-construídas com muito cuidado.

Alguns desenvolvedores evitam escrever classes de caso devido a 20 métodos adicionais, que você pode ver desmontando o arquivo de classe.

Por favor, consulte este link se você quiser verificar todos os métodos dentro de uma classe caso .


1
  • As classes de caso definem um objeto compagnon com métodos de aplicação e não aplicação
  • Classes de caso estendem Serializable
  • As classes de caso definem métodos hashCode e copy iguais
  • Todos os atributos do construtor são val (açúcar sintático)

1

Alguns dos principais recursos case classesestão listados abaixo

  1. as classes de casos são imutáveis.
  2. Você pode instanciar classes de caso sem newpalavra-chave.
  3. classes de casos podem ser comparadas por valor

Exemplo de código scala no scala fiddle, retirado dos documentos do scala.

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.