Streng, aber fair

Ein Blick unter die Haube des Typsystems in Swift
Keine Kommentare

Swift ist streng, was Typen angeht. Es ist nämlich eine statisch typisierte Sprache. Die Swift-Entwickler unter den Lesern haben bestimmt bereits die roten kleinen Kreise aus Xcode vor Augen – die Compilerfehler! Warum kommt es dazu, und weshalb hat man ab und an das Gefühl, Swift stehe bei der Entwicklung eher im Wege, anstatt den ohnehin schon steinigen Pfad in Richtung Ziel einfacher zu gestalten? Diese Fragen werden auf den folgenden Seiten durch einen Blick unter die Haube des Typsystems in Swift beantwortet.

Über die Zeit haben sich die eher losen Begriffe von „statischer vs. dynamischer“ bzw. „strenger vs. schwacher“ Typisierung rund um Programmiersprachen gefestigt. Aber was bedeuten sie denn nun genau? Alles dreht sich bei der Klassifizierung um Typen und darum, von welchem Typ Variablen und Konstanten sind. Doch im Grunde ist die Erklärung ganz einfach. Statisch typisierte Sprachen setzen bereits zur Kompilierzeit voraus, dass alle Typen definiert sind. Dynamisch typisierte Sprachen sind lockerer und überprüfen das erst zur Laufzeit. Nun haben beide Ansätze Vor- und Nachteile: gesunde Einschränkung vs. grenzenlose (Fehler-)Möglichkeiten?

Die statische Typisierung bewahrt den Entwickler und seine Anwendung bereits im Vorfeld vor mysteriösen Abstürzen und Fehlverhalten, frei nach dem Motto „fail fast, fail cheap“. Da man aber klar definieren muss, welcher Wert von welchem Typ ist, hat man als Entwickler oftmals das Gefühl der künstlichen Einschränkung. Es ist durchaus mühsam, bereits im Vorfeld für jeden verwendeten Wert den Typ festzulegen und im Programmablauf beispielsweise mit Typecasting sicherzustellen, dass er auch eingehalten wird.

Anders verhält es sich mit dynamischer Typisierung. Hier besteht nicht die Notwendigkeit, alle Typen zu kennen – man weist einfach Werte auf Variablen zu, ohne sich im Detail über die Typen im Klaren sein zu müssen. Das Verfassen von Quellcode ist so etwas freier, da man nicht ständig auf die Finger geklopft bekommt, wenn z. B. ein Float-Wert nicht mit einer String-Variable zusammenpasst. Erst, wenn das Programm ausgeführt wird, müssen die Werte und Typen kompatibel sein, was unter Umständen allerdings zu Fehlern und Abstürzen führen kann.

Swift zählt klar zu der Gruppe der statisch typisierten Sprachen, was Apple selbst „typsicher“ nennt. Der Entwickler soll sich sicher sein können, dass alle verwendeten Werte auch einen definierten Typ aufweisen – zu jeder Zeit. Somit hilft diese Art von Typsicherheit, Fehler im Programm frühestmöglich im Entwicklungsprozess zu finden, nämlich bereits da, wo der Code geschrieben und kompiliert wird. Dies soll wiederum Laufzeitfehlern jede Daseinsberechtigung entziehen.

Typbasierte Fehler, die sich während der Entwicklung bzw. noch beim Schreiben des Quellcodes einschleichen, werden in Xcode visuell anschaulich dargestellt. Das passiert in der Regel auch erstaunlich schnell. Kaum hat man z. B. einer Variable einen unpassenden Wert zugewiesen, wird man sofort durch ein Banner inklusive eines kleinen roten Punkts darauf hingewiesen. Fehler wollen eben schnell erkannt und beseitigt werden!

Hierarchie der Typen

Nun widmen wir uns der Basis, nämlich der Definition von Typen in Swift. Im Vergleich zu anderen, (teils) statisch typisierten Sprachen wie Java oder Objective-C gibt es hier feine, aber wesentliche Unterschiede, die im hierarchischen Aufbau der Typen begründet sind.

Zum Weltenvergleich verwenden wir einfach einen Typ, den wir gut kennen und in allen Sprachen vorfinden: integer. Integer ist ein Typ, dessen Nutzungsname zwischen int, Int und Integer oder NSNumber variiert, aber nahezu immer dasselbe tut: Er repräsentiert ganzzahlige Werte und stellt zudem grundlegenden Operationen zur Manipulation und Vergleichbarkeit eben dieser bereit. In Java wie auch in Objective-C ist die Klasse Integer das Resultat einer Kette von Klassenvererbungen und jeweils einer überschaubaren Anzahl an Protokollimplementierungen. Anders in Swift. Hier erbt Int von niemandem direkt, da er anstatt einer Klasse mittels eines Structs definiert wird. Er implementiert aber eine lange Liste an Protokollen (Abb. 1). Darüber hinaus wird Int auch an vielen Stellen durch Extensions erweitert. Int entsteht somit nicht an einer Stelle bzw. in einem Struct, sondern verstreut über den Swift-Quellcode. Wenn man nun Int als Graph visualisiert, ergibt sich ein fast erschreckend komplexes Bild (Abb. 2). Dieses Chaos ist allerdings Resultat eines sehr schlauen Vorgehens, da die Funktionalitäten an der Stelle definiert werden, wo sie auch zur Verfügung gestellt werden.

Abb. 1: Die Hierarchie von „Integer“ im Weltenvergleich

Abb. 2: Aufbau und Definition von „Int“ in Swift

Zwei Typen sind genug!

Wie wir nun wissen, ist Swift also eine Programmiersprache mit einem ausgeklügelten Typsystem. Wie funktioniert so ein Typ nun tatsächlich und wie ist er aufgebaut? Die überraschende Tatsache ist, dass das gesamte Typsystem auf nur zwei grundsätzlichen Typen aufbaut, dem Named Type und dem Compound Type. Aus diesen beiden werden alle weiteren gebildet und geformt. Klingt abstrakt? Ist es auch. Aber keine Sorge, wir sehen uns die Logik dahinter und den Aufbau der Typen im Detail an. Danach sollten keine Fragen mehr offen sein.

Starten wir mit den Named Types. Wie die Bezeichnung schon verraten mag, hat ein Named Type immer einen Namen. Diese Art von Typ verwenden wir bereits regelmäßig in unserer täglichen Arbeit, ohne uns dessen bewusst zu sein. In diese Kategorie fallen Klassen, Structs, Enums und Protokolle, also alles, was direkt bei der Definition nach einem Namen verlangt. Wir erinnern uns, dass die Basisdatentypen wie Int, String, Array und Dictionary in Swift als Structs definiert sind, was sie somit alle zu Named Types macht.

Wie sieht es nun mit bei den Compound Types aus? Auch hier erlaubt schon die Bezeichnung den ersten Hinweis, dass es sich um etwas Zusammengesetztes handeln muss. Compound Types erlauben uns, mithilfe von Named Types neue Konstrukte wie Tupel und Funktionen zu bauen und somit den Funktionsumfang für die Sprache zu erweitern. Nur eines besitzen sie nicht, nämlich einen Namen (Abb. 3).

Abb. 3: Compound Types bzw. zusammengesetzte Typen

Das Tupel ist das Musterbeispiel eines Compound Types, da es sich dabei um eine Ad-hoc-Gruppierung bzw. -Sammlung von Werten handelt. Dabei fasst man Werte unterschiedlichen Typs – also Named Types wie Int und String – mit runden Klammern zusammen und vergibt je nach Bedarf noch einen Namen pro Parameter. Die Anzahl der Parameter ist variabel, die Trennung erfolgt klassisch mit Kommas. Ein Tupel ist also ein Verbund von heterogenen Werten, der als Kreuzung von „Struct ohne Konstruktor“ und „Array mit variablem Typ“ verstanden werden kann. Das Besondere daran ist allerdings, dass es selbst keinen Namen hat, aber sehr wohl einen Typ, denn dieser setzt sich aus den im Tupel definierten Werten zusammen.

In Abbildung 4 sehen wir eine Variable mit dem Namen upperLeft, der ein Tupel zugewiesen wird. Das Tupel verbindet zwei Werte – offenbar vom Typ Int – und definiert Namen für die beiden Parameter x und y. Nun lautet die Frage: Von welchem Typ ist die Variable upperLeft? Richtig ist: vom Typ (Int, Int). An diesem Beispiel soll klar werden, dass nebst den Typen, die explizite Namen tragen (Int, String etc.), auch Verbünde dieser Typen existieren, die als eigenständige Typen agieren.

Um nun das Verständnis weiter zu schärfen, sehen wir uns Abbildung 4 etwas genauer an und überlegen, ob es valide ist, upperLeft einen neuen Wert wie (x: 12, y: 50) zuzuweisen. Ja, ist es, da hier der Typ (Int, Int) von dem neu zuzuweisenden Tupel erfüllt wird. Der Versuch, upperLeft ein Tupel (x: 10, y: “Manu“) zuzuweisen, wird allerdings scheitern, da wir hier (Int, String) auf (Int, Int) anwenden wollen. Swift ist streng und erwartet links wie rechts einer Anweisung den gleichen Typ, sodass es hier einen Kompilierfehler gibt.

Abb. 4: Funktionsweise von Tupeln als Compound Type

Um besser mit Tupeln umgehen zu können, gibt es die Möglichkeit, mit dem Keyword typealias einen Pseudonamen zu vergeben. So können die Tupel, die eigentlich keine Named Types sind, dennoch im Programmablauf als solche behandelt werden:

var aPoint = (x: 10, y:20)

typealias Positioning = (x:: Int, y: Int)

var lowerRight = Positioning(x: 10, y: 100)
lowerRight = aPoint

Neben dem Tupel bietet Swift als Compound Type noch die Funktion. Auch sie ist ein Verbund von Named Types und nutzt bereits die Tupel für ihre Zwecke. Und nicht zufällig heißt diese Art von Typ wie das Programmierkonstrukt selbst: Funktion. Es handelt sich tatsächlich um die Funktion, die wir nutzen, um Code zu strukturieren und zusammengehörigen Codeteilen Namen und Beschreibungen zu geben. Wenn wir eine Funktion schreiben, definieren wir nicht nur, was darin ablaufen soll, sondern auch, was hineingegeben wird und wieder herauskommt. Die Definition der Funktion, bzw. ihr „Kopf“, ist schlicht ihre Typdefinition.

Werfen wir einen Blick auf Abbildung 5, wo unterschiedliche Funktionen definiert sind. Das Bild sollte uns bekannt sein, aber nun wollen wir mit unserer neuen Typbrille auf die Definitionen blicken. Die Funktion processPerson nimmt eine ID vom Typ Int als Eingabeparameter und gibt nichts – also ein leeres Tupel, auch Void genannt – zurück. Der Typ der Funktion ist also (Int) -> (). Die zweite Funktion cvFromPerson hat ebenfalls einen Eingabeparameter Int, gibt allerdings einen String zurück. Somit ist ihr Typ (Int) -> String. Da Funktionen wie ein Typ behandelt werden können, kann man sie auch Variablen zuweisen. Hier muss aber ebenso wie bei den Tupeln darauf geachtet werden, dass der Typ auf der linken Seite der Anweisung der gleiche ist wie auf der rechten.

Abb. 5: Definition und Typen von Funktionen

Optionals?!

In Swift muss jede Variable oder Konstante generell und zu jeder Zeit einen definierten Wert aufweisen können. Null, Nil oder Ähnliches ist dabei nicht erlaubt. An diese Regel muss sich der Entwickler halten und gewährleisten, dass auch tatsächlich Werte verfügbar sind. Sollte das nicht der Fall sein, resultieren daraus Fehler und Programmabstürze.

Nun hat Swift ein besonderes Konstrukt an Bord, das Entwicklern den Umgang mit Typen ein wenig einfacher und vor allem sicherer gestalten soll: die Optionals. Die Idee an sich ist sehr simpel: Eine Variable kann einen Wert besitzen oder auch nicht. Unter der Haube ist ein Optional ein Enum, das zwei Werte annehmen kann: case none oder case some(x). Es kann also „nichts“ oder „etwas“ aufweisen, wobei das „Etwas“ im Optional gewrappt ist. Das Optional ist somit kein eigenständiger Typ. Man kann es sich vielmehr als Sicherheitscontainer um einen Typ vorstellen.

API Conference 2018

API Management – was braucht man um erfolgreich zu sein?

mit Andre Karalus und Carsten Sensler (ArtOfArc)

Web APIs mit Node.js entwickeln

mit Sebastian Springer (MaibornWolff GmbH)

Um dieses Verhalten zu aktivieren, muss das Optional bereits bei der Variablendeklaration entsprechend markiert werden, nämlich mit einem ? direkt am definierten Typ. Ab sofort verändert sich auch der Umgang mit der Variable bzw. Konstante entsprechend, da sie nicht mehr direkt angesprochen werden kann bzw. soll. Der Wert ist nun verpackt im Optional-Container und kann bzw. muss bei Bedarf ausgepackt werden. Hierzu gibt es zwei Vorgehensweisen: Man kann den Wert explizit herausnehmen (Explicit Unwrapping) oder erst nachfragen, ob sich ein Wert im Container befindet und mit diesem weiterarbeiten (Conditional Unwrapping) (Abb. 6).

Die erste Variante ist schnell und unkompliziert, aber auch unsicher und kann zu sofortigen Abstürzen führen, wie Abbildung 7 zeigt. Ohne Umschweife kann man einem Optional zu jeder Zeit durch Beigabe eines ! direkt nach dem Variablennamen den Wert entreißen und damit arbeiten. Befindet sich aber kein Wert im Container, nutzt man einen illegalen Zustand in Swift, nämlich den eines leeren Werts, was zu einem sofortigen Crash führt, wenn der Fehler nicht entsprechend aufgefangen wird. Sieht man im Programmablauf wild ! an Variablennamen angehängt, entspricht dies einem „Code Smell“, den man umgehend beheben sollte. Explicit Unwrapping ist also nur in Ausnahmefällen anzuwenden.

Die letztere Variante ist sauber, sicher und entspricht dem empfohlenen Vorgehen. Hier fragt man mit einer if-let-Anweisung das Optional, ob ein Wert im Container zur Verfügung steht. Im if-Block ist der Wert dann bereits ausgepackt und kann ruhigen Gewissens verwendet werden. Auf diese Weise wird man nie einen leeren Wert anfragen und vermeidet Programmabstürze, die dadurch ausgelöst werden können. Conditional Unwrapping ist der empfohlene Weg, sicher und mit nachhaltiger Freude mit Optionals zu arbeiten.

Abb. 6: Unwrapping von Optionals – Explicit und Conditional

 

Abb. 7: Crash bei Zugriff auf leeres Optional nach Explicit Unwrapping

Swift kennt sie alle

Wir kennen nun die Basis des Typsystems von Swift. Einen wesentlichen Bestandteil haben wir allerdings bislang außer Acht gelassen und als gegeben angenommen. In vielen Beispielen haben wir bei der Variablendeklaration nur einen Wert, aber keinen expliziten Typ definiert und Swift vertraut, dass es selbst korrekt herausfinden wird, welcher Typ am besten zu „10“, 10 oder 13.33 passt. Das hat soweit auch ganz gut funktioniert. Der Grund dafür ist eine Fähigkeit der Sprache, die „Typinferenz“ genannt wird. Swift hat also eine trainierte Spürnase und findet heraus, welcher Typ am besten zu bestimmten Werten passt. Der Compiler hat die Fähigkeit, automatisch den Datentyp aus Werten abzuleiten. Diese Eigenschaft ist alles andere als eine neue Erfindung, sondern vielmehr eine Besonderheit, die viele streng typisierte bzw. typsichere Programmiersprachen gemein haben, z. B. C++, C#, Scala, Go, F#, Kotlin und spätere Versionen von Visual Basic.

Der größte Vorteil von Typinferenz ist das bequeme Definieren von Variablen und Konstanten und der Umgang mit ihnen im weiteren Programmablauf. Man spart sich Tipparbeit und muss nicht jeder Variable, auf die man Text zuweist, explizit sagen, dass sie vom Typ String ist. Und je weniger man den Quellcode dekorieren muss, umso einfacher und klarer bietet er sich später zum Lesen an.

Den einzigen Nachteil stellt die erschwerte Fehlersuche dar – und diese kann gigantische Ausmaße annehmen. Klar nimmt man im Vorfeld den Vorteil gerne an, weniger Code schreiben zu müssen und vermeintlich redundante Informationen einfach weglassen zu können. Sobald man aber im Programmverlauf wissen möchte, welcher Typ von Funktion A zurückgegeben wird, um ihn wiederum korrekt an Funktion B zu übergeben, bekommt man heftige Schwierigkeiten. Man muss oftmals tief graben, um diese Informationen ausfindig zu machen. Abbildung 8 soll als kleine Gegenüberstellung dienen. Die Variablen theThings und theOtherThings bekommen jeweils als Ergebnis den Wert der Funktion doSomeThings geliefert. Oftmals ist eine Funktion auch nicht dort definiert und implementiert, wo sie danach genutzt wird. Somit ist für theThings nicht klar, von welchem Typ ihr Wert ist. Anders bei theOtherThings, da hier direkt definiert wird, dass jeder zugewiesene Wert vom Tupeltyp (Character, Character) sein muss. Würde die Funktion doSomeThings nun einen anderen Typ liefern, meldet Swift sofort einen Fehler.

Es ist also durchaus sinnvoll, Typen direkt an Variablen zu definieren. Vor allem ist es sinnvoll, sobald es sich um komplexere oder eigens definierte Typen handelt. Die Typinferenz erkennt für Swift, ob Typen kompatibel sind. Aber für den Entwickler muss diese Information ebenso zugänglich sein, damit er den Quellcode sowohl lesen und verstehen als auch weiter anwenden kann.

Abb. 8: Typinferenz vs. explizite Typdefinition

Fazit

Swift ist eine moderne und typsichere Programmiersprache. Ein einfach aufgebautes Typsystem kann uns helfen, logisch gut verständlichen Code zu schreiben. Eine strenge Typisierung vermeidet bereits zur Kompilierzeit viele Fehler, die einen sonst später zur Laufzeit teurer zu stehen kommen würden. Konstrukte wie Optionals bieten zusätzlichen Schutz – teils vor uns selbst –, um Fehler wie Zugriffe auf leere bzw. nicht existente Werte sauber abzufangen. Und man behalte im Hinterkopf: Am Ende sind es nur zwei Typen, die die Basis für all das bieten.

Entwickler Magazin

Entwickler Magazin abonnierenDieser Artikel ist im Entwickler Magazin erschienen.

Natürlich können Sie das Entwickler Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -