Einstieg

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.

Standard Konvertierungen

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
itmapa.de - X2H V 0.11

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.

Eigene implizite Konvertierungen

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"
}
itmapa.de - X2H V 0.9

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:

  • Bei der Addition den Double Wert in einen Konstruktor für MyComplex mit einem Imaginärteil von 0.0 zu verpacken,
  • In der Klasse 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.

Platzierung

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:

  • Im aktuellen Quelltextteil des Kontextes

  • Im zugehörigen Package Objekt

  • In allen importierten Singleton -/Companion Objekten und Package Objekten

  • Im Companion Objekt des Ausgangstyps (sofern vorhanden)

  • Im Comparion Objekt des Zieltyps (sofern vorhanden)

Für die Suche in den jeweiligen Companion Objekten, ist es nicht notwendig, diese in den Import aufzunehmen.

Regeln

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.

Beispiel der Konvertierung in Companion Objekten

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")
}
itmapa.de - X2H V 0.11

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