Scala Language
Fallklassen
Suche…
Syntax
- Fallklasse Foo () // Fallklassen ohne Parameter müssen eine leere Liste haben
- Fallklasse Foo (a1: A1, ..., aN: AN) // Eine Fallklasse mit den Feldern a1 ... aN erstellen
- case object Bar // Eine Singleton-Case-Klasse erstellen
Fallklassengleichheit
Ein Feature, das von Fallklassen frei bereitgestellt wird, ist eine automatisch generierte equals
Methode, die die Wertegleichheit aller einzelnen equals
prüft, anstatt nur die Referenzgleichheit der Objekte zu prüfen.
Bei gewöhnlichen Klassen:
class Foo(val i: Int)
val a = new Foo(3)
val b = new Foo(3)
println(a == b)// "false" because they are different objects
Mit Fallklassen:
case class Foo(i: Int)
val a = Foo(3)
val b = Foo(3)
println(a == b)// "true" because their members have the same value
Generierte Code-Artefakte
Der case
Modifikator bewirkt , dass die Scala - Compiler automatisch gemeinsamen Standardcode für die Klasse generieren. Die manuelle Implementierung dieses Codes ist langwierig und eine Fehlerquelle. Die folgende Fallklassendefinition:
case class Person(name: String, age: Int)
... wird der folgende Code automatisch generiert:
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)
}
Der case
Modifikator erzeugt auch ein Begleitobjekt:
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))
}
Wenn auf ein angelegtes object
, der case
hat Modifikator ähnlich (wenn auch weniger dramatisch) Effekte. Hier sind die wichtigsten Vorteile eine toString
Implementierung und ein über alle Prozesse hinweg konsistenter hashCode
. Beachten Sie, dass Fallobjekte (korrekt) eine Referenzgleichheit verwenden:
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"
}
Es ist immer noch möglich, manuell Methoden zu implementieren , die sonst durch den zur Verfügung gestellt werden würde case
Modifikator sowohl in der Klasse selbst und seine Begleiter Objekt.
Fallklassen-Grundlagen
Im Vergleich zu regulären Klassen bietet die Notation von Fallklassen mehrere Vorteile:
Alle Konstruktorargumente sind
public
und können auf initialisierte Objekte zugegriffen werden (normalerweise ist dies nicht der Fall, wie hier gezeigt):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"
Es stellt eine Implementierung für die folgenden Methoden:
toString
,equals
,hashCode
(basierend auf Eigenschaften),copy
,apply
undunapply
:case class Dog(age: Int) val d1 = Dog(10) val d2 = d1.copy(age = 15)
Es bietet einen praktischen Mechanismus für die Mustererkennung:
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.") }
Fallklassen und Unveränderlichkeit
Der Scala-Compiler stellt jedem Argument in der Parameterliste standardmäßig den val
. Dies bedeutet, dass Fallklassen standardmäßig unveränderlich sind. Jeder Parameter erhält eine Zugriffsmethode, es gibt jedoch keine Mutator-Methoden. Zum Beispiel:
case class Foo(i: Int)
val fooInstance = Foo(1)
val j = fooInstance.i // get
fooInstance.i = 2 // compile-time exception (mutation: reassignment to val)
Das Deklarieren eines Parameters in einer Fallklasse als var
überschreibt das Standardverhalten und macht die Fallklasse veränderbar:
case class Bar(var i: Int)
val barInstance = Bar(1)
val j = barInstance.i // get
barInstance.i = 2 // set
Eine andere Instanz, in der eine Fallklasse "veränderbar" ist, ist der veränderliche Wert in der Fallklasse:
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)
Beachten Sie, dass die hier vorkommende 'Mutation' sich in der Map befindet, auf die m
zeigt, nicht auf m
selbst. Wenn also ein anderes Objekt m
als Member hatte, würde es auch die Änderung sehen. Beachten Sie, wie im folgenden Beispiel die instanceA
geändert wird, instanceB
auch die instanceB
B geändert wird:
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)
Erstellen Sie eine Kopie eines Objekts mit bestimmten Änderungen
Fallklassen bieten eine copy
, mit der ein neues Objekt erstellt wird, das bei bestimmten Änderungen dieselben Felder wie das alte Objekt verwendet.
Wir können diese Funktion verwenden, um ein neues Objekt aus einem vorherigen Objekt zu erstellen, das einige der gleichen Merkmale aufweist. Diese einfache Fallklasse veranschaulicht diese Funktion:
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 diesem Beispiel sehen wir, dass die beiden Objekte ähnliche Merkmale aufweisen ( grade = A1
, subject = Math
), es sei denn, sie wurden in der Kopie ( firstName
und lastName
) angegeben.
Einzelelement-Gehäuseklassen für die Typsicherheit
Um die Typsicherheit zu erreichen, möchten wir manchmal die Verwendung primitiver Typen in unserer Domäne vermeiden. Stellen Sie sich zum Beispiel eine Person
mit einem name
. Normalerweise würden wir den name
als String
kodieren. Allerdings wäre es nicht schwer sein , ein mischen String
einer darstellt Person
‚s name
mit einem String
repräsentiert eine Fehlermeldung:
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!
Um solche Fehler zu vermeiden, können Sie die Daten folgendermaßen codieren:
case class PersonName(value: String)
case class ErrorMessage(value: String)
case class Person(name: PersonName)
und jetzt wird unser Code nicht kompiliert, wenn wir PersonName
mit ErrorMessage
oder sogar einen normalen String
mischen.
val maybeName: Either[ErrorMessage, PersonName] = ???
maybeName.foreach(reportError) // ERROR: tried to pass PersonName; ErrorMessage expected
maybeName.swap.foreach(reportError) // OK
Dies verursacht jedoch einen geringen Laufzeit-Overhead, da wir nun String
s in / aus seinen PersonName
Containern PersonName
. Um dies zu vermeiden, können Klassen für PersonName
und ErrorMessage
Werte erstellt werden:
case class PersonName(val value: String) extends AnyVal
case class ErrorMessage(val value: String) extends AnyVal