Swift-Flight, Schwalbenflug, auf Crash-Kurs mit Swift

Nach meinem ersten generellen Eindruck von Swift dokumentiere ich in diesem Artikel das Kennenlernen von Swift aus Sicht eins langjährigen Objective-C Entwicklers.

Swift ist eine Schwalbe. Also auf deutsch. Und die bekannten Werbesprüche über und Hypes um Swift haben mit der Wahrheit nicht viel zu tun. Viel von dem Rummel ist kommerzielles Interesse, weil man Schulungen, sich selbst und neue Bücher durch Swift verkaufen kann. Je mehr Swift über den grünen Klee gelobt wird, desto größer ist der Bedarf und der Berg Kohle, den man einnehmen kann damit. Was bin ich froh, daß meine Seite hier nichts verkaufen will.

Swift und C

So stellte Apple Swift auf der WWDC 2014 mit der Schlagzeile "Objective-C without the C" vor. Das ist schlicht gelogen, denn Swift benötigt C, weil alle Libraries, die es benutzt, in C und seinen Verwandten geschrieben sind, und Swift arbeitet mit C zusammen wie Mike Ash auf der NSSpain 2014 in seinem Vortrag Swift and C zeigt. Ich habe seinen Playground, der zeigt, wie man C in Swift verwendet, aktualisiert und auf GitHub zur Verfügung gestellt.

Weil Swift zwingend auf C und Objective-C angewiesen ist, da alle Frameworks darin geschrieben sind, kommt auch Swift nicht ohne Pointer aus. OS X ist ein Unix und Unix ist C. OS X (und damit iOS et cetera) ist in C und C++ geschrieben und die Application-Libraries in C und Objective-C. Selbst wenn Apple es wollte, sie könnten die C-Sprachen nicht ablösen.

Tatsächlich stellt Swift die ganze C stdlib, die C Standard Library, aus Darwin, dem Open Source Unix-System in OS X, zur Verfügung – in Swift-Syntax. Man kann also alles in Swift machen wie in C, jede ach so verpönte Sache, nur halt in Swift-Syntax. Swift ist also C im Schafspelz plus native Swift-Features.

Swift ist unsicher

Viele Turorials und Bücher zu Swift behaupten, Swift würde "unsicheres" Pointer-Management beseitigen ("Swift removed unsafe pointer management"). Wie man unter anderem an der UnsafePointer Structure von Swift sehen kann haben diese Tutorials keine Ahnung, wovon sie sprechen.

Und überhaupt: Wollen die mich veralbern? Ich habe schon vor 30 Jahren, Mitte der 80er, im Informatik-Unterricht in der Schule mit Pointern gearbeitet in Turbo Pascal. Und jetzt soll das ein Problem sein? Hallo, McFly, jemand zuhause? Spielen wir jetzt "Java" und nennen alle Pointer "Referenzen" nach dem Motto: Was ich nicht sehe, ist auch nicht da?

Man kann zum Beispiel in Swift schreiben (auch im Playground):

let ptr = malloc(42)
free(ptr)

Der ptr ist dann bei mir laut Playground ein "UnsafeMutablePointer(0x7FA6F0E1BD70)", also ein Pointer auf Speicher mit genannter Adresse. Und es ist ein … "unsafe pointer". In Swift! Mufasa! Pointer! Pointer!

A pointer! Now that's power! Wenn Apple also Sicherheit als Abwesenheit von Pointern in Swift definiert, nun, dann ist Swift wohl unsicher:

var pointerToString:UnsafeMutablePointer<String>!
pointerToString = UnsafeMutablePointer<String>.alloc(1)
pointerToString.initialize("yo man")
print("memory content: " + pointerToString.memory) // "memory content: yo man"
pointerToString.memory = "hello"
print("memory content: " + pointerToString.memory) // "memory content: hello"

Wo wir gerade über Sicherheit sprechen: Selbst mit dem Swift-Compiler gibt es nach über einem Jahr in freier Wildbahn immer noch Probleme. Hier gibt es Tests, die den Swift-Compiler crashen lassen. Zum Zeitpunkt meines Schreibens sind es noch 407 von 3720.

Ein weiteres Kern-Feature macht Swift unsicher: Das Unwrapping von Optionals kann es so richtig krachen lassen.

Swift ist schwer

Man hört, Swift wäre einfach. Aber Code, der die wirklich typischen neuen Features von Swift benutzt, sieht zum Beispiel so aus:

func add(a: Int) -> (Int -> Int) {
    return {b in a + b }
}
let sum = add(4)(5)

Ist das einfach? Wir sehen hier Function Currying, Type Inferring und einzeilige Closures ohne Return. Du glaubst, Function Currying würde ansonsten nicht benutzt in Swift? Ha! Instanz-Methoden sind Curried Functions in Swift. Da ist doch ein Return zu sehen? Ja, aber ich meine das nicht sichtbare Return im Closure. Wie "nicht sichtbar"? Du siehst, es gibt noch eine Menge zu lernen, bevor man Swift wirklich "kann".

Swift 3 nimmt viele Änderungen vor, um die Sprache leichter lesbar, einfacher und weniger komplex zu machen laut WWDC 2016: What's new in Swift. So wurde das Function Currying beseitigt mit Swift 3. Ein klares Eingeständnis, daß Swift bisher eben nicht so einfach war, wie es verkauft wurde.

Im Rest dieses Artikels werden an verschiedenen Stellen noch mehr Punkte hinzukommen, an denen Swift komplizierter ist als Objective-C.

Swift bis 1.2 ohne Sets

Swift bekam erst mit Version 1.2 Set (Menge) als nativen Datentyp. Eine Programmiersprache ohne Sets rauszubringen, das zeigt, wie eilig Apple es offenbar hatte. Relationen in Core Data werden unter anderem mit Sets abgebildet. Unter anderem? Ja, denn NSOrderedSet geht auch und das ist keine Subklasse davon.

Daß es daher mit Core Data Relationen in Swift anfangs Probleme gab, wundert mich jetzt jedenfalls nicht mehr. Aber es bestätigt meine Einschätzung, daß es zu riskant ist, ein so neue und noch völlig im Fluß befindliche Sprache so früh in etwas anderem als in Test-Projekten einzusetzen.

Overkill mit switch und case

In Swift können Switch-Statements nicht nur die gewohnten Integer-Werte bzw. Enumerations verwenden, sondern beliebige Typen. Man kann sogar mehr als einen Vergleichswert haben, indem man Tupel benutzt. Und im Case-Teil kann man als Einzelwerte oder in einem Tupel auch noch Ranges, Wildcards (das _ trifft auf alles zu) verwenden, Variablen definieren und auch noch Where-Clauses einsetzen.

switch (indexPath.section, indexPath.row) {
    case (0, _):
        println("Konfiguriere Sektion 0")
    case (1, 0):
        println("Konfiguriere Sektion 1, Reihe 0")
    case (1, _):
        println("Konfiguriere die restlichen Zellen von Sektion 1")
    case (2, 3...5):
        println("Konfiguriere in Sektion 2 die Zellen in den Zeileb 3 bis 5")
    case (2, _):
        println("Konfiguriere alle anderen Reihen in Sektion 2")
    case (_, 0):
        println("Konfiguriere Reihe 0 in allen übrigen Sektionen")    
    case (var section, let row) where contains(highlightedIndexPaths, indexPath):
        section++
        println("Konfiguriere optisch hervorgehobene Zelle in Reihe Nr. \(row + 1), Sektion Nr. \(section)")”
    case (var section, let row):
        section++
        println("Konfiguriere die Reihe Nr. \(row + 1) in Sektion Nr. \(section)")
}

Code-Beispiel basiert auf dem iBook von Maurice Kelly Swift Translation Guide for Objective-C Users.

Das ist ein gutes Buch für mich, weil es auf meinem Wissen über Objective-C aufsetzt und den Einstieg in Swift schneller ermöglicht als wenn man ganz neu im Thema Apple-Programmierung wäre.

Die im Case-Teil definierten Variablen verhalten sich ähnlich wie Wildcards: Sie treffen auf jeden beliebigen Wert zu. Es gibt kein break, weil ein automatisches Weiterlaufen in Swift nicht passiert. In Swift ist es genau andersherum: Man muß ein Weiterlaufen, wenn es gewünscht wird, mit fallthrough anstoßen. Man kann auch mehrere Bedingungen durch Kommata getrennt in einer Zeile schreiben, um sie zu kombinieren. Das kann man an Stellen tun, wo sich mit Objective-C mehrere case ohne break anbieten würden.

Optionals und ihre Folgen

Ob die Optionals in Swift, die den Code übersähen mit ? und ! besser sind als das gutmütige Nil-Verhalten von Objective-C? Das wage ich zu bezweifeln.

Objective-C ignoriert Messages an Nil. Bei anderen Sprachen muß man das abfangen, weil es sonst knallt. Das ermöglicht schlanken Objective-C Code mit weniger Fallunterscheidungen. Auswertungen auf Nil werden auch gutmütig und intuitiv gehandhabt: Nil interpretiert als BOOL ist NO, als egal welche Zahl interpretiert ist es 0. Der BOOL Typ von Objective-C ist übrigens über ein Jahrzent älter als der bool Typ von C.

Swift geht einen anderen Weg und will von vornherein, also beim Deklarieren der Variable, festlegen, ob die Variable immer einen Wert haben muß oder ein Optional ist. Optionals werden mit ? deklariert. Und immer wenn man ein Optional an ein Nicht-Optional zuweisen will, muß man es forciert mit ! machen. Objective-C erreicht hier Sicherheit auf eine entspanntere Art und Weise, macht allerdings das Debugging im Fehlerfall etwas mühsamer, weil bei ungeplanten Nil-Werten unerwartetes Verhalten erst etwas später im Code auftritt.

Die Regelungswut von Swift dient wahrscheinlich dazu, daß der Compiler den Code dann besser optimieren kann.

Ein Bool-Optional, dessen Wert man negiert an einen Nicht-Optional zuweisen möchte, hat dann links und rechts ein Rufzeichen. Links die Negation, rechts das Forced Unwrapping. Schön ist anders.

var boolOptional: Bool? = false
let choice = !boolOptional!

Um sich wiederholtes Forced Unwrapping bei einem Optional zu ersparen, kann man auch einen Implicitly Unwrapped Optional verwenden, der weiterhin ein Optional ist, aber immer automatisch forciert ausgepackt wird. Die text Variable wird bei jeder Verwendung automatisch auch ohne ! forciert entpackt:

var optionalText: String? = "hallo, welt!"
let text: String! = optionalText
let capsText = text.capitalizedString

Wenn sich beim forcierten oder impliziten Unwrapping jedoch herausstellt, daß kein Wert enthalten ist, dann crasht das Programm zur Laufzeit. Inkonsequenterweise macht ein leeres Optional jedoch keine Probleme als Parameter für print() und beim Einbetten in einen String.

Wo Objective-C mit Messages gegen nil keine Probleme hat …

UIView *superGrannyView = myView.superview.superview.superview

… muß man bei Swift das Optional Chaining einsetzen, was den Code mit Fragezeichen garniert.

let superGrannyView = myView.superview?.superview?.superview

Eleganter finde ich es ohne. Was Swift an Semikolons einspart, reißt es dreimal wieder um mit seiner Flut an !!!!! und ????.

Der ternäre Operator ?: sieht in Swift ebenso aus, aber wenn man die Kurzform damit verwenden möchte, bei der man den Wert zwischen ? und : wegläßt, um den ersten Wert zuzuweisen, falls er nicht nil ist, ansonsten den letzten Wert, dann verwendet Swift ?? dafür.

NSString *text = someText ?: "war wohl nil"
let text = someText ?? "war wohl nil"

Funktionen

Bei Objective-C unterscheidet man zwischen Methoden, die objektorientiert an Klassen hängen, und C-Funktionen. Swift nennt alles Funktion und kann sie an Klassen, Structures und Enumerations hängen sowie alleinstehend definieren. Swift-Funktionen haben deutlich komplexere Variationsmöglichkeiten als Objective-C Methoden.

Parameter

Bei Objective-C hat man in der Regel benannte Parameter, man kann die Benennung aber auch weglassen, was böse und unüblich ist, und hat dann nur einen Methodennamen und jeweils einen Doppelpunkt vor allen Parametern.

In Swift kann man unbenannte Parameter haben sowie interne Namen für die Verwendung innerhalb der Methode und externe Parameternamen für den Methoden-Aufruf. In optischer Anlehnung an Objective-C läßt man den externen Namen des ersten Parameters in Swift oft weg und hängt ihn an den Methodennamen. Bei Objetive-C ist allerdings die ganze Parameterbenennung Teil des Methodennamens. Swift trennt Funktionsname und Parameternamen.

Default Parameter-Werte

Swift führt Default Parameter ein. Damit kann man dasselbe erreiche, was man bisher in Objective-C mit Convenience-Methoden umgesetzt hat: Eine Methode mit weniger Parametern ruft eine andere Methode mit mehr Parametern und Default-Werten auf. Mit Swift kann das nun in einer Methode abgefrühstückt werden, was aber die Lesbarkeit nicht unbedingt erhöht. Default-Werte werden mit = an den Parmeter angehängt.

var: Veränderbare Parameter

Methoden-Parameter in Objective-C können innerhalb der Methode immer verändert werden. In Swift muß man dazu var vor den Parameter schreiben, ansonsten weigert sich Swift, den Parameter zu verändern. Wieder ein Prinzip aus der Funktionale Programmierung. Das ist jedoch nicht zu verwechseln mit Call-By-Reference und Call-By-Value, denn Parameter in Objective-C werden immer By-Value übergeben. Swift möcht einfach nur sicherstellen, daß man im Laufe der Methode den ursprünglichen Input nicht versehentlich verändert.

inout: Veränderbare Output-Parameter

Wird ein Parmeter in Swift mit inout markiert, dann kann die Funktion den Parameter nicht nur verändern, sondern die Änderung wird auch in die übergebene Variable zurückgeschrieben. Dies wird gerne mit den typischen NSError Output-Parametern von Objective-C verglichen, wo ein Pointer auf einen Pointer übergeben wird, um den Original-Pointer im aufrufenden Code innerhalb der Methode ändern zu können, weil auch Pointer By Value übergeben werden. Dadurch kann die Methode den Pointer des aufrufenden Codes auf ein anderes Objekt zeigen lassen.

By Value sind die Funktions- bzw. Methodenaufrufe in (Objective-)C, weil ihre Instructions auf den Stack gelegt werden zusammen mit Kopien der Parameter. Das ähnelt dem var-Paremter von Swift, weil auch hier eine veränderliche lokale Kopie des Parameters beim Aufruf der Funktion entsteht.

Interessanterweise gibt es aber auch in Objective-C einen inout-Modifier für Parameter, zum Beispiel hier:

- (void)popoverPresentationController:(UIPopoverPresentationController *)popoverPresentationController
          willRepositionPopoverToRect:(inout CGRect *)rect
                               inView:(inout UIView * _Nonnull *)view

Der zugehörige Swift-Code benutzt jedoch kein inout:

optional func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController,
                willRepositionPopoverToRect rect: UnsafeMutablePointer<CGRect>,
                                     inView view: AutoreleasingUnsafeMutablePointer<UIView?>)

Und hier:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
                     withVelocity:(CGPoint)velocity
              targetContentOffset:(inout CGPoint *)targetContentOffset

Und wieder der zugehörige Swift-Code:

optional func scrollViewWillEndDragging(_ scrollView: UIScrollView,
                           withVelocity velocity: CGPoint,
                    targetContentOffset targetContentOffset: UnsafeMutablePointer<CGPoint>)

Ein inout ist der Default-Wert bei Objective-C und bedeutet, daß die Structure beim Aufruf kopiert wird und am Methodenende wieder zurückkopiert wird in den aufrufenden Code. Es gibt auch einzelne in und out Modifier, die dann nur ein Kopieren beim Aufruf (also nur lesend) bzw. beim Rückkehren (also nur schreibend) deklariert.

Der inout-Modifier hat jedenfalls bei Swift und Objective-C nicht dieselbe Bedeutung. In Anlehnung an die Output-Parameter (Pointer auf Pointer) in C und Objective-C werden bei Swift die inout-Parameter mit & übergeben, um ersichtlich zu machen, daß die aufgerufene Funktion den Parameter nach außen verändern kann.

Im Alltag wird ein typischer Objective-C Entwickler eigentlich nie den Objective-C inout-Modifier benutzen. Der Swift inout-Modifier entspricht dem auch nicht, sondern ist ein Äquivalent zum Pointer-auf-Pointer, wie man es von NSError-Parametern kennt.

Man beachte auch, daß Swift hier UnsafeMutablePointer verwendet. Also nichts mit "sicher" und "keine Pointer". Falls das mal wieder jemand behauptet, dürft Ihr ihn auslachen.

Variadic Parameter

Bei Objective-C arbeitet man mit va_list und va_arg et cetera. Bei Swift bekommt man direkt einen Array geliefert, der mit for-in durchlaufen werden kann.

Variadic Parameter in Swift stören sich jedoch an den Default Values von Swift: Man kann keine Variadic Parameter übergeben, wenn man Default Values beim Aufrufen verwendet, da der Variadic Array in dem Fall leer wäre.

Funktionen als Parameter und Rückgabewerte

Wenn man Swift wirklich hätte einfach gestalten wollen, dann hätte man Funktionen nicht als Parameter oder als Rückgabewerte ermöglicht. C kann das und damit Objective-C auch. Aber Swift ist doch angeblich eine einfachere Sprache. Wer schon bei Pointern Angst bekommt, sollte lieber einen großen Bogen um Funktionen als Parameter oder Rückgabewerte machen.

Da Swift Funktionale Programmierung ermöglichen soll, ist dies jedoch ein zwingendes Feature, das die Sprache beherrschen muß. Funktionen, die mit Funktionen hantieren, zeigen deutlich, daß Funktionale Programmierung nicht einfach ist, egal wie cool das Buzzword auch kommt.

Return Values

Swift kann außer den üblichen Typen auch Tupel zurückgeben bei Funktionsaufrufen. Das macht den Code jedoch schnell schwieriger lesbar und wartbar, wenn man es damit übertreibt.

Blocks und Closures

Swift hat wie die C-Sprachen auch Blocks und nennt sie Closures. Wie die C-Sprachen verwendet auch Swift eine fast identische Syntax für Funktionen und Blocks bzw. Closures. Damit verhalten sich beide Welten immerhin ähnlich: Wer Funktionen versteht, kann auch mit Blocks und Closures umgehen.

Anstelle des Caret ^ hat Swift "in" als Keyword, um ein Closure zu kennzeichnen. Das soll angeblich darauf hinweisen, daß die Parameter in den darauf folgenden Statements verwendet werden. Allerdings kann man die Parameter auch komplett weglassen, was diese Vermutung absurd erscheinen läßt.

Wer sich mit dem Lesen der Block-Syntax schwer tut, dem hilft die Rechts-Links-Regel von C, mit der man jede C-Deklaration und somit auch jede Objective-C-Deklaration entziffern und erstellen kann. Blocks sind nur Funktionen mit einem ^ anstatt oder vor dem (Funktions/Block-) Namen.

Analog dazu sieht in Swift ein Closure aus wie eine Funktion mit "in" am Ende. Wer also Swift dafür lobt, daß Closures wie Funktionen aussehen, kann dasselbe Lob auch den C-Sprachen zuteil werden lassen, wo Blocks ebenfalls wie Funktionen aussehen.

Closure als Parameter

Bei Objective-C und C kann man einen Block und eine Funktion, die jeweils als Parameter übergeben werden, noch am vorhandenem oder fehlendem ^ unterscheiden. Bei Swift gibt es keinen erkennbaren Unterschied mehr: Closures und Funktionen sind austauschbar, wenn sie als Parameter für eine Funktion dienen:

// A Function taking Int and Int and returning Int
func funcTest(_: Int, _: Int) -> Int {
    return 1;
}

// A Closure taking Int and Int and returning Int
var closureTest = { (someInt: Int, otherInt: Int) -> Int in
    return 2;
}

// Invoking function and closure directly
funcTest(1, 2) // returns 1
closureTest(1, 2) // returns 2

// This function can take a closure or a function for parameter
func doSomething(dingsTest: (Int, Int) -> (Int)) -> Int {
    return dingsTest(1, 2)
}

// functions and closures can be used interchangeable for parameter
doSomething(funcTest) // returns 1
doSomething(closureTest)  // returns 2

In C und damit auch in Objective-C können Blocks und Functions nicht gegeneinander ausgetauscht werden:

#include <stdio.h>

// funcTest is a function taking two ints and returning int
int funcTest(int a, int b) {
    return 1;
}

// blockTest is a block taking two ints and returning int
int(^blockTest)(int, int) = ^int(int a, int b) {
    return 2;
};

// doSomething is a function taking a block and returning int
int doSomething( int(^dingsTest)(int,int) ) {
    return dingsTest(1, 2);
}

// doOtherThing is a function taking a function and returning int
int doOtherThing( int(dingsTest)(int,int) ) {
    return dingsTest(1, 2);
}

int main(void) {

// This line would give compiler error:
// passing 'int (^)(int, int)' to parameter of incompatible type 'int (*)(int, int)'
// int a = doOtherThing(blockTest);

// doSomething can only take a block but no function
int a = doSomething(blockTest);
printf("a value is %d\n", a); // prints 2

// doOtherThing can only take function but no block
int b = doOtherThing(funcTest);
printf("b value is %d\n", b); // prints 1

return 0;
}

Wenn das Closure der einzige oder letzte Parameter eines Funktionsaufrufes ist, dann darf man es hinter die Funktions-Klammer schreiben.

Implizites Return

Wenn ein Closure nur aus der Return-Zeile besteht, dann kann das return vor dem Befehl weggelassen werden. Ein ganz toller Compiler ist das, der das alleine bemerkt, aber gut lesbar finde ich es nicht. Man merkt, daß Swift von einem Compiler-Guru entworfen wurde.

Type Inference

Wenn der Swift-Compiler die Typen der Parameter und Rückgabewerte erschliessen kann, dann kann man die Typangaben weglassen. Bleiben dann nur noch die Parameternamen übrig, dann kann sogar die Klammer um die Parameterliste fortfallen.

Bei Objective-C Blocks kann die Deklaration des Rückgabewertes ebenfalls aus dem Block erschlossen werden und ist daher auch hier optional. Wenn man den Return Type wegläßt und die Parameterliste zufällig (void) ist, dann kann man dieses (void) auch noch weglassen.

Shorthand Argument Names

Wenn durch Type Inference nur noch die Namen der Parameter und das "in" Keyword übrigbleiben, dann kann man sowohl die Namen als auch das "in" auch noch komplett weglassen. Man kann dann allerdings auf die Parameter im Closure nur noch mit $0 und $1 und so weiter zugreifen.

Wieder ein Beispiel, wo eine Swift-Option zur "Optimierung" des Codes die Lesbarkeit verschlechtert.

Operator Functions

Operatoren wie < und == et cetera sind in Swift Funktionen, sogenannte Operator Functions. Ein Operator kann also wie eine Funktion benutzt werden. Darum kann ein Operator auch wie eine Funktion als Parameter übergeben werden.

Oben hatten wir gesehen, daß Closures und Funktionen gegenseitig ausgetauscht werden können, wenn die Typen ihrer Parameter und Rückgabewerte übereinstimmen. Wenn die Signatur eines Closures also mit der Signatur einer Operator-Funktion übereinstimmt, dann kann man die Operator-Funktion als Parameter übergeben, wo das Closure erwartet wurde. Und weil die Operator-Funktion identisch mit dem Operator ist, genügt es, den Operator zu übergeben.

Hier sind ein paar Schritte, die zeigen, wie stark Swift-Code eingedampft werden kann. Ob das lesbar und wartbar ist, wage ich zu bezweifeln. In dem Beispiel machen match0 bis match6 alle dasselbe. Im letzten Schritt wird ein Closure durch einen Operator, der ja eine Operator-Funktion ist, ersetzt.

// structure has a function
// function takes a closure for parameter
// closure takes two Strings, returns Bool
// function compares strings by calling that closure
struct Pair {
    var first:String, second:String
    func matchesComparison(comparison:(String, String) -> Bool) -> Bool {
        return comparison(first, second)
    }
}

let pair = Pair(first: "Markus", second: "MacMark.de")

// basic syntax
let match0:Bool = pair.matchesComparison({
    (one:String, other:String) -> Bool in
        return one >= other
})

// trailing closure syntax
let match1:Bool = pair.matchesComparison() {
    (one:String, other:String) -> Bool in
        return one >= other
}

// type inference
let match2 = pair.matchesComparison() {
    one, other in
        return one >= other
}

// implicit return
let match3 = pair.matchesComparison() {
    one, other in
        one >= other
}

// shorthand argument names
let match4 = pair.matchesComparison() {$0 >= $1}

// trailing closure: dropping paranthesis
let match5 = pair.matchesComparison {$0 >= $1}

// replace the closure with an operator (i. e. operator function)
let match6 = pair.matchesComparison(>=)

Das Code-Beispiel basiert auf dem iBook von Maurice Kelly Swift Translation Guide for Objective-C Users und wurde von mir ergänzt.

Das ist ein gutes Buch für mich, weil es auf meinem Wissen über Objective-C aufsetzt und den Einstieg in Swift schneller ermöglicht als wenn man ganz neu im Thema Apple-Programmierung wäre.

Äußere Variablen ändern

In Objective-C muß man alle äußeren Variablen, die innerhalb eines Blocks änderbar sein sollen, mit __block markieren. In Swift ist das nicht nötig, der Compiler nimmt das automatisch unsichtbar vor. Das ist insofern problematisch als man nicht mehr direkt erkennen kann, welche Variablen eventuell im Block geändert werden.

Der Objective-C Compiler, bzw. das Clang-Frontend für LLVM, erkennt auch, welche äußeren Variablen im Block geändert werden und meldet das als Fehler, bis man __block davor schreibt. Das Swift-Frontend für LLVM nimmt die Änderung automatisch vor, was vielleicht nicht immer im Sinne des Entwicklers ist. Mir kommt dieses Verhalten uneinheitlich vor, weil Swift ansonsten darauf bedacht ist, alles explizit zu machen, z. B. konstante und änderbare Variablen mit let und var zu unterscheiden.

Strings

Swift implementiert Strings als ein Struct mit Properties, die den Inhalt des Strings als verschiedene Collections präsentieren können. Und zwar als Collection von Character, UnicodeScalar, UTF16.CodeUnit und UTF8.CodeUnit.

Die einzelnen Zeichen im String sind nicht gleichmäßig 1 Byte lang, sondern variabel. Dadurch kann man nicht einfach einen Integer als Index für den String-Inhalt benutzen oder den Index um 1 hoch oder runterzählen, denn dann würde man nicht korrekt treffen, sondern eventuell zwischen 2 Bytes landen, die zu einem Zeichen gehören.

NSString unterstützt Unicode mit 16-Bit Encoding.Swift String unterstützt auch das neuere 21-Bit Encoding. Wo Objective-C eine sichtbare Einschränkung hat, ist bei Variablen-Namen. Die müssen ASCII-Buchstaben enthalten. Bei Swift dürfen Variablen-Namen hingegen Unicode-Zeichen enthalten.

Auch sind Indizes nicht von String zu String übertragbar. Jeder String in Swift produziert individuelle Index-Werte. Und das sind keine Integer, sondern String.CharacterView.Index.Types.

Das alles macht spezielle Methoden nötig, mit denen man sich im Swift String bewegen kann, denn 1 Byte ist hier nicht 1 Char.

var name = "MacMark"
var index = name.startIndex // 0
var character = name[index] // M
index.dynamicType           // String.CharacterView.Index.Type
name.dynamicType            // String.Type
index = index.advancedBy(2) // 2
name[index]                 // c

NSString und Unicode ist eine eigene Betrachtung wert, denn auch bei Objective-C kommen Unicode-Zeichen nur im Falle der normalen Zeichen (das sind ganz grob gesagt alle außer Emojis) mit einer Code Unit aus. Alles andere benötigt zwei Code Units (ein Surrogate Pair).

Die length-Methode von NSString gibt die Anzahl der unichar im String zurück. Die Angabe entspricht der Anzahl sichtbarer Zeichen jedoch nur, wenn der NSString Zeichen aus Unicode Plane 0, dem Basic Multilingual Plane, enthält, in dem alle Zeichen der Welt enthalten sind. Emojis liegen jedoch in Plane 1 und erfordern 2 unichars pro Emoji. Dadurch ist diese Ausgabe von length unerwartet:

NSString *hey = @"😛";
NSLog(@"length %zd", hey.length); // length 2

Bei Swift gibt es offenbar aus diesem Grund kein length für String. Es gab mal countElements() und danach gab es count(), die als globale Funktionen die Anzahl der Characters lieferten, aber in Swift 2 sind die nicht enthalten und nun zählt man die Zeichen am besten mit characters.count so:

var countMe = "hallo"
countMe.characters.count // 5
countMe.utf16.count // 5
countMe.utf8.count // 5
countMe.unicodeScalars.count // 5
countMe = "😛"
countMe.characters.count // 1
countMe.utf16.count // 2
countMe.utf8.count // 4
countMe.unicodeScalars.count // 1

Je nachdem welches Property des String gewählt wird, ergibt sich eine andere Darstellung des Inhalts mit einer anderen Länge.

Ein Lichtblick ist, daß man Strings recht elegant zusammenbauen kann:

let greeting = "Hey, \(name)" // Hey, MacMark
let longGreeting = greeting + ", what's up?" // Hey, MacMark, what's up?

Eine Schattenseite ist, daß Swift offenbar selbst nichts anbietet für reguläre Ausdrücke. Ich sehe keine Alternative zu NSPredicate und NSRegularExpression. Auch für Substrings muß man auf Methoden von NSString zurückgreifen, die im String Type von Swift verfügbar sind. Viele Methoden von NSString lassen sich auch auf String von Swift aufrufen, aber nicht alle.

Klassen

Klassen in Swift müssen von keiner Klasse erben. Klassen, Structures und Enumerations in Swift unterscheiden sich hauptsächlich dadurch, daß nur Klassen Funktionen und Daten erben können. Speicherverwaltung läuft wie gewohnt über Automatic Reference Counting (ARC), deinit übernimmt die Funktion von dealloc.

Swift erfordert zum Überschreiben einer Funktion das Schlüsselwort override davor. In meinen Augen ist es das übliche Swift-Babysitting des Entwicklers und explizites Getue. Andere nennen es Sicherheit. Aber es macht Swift schwerfällig, Objective-C hat echt weniger deklarativen Ballast, ist ja auch eine dynamische Sprache im Gegensatz zu Swift. Man sollte einen Entwickler nicht in Watte packen.

Auch der designierte Initializer wird in Swift ausdrücklich festgelegt, indem alle anderen Initializer mit convenience markiert werden. Bei Objective-C ist es schlicht der Inititializer mit den meisten Parametern. Den Aufruf eines geerbten Initializers macht man bei Objective-C am Anfang, bei Swift am Ende des designierten Initializers.

Aufgrund seiner Natur muß Swift andere Wege gehen, um dasselbe zu erreichen wie Objective-C. Bei Swift sagt man nicht class, sondern type.

Aufgrund seiner Dynamik würde eine Zugriffskontrolle auf Daten und Methoden in Objective-C keinen Sinn ergeben. Swift kennt private, internal und public, wobei internal der Default-Wert ist für alles. Internal bedeutet Sichtbarkeit für alles Sourcen auf Module-Ebene, was einem Target entspricht. Private bezieht sich auf die Quellcode-Datei.

Header Files

C-Sprachen haben Header-Files, die als Übersicht dienen, was man an Funktionen und Methoden zur Verfügung hat. Anfangs gab es das nicht bei Swift, aber ab Swift 2 werden Header dynamisch angezeigt von Xcode, wenn man Navigate -> Jump to Generated Interface benutzt.

Es ist halt schon einfacher, sich in einem Header eine Übersicht zu verschaffen, als sich durch die komplette Implementierung zu lesen.

Stored Properties

Stored Properties haben eine Instanzvarible und können auch für die Klassenebene als type properties deklariert werden. In Objective-C werden die Accessor-Methoden vom Compiler erzeugt, können aber auch vom Entwickler angelegt werden, um

Da Swift keine Accessor-Methoden für Stored Properties erzeugt, bzw. diese nur für Computed Properties vom Entwickler angelegt werden, muß Swift andere Wege gehen, um obiges zu erreichen:

class niceClass {
    var someText = "text"
    var otherText = "more text"
}

Computed Properties

Angeblich neu in Swift, aber das hatte Objective-C auch schon: Wenn man in Objective-C die benötigten Accessor-Methoden selbst implementiert, dann wird keine Instanzvariable erzeugt. Das passiert in dem Fall nur, wenn man es erzwingt mit:

@synthesize myProp = _myProp;

In Swift hat eine Computed Property keine zugrundeliegende Variable und dient dazu, andere Properties zu ändern. Es gibt Swift die Möglichekeit, wie in Objective-C im Setter eines Properties andere Properties zu aktualisieren. In Swift werden die Accessor-Methoden einer Computed Property direkt nach der Variablen in einem Block angegeben. Sie heißen immer get und set, haben keinen Return-Type angegeben und selbst den Parameter für set kann man sich sparen, denn Swift nennt ihn dann einfach newValue.

class greatClass {
    var someText = "text"
    var otherText : String {
        get {
            return someText
        }
        set (newText) {
            someText = newText
        }
    }
}

Im Gegensatz zu Stored Properties können Computed Properties nicht als Type Properties deklariert werden.

Lazy Stored Properties

In Objective-C haben Properties oft eine Getter-Methode, die das Property erst und nur beim ersten Zugriff initialisiert, die Lazy Initialization. Da Swift keine Getter-Methode für ein Stored-Property hat, muß man dieses Verhalten in Swift auf andere Weise nachbilden.

Wenn man ein Swift Property mit lazy markiert, dann wird es auch erst initialisiert, wenn es das erste Mal direkt gelesen wird.

class someClass {
    var someText = "text"
    lazy var someView  = UIView() // or some really expensive class
}

var mc = someClass()
mc.someText
mc.someView // someView initialized

Property Observers

Swift hat kein Key Value Observing (KVO), das Objective-C Entwickler kennen. Swift bietet jedoch Property Observer für einen ähnlichen Effekt. Die Code-Blöcke willSet und didSet, die man an einem Property deklarieren kann, geben Zugriff auf den neuen bzw. den alten Wert, wenn eine Änderung vorgenommen wird.

Dies ist auch der Swift-Weg, um weitere Methoden aufzurufen, falls das Property geändert wird. Außerdem kann man didSet verwenden, um zu entscheiden, ob das Property überhaupt aktualisiert werden sollte, indem man es auf oldValue setzt. Die Parameter sind unter newValue bzw. oldValue verfügbar, falls man keine eigenen Namen vergibt.

class aClass {
    var text: NSString? {
        willSet {
            // do something
            newValue
        }
        didSet {
            // do something
            oldValue
        }
    }
    
    var text2: NSString? {
        willSet (shinyNew) {
            // do something
            shinyNew
        }
        didSet (oldVersion) {
            // do something
            oldVersion
        }
    }
}

In Objective-C kann man analoges Verhalten nicht nur mit KVO umsetzen, sondern auch wie oben erwähnt direkt in den Accessor-Methoden.

Key Value Observing (KVO)

Swift hat kein Key Value Observing. Das dürfte daran liegen, daß KVO die Dynamik von Objective-C verwendet und ohne dies nicht funktionieren kann. KVO macht isa swizzling. KVO erzeugt spontan zur Laufzeit eine Subklasse der beobachteten Klasse und überschreibt darin den Setter der beobachteten Property. Setter werden allein schon aufgrund der Deklaration eines Properties vom Compiler angelegt.

Wird nun der Setter aufgerufen, wird tatsächlich die synthetische Subklasse aufgerufen, die KVO mit der nötigen "Magie" versorgt, und dieser Setter ruft wiederum den ursprünglichen Setter auf. Dieses Verhalten ist vollkommen transparent für das beobachtete Objekt. Es sieht also nur nach Magie aus, wenn man das nicht weiß.

Jedes Objective-C Objekt hat eine isa variable ("is a", "ist ein"), die das Verhalten des Objektes definiert. Ein Änderung der isa Variablen ändert effektiv die Klasse des Objektes, die ihre Struktur definiert und alle Nachrichten, auf die sie antwortet. Isa Swizzling wird u. a. von KVO eingesetzt. Swift ist keine dynamische Sprache wie Objective-C und kann so etwas darum nicht tun. Daher hat Swift stattdessen die oben beschriebenen Property Observer, die schon zur Compile-Zeit feststehen müssen. Dadurch ist KVO auf Objekten von Klassen, deren Quellcode man nicht ändern kann, bei Objective-C sehr leicht, bei Swift muß man sich etwas anderes ausdenken.

Read-only Properties

Für den Objective-C Property-Modifier readonly gibt es bei Swift keinen direkten Ersatz. In Swift würde man das Property als private deklarieren und mit einem gesondertem Computed Property darauf zugreifen, das keinen Setter anbietet. Der Effekt wäre dann in beiden Fällen, daß es keinen direkten Weg gäbe, um von außen Änderungen an dem Property zu machen.

Subscripting

Auch Swift bietet Subscripting für eigene Objekte an. Allerdings nicht nur für Klassen, sondern auch für Structures und Enumerations. Außerdem kann man beliebig viele Subscripte definieren. Und der Key bzw. Index kann aus mehreren Werten bestehen, ebenso die Rückgabe.

Protocols

Protocols sind in Swift ähnlich zu denen in Objective-C, sie sind allerdings selbst komplette Typen. Beide Sprachen können Protocols vererben an andere Protocols.

Extensions

Extensions sind ähnlich zu Categories in Objective-C.

Valid XHTML 1.0!

Besucherzähler


Latest Update: 15. September 2016 at 22:03h (german time)
Link: osx.realmacmark.de/dev/osx_dev_swift_flight.php
Backlinks-Statistik deaktiviert; kann rechts im Menü eingeschaltet werden.