Passt der Typ einer Variablen nicht in den aktuellen Kontext, kann
der Compiler, unter bestimmten Voraussetzungen, die Variable automatisch
in eine Variable des passenden Typs konvertieren. Diese automatische
Konvertierung wird als implizite Konvertierung (engl. implicit conversion)
bezeichnet. Als Kurzbezeichnung hat sich auch der englische Begriff Implicits durchgesetzt.
Wenn in Java einer double
-Variablen ein int
-Wert
zugewiesen wird, wird der int
-Wert automatisch in den Typ double
umgewandelt. Diese automatische Umwandlung gibt es in Scala so nicht.
In Scala wird dafür eine Funktion namens int2double
aus
der Klasse scala.Predef
verwendet, die mit dem Schlüsselwort
implicit
gekennzeichnet ist.
Damit kommen wir schon zum Kern der impliziten Konvertierung. Funktionen, die mit
dem Schlüsselwort implicit
gekennzeichnet sind,
können vom Compiler automatisch aufgerufen werden, um ein Objekt eines
bestimmten Typs in einen anderen Typ zu konvertieren. Diese automatische Konvertierung
findet statt, wenn ein zu übergebendes Objekt nicht zur Signatur der Methode
oder nicht dem Typ einer Variablen entspricht.
Damit eine implizite Funktion automatisch aufgerufen werden kann, muss diese sich innerhalb der
aktuellen Sichtbarkeit (engl. scope) befinden. Da die Klasse Predef
standardmäßig sichtbar ist, stehen auch alle dort definierten impliziten
Konvertierungen zur Verfügung.
Wie im Einstieg beschrieben, sind in Scala eine Reihe von Standardkonvertierungen vorgesehen. Hier nochmals ein Beispiel für eine Standard Typ-Konvertierung.
Wer ein paar Stunden mit Scala gearbeitet hat, ist wahrscheinlich (evtl. unbewusst) auch schon der impliziten Tykonvertierung begegnet.
So wird z.B. bei for
-Schleifen zur Iteration über Int
-Werte eine implizite
Typkonvertierung von Int
nach RichInt
vorgenommen. Hier ein Beispiel:
scala> for (i <- 0 until 3) println(i) 0 1 2
Der Generator <-
erwartet eine Collection, aus denen er die Werte für die Schleife generieren kann.
Im Ausdruck 0 until 3
sehen wir einen Methodenaufruf in der Operatorschreibweise,
den wir auch wie folgt ausdrücken können:
0.until(3)
Sehen wir uns nun die API Dokumentation zu
Int
an, stellen wir fest, dass die Methode until
für Int
nicht
definiert ist. Wenn wir uns nun die API Dokumentation zu RichInt
ansehen, sehen wir, dass eine entsprechende Methode definiert ist.
Da wir selber keine implizite Konvertierung definiert haben, muss diese also standardmäßig zur
Verfügung stehen. Sehen wir uns nun die API Dokumentation zu Predef
an, sehen wir eine Reihe von impliziten Typkonvertierungen.
Auch die für unser Bespiel benötigte Konvertierung implicit def intWrapper (x: Int): RichInt
finden wir in der Dokumentation. Definiert wird intWrapper
in der Klasse
LowerPriorityImplicits
, die von Predef
erweitert wird. Da Predef
in allen Scala Klassen automatisch importiert
wird, stehen uns dessen implizite Typkonvertierungen immer zur Verfügung.
Für die ersten Schritte definieren wir zunächst eine Klasse MyComplex
,
welche als Basis für erste Beispiele zur impliziten Typkonvertierung dienen soll.
class MyComplex(val re: Double,val im: Double){ def +(value: MyComplex) = new MyComplex(re+value.re,im+value.im) def -(value: MyComplex) = new MyComplex(re-value.re,im-value.im) override def toString() = re+(if(im >= 0)" + "else" - ")+ scala.math.abs(im)+" * i" }
Die Klasse MyComplex
soll Berechnungen mit komplexen Zahlen ermöglichen.
Um die Klasse übersichtlich zu halten, wurden nur die beiden Grundrechnungsarten
Addition und Subtraktion implementiert. Weiterhin wird die Methode toString
überschrieben, um geeignete Ausgaben machen zu können.
Die nächsten Schritte hin zur impliziten Konvertierung gehen wir im Scala Interpreter
unter Verwendung der (kompilierten) Klasse MyComplex
. Dafür starten wir
den Scala Interpreter und führen einen Test der Klasse MyComplex
durch.
Nach dem Start des Interpreters definieren wir zwei Variablen vom Typ MyComplex
und weisen einer dritten Variablen die Summe der ersten beiden Variablen zu:
scala> val value1 = new MyComplex(1,3) value1: MyComplex = 1.0 + 3.0 * i scala> val value2 = new MyComplex(1,-2) value2: MyComplex = 1.0 - 2.0 * i scala > val value3 = value1 + value2 value3: MyComplex = 2.0 + 1.0 * i
Möchten wir nun ein Double
Wert (eine komplexe Zahl ohne Imaginärteil)
zu einem MyComplex
Wert hinzuaddieren, erhalten wir eine Fehlermeldung vom Interpreter,
da eine Addition von Double
Werten zu einem MyComplex
Wert nicht definiert
ist.
val value4 = value3 + 3.0 <console>:8: error: type mismatch; found : Double(3.0) required: MyComplex val value4 = value3 + 3.0 ^
In Java kämen nun folgende Möglichkeiten zur Lösung des Problems infrage, welche auch in Scala funktionieren:
Double
Wert in einen Konstruktor für MyComplex
mit einem Imaginärteil von 0.0 zu verpacken,
MyComplex
entsprechende Methoden für Operationen mit Double
Werten zu definieren.
Beide Wege sind nicht sehr elegant und quelltextintensiv. Die zweite Variante scheitert spätestens
bei Ausdrücken wie 3.0 + value
, da die Methoden in der Klasse Double
nicht definiert sind, sondern in der Klasse MyComplex
. Wenn man bedenkt, dass eine
ausprogrammierte Klasse zu komplexen Zahlen eine Vielzahl von Methoden enthält und auch
mit Float, Int, Short
und Long
Werten umgehen können sollte,
kommt eine Menge Quelltext zustande.
An dieser Stelle kommt die implizite Konvertierung (implizite Typkonvertierung) in Scala zum Einsatz. Eine implizite Typkonvertierung bewirkt, dass bei Bedarf ein bestimmter Typ in einem, im entsprechenden Kontext stehenden, anderen Typ vom Compiler automatisch umgewandelt wird. Das nachfolgende Beispiel zeigt die Definition einer impliziten Typkonvertierung in Scala:
scala> implicit def doubleToMyComplex(d: Double) = new MyComplex(d,0.) doubleToMyComplex; (d: Double)MyComplex scala> val value5 = value3 + 3.0 value5: MyComplex = 5.0 + 1.0 * i scala> val value6 = 1. + value5 value6: MyComplex = 6.0 + 1.0 * i
Eine implizite Typkonvertierung wird mit dem Schlüsselwort implicit
eingeleitet.
Anschließend folgt die Funktionsdefinition zur automatischen Konvertierung, welche als Argument den
Ausgangstyp erwartet und als Ergebnis den Zieltyp liefert. Damit eine implizite
Typkonvertierung durchgeführt werden kann, muss diese im aktuellen Sichtbarkeitsbereich liegen.
Implizite Typkonvertierungen werden nicht nur auf Parameter einer Methode angewendet. Ist z.B. eine Funktion nicht in einem Typ definiert, kann diese durch implizite Typkonvertierung bereitgestellt werden. Ein Beispiel hierfür ist
scala> val value6 = 1. + value5
Im Typ Double
ist keine Funktion für den Parameter MyComplex
definiert
(MyComplex
ist auch nicht Bestandteil der Standardbiliotheken). Diese Funktion wird
erst durch die implizite Typkonvertierung zur Verfügung gestellt.
Wie im Einstieg bereits angemerkt, muss eine implizite Typumwandlung im Sichtbarkeitsbereich des aktuellen Kontextes definiert sein. Wenn nötig, sucht der Compiler an folgenden Stellen nach einer Definition zur impliziten Typumwandlung:
Für die Suche in den jeweiligen Companion Objekten, ist es nicht notwendig, diese in den Import aufzunehmen.
Auch wenn passende implizite Typumeandlungen im aktuellen Sichtbarkeitsbereich definiert sind, haben diese Regeln in Ihrer Anwendung. Folgende Regeln gilt es unter anderen zu beachten:
Keine Konvertierung
Es wird keine Konvertierung vorgenommen, wenn sich der Quelltext auch ohne Konvertierung compilieren läßt.
Die Umwandlung muss eindeutig sein
Ist im aktuellen Sichtbarkeitsbereich mehr als eine passende Typumwandlung definiert, nimmt der Compiler keine Typumwandlung vor.
Keine Mehrstufe Konvertierung
Es wird nur eine einzelne Typumwandlung vorgenommen. Die Umwandlung in den passenden Typ muss mit Hilfe einer Funktion erfolgen. Es werden keine zwei oder mehr Konvertierungen hintereinander geschaltet, um zum gewünschten Typ zu gelangen. Das heißt, das ein Objekt des Typs A nicht zunächst implizit zum Typ B um dann implizit zum Typ C umgewandelt wird.
In diesem Abschnitt soll ein Beispiel zeigen, wie Companion Objekte dabei helfen können die Companion Klasse Kompatibel zu anderen Typen mithilfe von Implicits zu machen.
Im Beispiel definieren wir zwei Klassen A und B, die in Ihren Companion Objekten jeweils ein Implicit definieren um den eigenen Typ in den Typ des anderen zu konvertieren.
object MainClass { def main(args: Array[String]) { val a : A = new B() val b : B = new A() } } // Neue Datei A.scala object A { implicit def aToB(a: A) : B = { println("Konvertiere A nach B") new B() } } class A { println("Konstruktor A") } // Neue Datei B.scala object B { implicit def bToA(b: B) : A = { println("Konvertiere B nach A") new A() } } class B { println("Konstruktor B") }
Der Ablauf des Programms führt zu folgender Ausgabe auf der Systemausgabe:
Konstruktor B Konvertiere B nach A Konstruktor A Konstruktor A Konvertiere A nach B Konstruktor B