Optionals in Swift — Fluch oder Segen?

Viele Programmiersprachen unterscheiden zwischen Wert- und Referenztypen. Werttypen sind für elementare Daten gedacht, vor allem Zahlen. Referenztypen enthalten dagegen Verweise auf Objekte. Sie können null sein, also nicht initialisiert, leer.

Swift unterscheidet ebenfalls zwischen Wert- und Referenztypen. Allerdings ist in Swift vieles ein Werttyp, was in anderen Sprachen ein Referenztyp wäre — Arrays, Dictionaries, Sets etc. Die eigentliche Besonderheit ist aber der Umgang mit dem Zustand null, der in Swift nil heißt:

  • »Gewöhnliche Variablen« — egal, ob Wert- oder Referenztypen — müssen vor vor der ersten Verwendung initialisiert werden; in Klassen muss die Initialisierung im Rahmen der Deklaration oder durch die init-Funktion erfolgen.

  • Nur wenn die Variable zusätzlich als »Optional« gekennzeichnet wird, ist auch der Zustand nil erlaubt. Optionals gibt es gleichermaßen für Wert- und Referenztypen.

Deklaration von Variablen

»Gewöhnliche« Variablen werden entweder sofort initialisiert — dann ergibt sich daraus auch gleich ihr Typ (a, b). Oder aber sie werden mit der Angabe eines Typs deklariert. Die Initialisierung kann später stattfinden, aber Sie muss vor dem ersten Auslesen erfolgen; andernfalls liefert der Compiler einen Fehler.

Optionals werden bei der Deklaration durch ein nachgestelltes Frage- oder Ausrufezeichen gekennzeichnet (e bis h). Solange sie nicht explizit initialisiert werden, haben Sie den Wert nil.

// normale Variable
var a = 1        // Werttyp (Int)
var b = NSDate() // Referenztyp (NSDate)
var c:Int        // Int, hat noch kein Wert
var d:NSDate     // NSDate, hat noch kein Wert

// Optionals
var e:Int?       // enthält anfänglich nil
var f:NSDate?    // enthält anfänglich nil

// Implicitly Unwrapped Optionals
var g:Int!       // enthält anfänglich nil
var h:NSDate!    // enthält anfänglich nil

Variablen versus Konstanten Swift unterscheidet mit den Schlüsselwörtern var und let zwischen Variablen und Konstanten. Die hier präsentierten Beispiele verwenden durchwegs var. Optionals sind aber grundsätzlich auch für Konstanten denkbar, wenngleich dies in der Praxis seltener vorkommt. In diesem Fall ist nur eine einmalige Initialisierung erlaubt. Der Compiler stellt dies sicher.

Gewöhnliche Optionals versus Implicitly Unwrapped Optionals

Wie das vorige Beispiel gezeigt hat, gibt es zwei Arten, um Optionals zu deklarieren: entweder mit einem Ausrufezeichen oder mit einem Fragezeichen. Der Unterschied äußert sich darin, wie sich die Variable beim Zugriff auf die Daten verhält.

Gewöhnliche Optionals müssen beim Auslesen jedes Mal explizit durch ein nachgestelltes Ausrufezeichen in den zugrunde liegenden Datentyp umgewandelt werden. Die Anweisung e+1 ist nicht erlaubt, weil der Plus-Operator zwar zwei Integer-Zahlen addieren kann, nicht aber ein Optional und einen Integer.

e = 3
println(e + 1)    // nicht erlaubt!
println(e! + 1)

Das nachgestellte Ausrufezeichen erzwingt eine Umwandlung des Optionals in den zugrundeliegenden Datentyp — bei e also in eine ganze Zahl. Sollte sich dabei herausstellen, dass das Optional nil enthält, tritt ein Fehler auf!

Werden Optionals hingegen in der Form Datentyp! deklariert, dann erfolgt die Umwandlung in den zugrundeliegenden Datentyp, also das sogenannte »Unwrapping«, automatisch. Aber natürlich kommt es auch hierbei zu einem Fehler, sollte das Optional nil enthalten.

g=3
println(g+1)     // OK

Implicitly Unwrapped Optionals werden oft für Eigenschaften verwendet, die nicht sofort in der init-Funktion, sondern erst später initialisiert werden. In der iOS- und OS-X-Programmierung stoßen Sie insbesondere bei Outlets ständig auf Optionals. Die folgende Variable textView wird erst kurz vor dem Erscheinen der Ansicht (der View) initialisiert. Die Variable kann verwendet werden, sobald die Methode viewDidLoad aufgerufen wurde.

class ViewController: UIViewController {
  // Zugriff auf das Textfeld
  @IBOutlet weak var textView: UITextView!
  ...
}

Die Verwendung von Implicitly Unwrapped Optionals erspart in allen weiteren Methoden das Nachstellen des Ausrufezeichens und führt so zu etwas übersichtlicherem Code.

nil-Test und if-let

Vor dem Auslesen von Optionals müssen Sie zumeist überprüfen, ob valide Daten vorliegen oder nicht. Eine Möglichkeit besteht darin, einfach varname == nil bzw. varname != nil abzufragen.

// regex hat den Datentyp NSRegularExpression?, ist also ein Optional!
let regex = NSRegularExpression(pattern:apattern, ...)
if regex != nil {
  let matches = regex!.numberOfMatchesInString(...)
}

Mit if-let können Sie den nil-Test mit einer Zuweisung kombinieren. Wenn der Ausdruck auf der rechten Seite der Zuweisung ungleich nil ist, dann wird der Wert des Optionals in der Variablen auf der linken Seite gespeichert. In n wird also das ausgepackte Optional gespeichert, und der Datentyp ist Int:

// inputString ist eine Zeichenkette, die 
// der Benutzer eingegeben hat;
// toInt versucht die Zeichenkette als Int-Wert
// zu interpretieren; die Methode liefert den Datentyp
// Int?, also ein Optional
if let n = inputString.toInt()  {  // n hat den Datentyp Int
  for var i=1; i<=n; i++ {
    println(i)
  }
}

if-let-Konstruktionen kommen in Swift derart häufig vor, dass es eine ganze Menge von Syntaxvarianten gibt. So können Sie if-let mit dem Casting-Operator as? verbinden.

if let a = opt() as? String {
  // wird nur erreicht, wenn sich opt() als String 
  // interpretieren lässt
}

Mehrere Zuweisungen können auf einmal durchgeführt werden:

if let k1 = opt1, k2 = opt2, k3 = opt3 {
  // wird nur erreicht, wenn opt1, opt2 und opt3 alle
  // ungleich nil sind
}

Vor let oder var können Sie noch eine optionale Bedingung angeben. Nur wenn diese erfüllt ist und die Optionals ungleich nil sind, wird der if-Block ausgeführt:

if condition, let/var a=opt1, b=opt2, c=opt3 {
  // dieser Code wird nur ausgeführt, wenn die Bedingung 
  // zutrifft und opt1, opt2 und opt3 jeweils ungleich nil
  // sind
}

Nach den Zuweisungen können Sie mit where eine weitere Bedingung formulieren, die bereits auf die neuen Variablen Bezug nimmt:

if let/var x=optional() where condition {
  // dieser Code wird nur ausgeführt, wenn optional() ein 
  // Ergebnis ungleich nil liefert und die Bedingung
  // erfüllt ist
}

Das Schlüsselwort where darf nur einmal nach allen Zuweisungen verwendet werden. Mehrere Bedingungen können durch logische Operatoren wie && oder || verknüpft werden:

var opt1:Int? = 4
var opt2:Int? = 2
var opt3:Int? = 3
if let a=opt1, b=opt2, c=opt3 where a==b*b && c>2   {
  println("bingo")
}

In der Praxis sehen die resultierenden if-let-Kommandos freilich selten so übersichtlich wie in den obigen Beispielen aus. Die endlos langen Methoden- und Parameternamen der iOS- und OS-X-Bibliotheken führen zu if-let-Monsterkommandos, die sich über viele Zeilen erstrecken. Zwei Beispiel aus der Praxis:

// NSBitmap in PNG-Datei speichern
private func savePNG(img:NSImage, _ fname:String)- > Bool {
    if let tiffdata = img.TIFFRepresentation,
           imageRep = NSBitmapImageRep(data: tiffdata),
           pngData = imageRep.representationUsingType(
             .NSPNGFileType, properties: [:])
    {
      return pngData.writeToFile(fname, atomically: false)
    }
    return false
}
// Zugriff auf ein Textdatei
if let path = NSBundle.mainBundle().pathForResource(
                        "bundlename", ofType: "txt"),
       txt = String(contentsOfFile: path ,
                          encoding: NSUTF8StringEncoding,
                          error: nil)
{
  // ...
}

Optional Chaining

Mitunter entstehen ganze Ketten von Optionals: Wenn ein Optional ungleich nil ist, dann soll darauf eine Methode angewendet werden. Das Ergebnis ist aber selbst ein Optional — und wenn es ebenfalls ungleich nil ist, dann soll eine seiner Eigenschaften ausgelesen werden.

if let a = optional {
  if let b = a.method() {
    if let c = b.property {
      // c verarbeiten
    }
  }
}

Der obige Code lässt sich in einem if-let-Kommando zusammenfassen:

if let a = optional, b = a.method(), c = b.property {
  // c verarbeiten
}

Aber es geht noch eleganter. Swift sieht für solche Fälle die Operatorkombination ?. vor: Wenn der vorherige Ausdruck nil ist, dann lautet das Endergebnis nil, andernfalls wird die nachfolgende Methode oder Eigenschaft ausgewertet — und so weiter. »The Swift Programming Languange« nennt diesen mehrfach verknüpften nil-Test Optional Chaining. Die Kurzschreibweise für den obigen Code lautet damit so:

if let c = optional?.method()?.property {
  // c verarbeiten
}

Optional Chaining ist in zwei Varianten besonders beliebt:

  • optional?.method() bewirkt, dass die Methode nur dann aufgerufen wird, wenn der vorangestellte optionale Ausdruck nicht nil ist. Andernfalls wird die Anweisung ignoriert, und es tritt kein Fehler auf. Sie ersparen sich also umständlichen Code der Art if optional != nil { optional!.method() }.

  • optional?.property = xxx bewirkt analog, dass die Eigenschaft nur dann verändert wird, wenn der vorangestellte optionale Ausdruck ungleich nil ist. Auch hier ersparen Sie sich den Test optional != nil.

Fluch oder Segen?

Der Umgang mit Optionals ist gewöhnungsbedürftig. Der Code wird durch die vielen Frage- und Ausrufezeichen nicht unbedingt lesbarer. if-let-Monsterkommandos tragen ebenfalls nicht gerade zur guten Verständlichkeit des Codes bei.

Die relativ sperrige Syntax wird damit begründet, dass man mit ihrer Hilfe einen ungewollten Zugriff auf eine nicht initialisierte Variable vermeidet, also ein in anderen Programmiersprachen häufiges Problem (null pointer exception).

An sich ist die Idee gut — wenn Optionals die Ausnahme und nichtoptionale Variablen die Regel wären. In der Praxis ist es aber gerade umgekehrt: Bei der Entwicklung von iOS- oder OS-X-Programmen entsteht oft der Anschein, als würde nahezu jede Eigenschaft oder Methode Optionals liefern. Somit müssen Sie Ihren Code ständig gegen den Fall abzusichern, dass eine Variable oder ein Ausdruck eben doch einmal nil liefert.

Nach einigen Monaten gewöhnt man sich an Optionals wie man sich eben an andere Eigenheiten diverser Programmiersprachen gewöhnt. Aber eine rechte Begeisterung für das Konzept mag sich nicht einfinden. Vermutlich liegt das weniger an der Idee an sich, sondern daran, dass nicht einmal Apple über Nacht seine ganzen Frameworks und Libraries auf den Kopf stellen kann. Die Foundation, Cocoa und Cocoa Touch wurden entwickelt, bevor es Optionals gab — und das merkt man leider bei jeder Zeile Code, die man schreibt.

Zu nahezu denselben Schlussfolgerungen ist übrigens der Blogger David Kopec gekommen, aus dessen Beitrag My Experience Building a Small Mac App in Swift ich hier einige Sätze zitiere:

The big problem with optionals though is interacting with the Cocoa frameworks. I feel like Swift was designed for most variables to be non-optional, non-nullable values (I could be wrong of course) and optionals are supposed to be the special cases. But instead, due to having to work with the Objective-C Cocoa frameworks, almost everything is an optional.