Scala Language
Gevalklassen
Zoeken…
Syntaxis
- case-klasse Foo () // Case-klassen zonder parameters moeten een lege lijst hebben
- case-klasse Foo (a1: A1, ..., aN: AN) // Maak een case-klasse met velden a1 ... aN
- case object Bar // Maak een singleton case-klasse
Case Class Equality
Een functie die gratis wordt aangeboden door case-klassen is een automatisch gegenereerde methode voor equals
die de waarde-gelijkheid van alle individuele ledenvelden controleert in plaats van alleen de referentie-gelijkheid van de objecten te controleren.
Met gewone lessen:
class Foo(val i: Int)
val a = new Foo(3)
val b = new Foo(3)
println(a == b)// "false" because they are different objects
Met casusklassen:
case class Foo(i: Int)
val a = Foo(3)
val b = Foo(3)
println(a == b)// "true" because their members have the same value
Gegenereerde code-artefacten
De case
modifier zorgt ervoor dat de Scala-compiler automatisch gemeenschappelijke boilerplate-code voor de klasse genereert. Het handmatig implementeren van deze code is vervelend en een bron van fouten. De volgende definitie van de case-klasse:
case class Person(name: String, age: Int)
... wordt de volgende code automatisch gegenereerd:
class Person(val name: String, val age: Int)
extends Product with Serializable
{
def copy(name: String = this.name, age: Int = this.age): Person =
new Person(name, age)
def productArity: Int = 2
def productElement(i: Int): Any = i match {
case 0 => name
case 1 => age
case _ => throw new IndexOutOfBoundsException(i.toString)
}
def productIterator: Iterator[Any] =
scala.runtime.ScalaRunTime.typedProductIterator(this)
def productPrefix: String = "Person"
def canEqual(obj: Any): Boolean = obj.isInstanceOf[Person]
override def hashCode(): Int = scala.runtime.ScalaRunTime._hashCode(this)
override def equals(obj: Any): Boolean = this.eq(obj) || obj match {
case that: Person => this.name == that.name && this.age == that.age
case _ => false
}
override def toString: String =
scala.runtime.ScalaRunTime._toString(this)
}
De case
modifier genereert ook een bijbehorend object:
object Person extends AbstractFunction2[String, Int, Person] with Serializable {
def apply(name: String, age: Int): Person = new Person(name, age)
def unapply(p: Person): Option[(String, Int)] =
if(p == null) None else Some((p.name, p.age))
}
Wanneer toegepast op een object
, heeft de case
modifier vergelijkbare (zij het minder dramatische) effecten. Hier zijn de belangrijkste voordelen een toString
implementatie en een hashCode
waarde die consistent is in alle processen. Merk op dat caseobjecten (correct) referentie-gelijkheid gebruiken:
object Foo extends Product with Serializable {
def productArity: Int = 0
def productIterator: Iterator[Any] =
scala.runtime.ScalaRunTime.typedProductIterator(this)
def productElement(i: Int): Any =
throw new IndexOutOfBoundsException(i.toString)
def productPrefix: String = "Foo"
def canEqual(obj: Any): Boolean = obj.isInstanceOf[this.type]
override def hashCode(): Int = 70822 // "Foo".hashCode()
override def toString: String = "Foo"
}
Het is nog steeds mogelijk om handmatig methoden te implementeren die anders door de case
modifier zouden worden geleverd in zowel de klasse zelf als het bijbehorende object.
Basiscursus case-klasse
In vergelijking met reguliere klassen biedt notatie van case-klassen verschillende voordelen:
Alle constructorargumenten zijn
public
en zijn toegankelijk voor geïnitialiseerde objecten (normaal is dit niet het geval, zoals hier wordt aangetoond):case class Dog1(age: Int) val x = Dog1(18) println(x.age) // 18 (success!) class Dog2(age: Int) val x = new Dog2(18) println(x.age) // Error: "value age is not a member of Dog2"
Het biedt een implementatie voor de volgende methoden:
toString
, isequals
,hashCode
(op basis van eigenschappen),copy
,apply
en nietunapply
:case class Dog(age: Int) val d1 = Dog(10) val d2 = d1.copy(age = 15)
Het biedt een handig mechanisme voor het matchen van patronen:
sealed trait Animal // `sealed` modifier allows inheritance within current build-unit only case class Dog(age: Int) extends Animal case class Cat(owner: String) extends Animal val x: Animal = Dog(18) x match { case Dog(x) => println(s"It's a $x years old dog.") case Cat(x) => println(s"This cat belongs to $x.") }
Casusklassen en onveranderlijkheid
De Scala-compiler voorvoegt elk argument in de parameterlijst standaard met val
. Dit betekent dat standaardklassen onveranderlijk zijn. Elke parameter krijgt een accessormethode, maar er zijn geen mutatiemethoden. Bijvoorbeeld:
case class Foo(i: Int)
val fooInstance = Foo(1)
val j = fooInstance.i // get
fooInstance.i = 2 // compile-time exception (mutation: reassignment to val)
Het declareren van een parameter in een case-klasse als var
overschrijft het standaardgedrag en maakt de case-klasse veranderlijk:
case class Bar(var i: Int)
val barInstance = Bar(1)
val j = barInstance.i // get
barInstance.i = 2 // set
Een ander voorbeeld wanneer een case-klasse 'mutable' is, is wanneer de waarde in de case-class veranderlijk is:
import scala.collection._
case class Bar(m: mutable.Map[Int, Int])
val barInstance = Bar(mutable.Map(1 -> 2))
barInstance.m.update(1, 3) // mutate m
barInstance // Bar(Map(1 -> 3)
Merk op dat de 'mutatie' die hier plaatsvindt op de kaart staat waar m
wijst, niet naar m
zelf. Dus als een ander object m
als lid zou hebben, zou het ook de verandering zien. Merk op hoe in het volgende voorbeeld instanceA
ook instanceB
verandert:
import scala.collection.mutable
case class Bar(m: mutable.Map[Int, Int])
val m = mutable.Map(1 ->2)
val barInstanceA = Bar(m)
val barInstanceB = Bar(m)
barInstanceA.m.update(1,3)
barInstanceA // Bar = Bar(Map(1 -> 3))
barInstanceB // Bar = Bar(Map(1 -> 3))
m // scala.collection.mutable.Map[Int,Int] = Map(1 -> 3)
Maak een kopie van een object met bepaalde wijzigingen
Casusklassen bieden een copy
waarmee een nieuw object wordt gemaakt dat dezelfde velden deelt als het oude, met bepaalde wijzigingen.
We kunnen deze functie gebruiken om een nieuw object te maken van een vorig object dat enkele van dezelfde kenmerken heeft. Deze eenvoudige casusklasse om deze functie te demonstreren:
case class Person(firstName: String, lastName: String, grade: String, subject: String)
val putu = Person("Putu", "Kevin", "A1", "Math")
val mark = putu.copy(firstName = "Ketut", lastName = "Mark")
// mark: People = People(Ketut,Mark,A1,Math)
In dit voorbeeld kunnen we zien dat de twee objecten delen vergelijkbare kenmerken ( grade = A1
, subject = Math
), tenzij zij hebben in de kopie (gespecificeerd firstName
en lastName
).
Gevalklassen uit één element voor type veiligheid
Om typeveiligheid te bereiken, willen we soms het gebruik van primitieve typen op ons domein vermijden. Stel je bijvoorbeeld een Person
met een name
. Doorgaans coderen we de name
als een String
. Het zou echter niet moeilijk zijn om een String
die de name
van een Person
vertegenwoordigt te Person
met een String
die een foutbericht weergeeft:
def logError(message: ErrorMessage): Unit = ???
case class Person(name: String)
val maybeName: Either[String, String] = ??? // Left is error, Right is name
maybeName.foreach(logError) // But that won't stop me from logging the name as an error!
Om dergelijke valkuilen te voorkomen, kunt u de gegevens als volgt coderen:
case class PersonName(value: String)
case class ErrorMessage(value: String)
case class Person(name: PersonName)
en nu onze code zal niet compileren als we mengen PersonName
met ErrorMessage
, of zelfs een gewone String
.
val maybeName: Either[ErrorMessage, PersonName] = ???
maybeName.foreach(reportError) // ERROR: tried to pass PersonName; ErrorMessage expected
maybeName.swap.foreach(reportError) // OK
Maar dit brengt een korte runtime-overhead met zich mee, omdat we nu String
s naar / van hun PersonName
containers moeten in- / uitschakelen. Om dit te voorkomen, kan men maken PersonName
en ErrorMessage
waarde klassen:
case class PersonName(val value: String) extends AnyVal
case class ErrorMessage(val value: String) extends AnyVal