Swift 2: Zeichenketten

Kurz bevor mein Buch zu Swift 1.2 in Druck ging, stellte Apple Swift 2.0 vor. Jetzt bin ich also dabei, das ganze Manuskript nochmals zu überarbeiten, damit es im September als echtes Swift-2.0-Buch erscheinen kann. Im Zuge dieser Überarbeitung werden ich gelegentlich die wichtigsten Neuerungen in Blog-Beiträgen präsentieren. Heute beginne ich mit Zeichenketten.

Swift-Zeichenketten bestehen aus Extended Grapheme Cluster

In den meisten Programmiersprachen sind Zeichenketten eine recht simple Sache: Mehrere Bytes im Speicher werden als Text interpretiert, wobei die einzig interessante Frage zumeist darin besteht, welcher Zeichensatz dabei intern zur Anwendung kommt (z.B. UTF-8 oder UCS-2).

Swift verwendete bereits in Version 1 einen ganz anderen Ansatz — und daran hat sich auch in Version 2 nichts geändert:

  • Ein Character speichert ein einzelnes Unicode-Zeichen. Genau genommen kann es sich dabei um ein Extended Grapheme Cluster handeln, also um ein aus mehreren Unicode-Skalaren zusammengesetztes Symbol.

  • Ein String entsteht aus einer Aneinanderreihung von Character-Objekten.

Jedes Character-Objekt entspricht einem Unicode-Zeichen. Gemäß der Unicode-Spezifikation kann ein Zeichen aber aus mehreren Unicode-Skalaren zusammengesetzt sein! Beispielsweise kann das Zeichen »ä« (Umlaut-A) sowohl durch den Unicode-Skalar mit dem hexadezimalen Code 0xe4 dargestellt werden als auch durch die Kombination aus dem Buchstaben »a« (Code 0x61) und dem Zeichen »Combining Diaeresis« (Code 0x308). In diesem Fall spricht man von einem Extended Grapheme Cluster — das Umlaut-A ist also aus mehreren Einzelzeichen zusammengesetzt worden. Derartige Zeichenkombinationen kommen bei asiatischen Sprachen häufig vor.

Eine Besonderheit von Swift besteht nun darin, dass es Zeichen(ketten) unabhängig vom internen Aufbau dann als gleichwertig betrachtet, wenn ihre Unicode-Bedeutung gleichwertig ist. Das folgende Beispiel illustriert dies:

let single = "ä"           // ä als Einzelzeichen
let cluster = "a\u{308}"   // ä als Zeichenkombination aus a und "
cluster == single          // true!

Unterschiede zwischen Swift 1.n und Swift 2

In Swift 1.n war ein String eine Aufzählung (Collection) von Character-Objekten. Oder, um es exakter zu formulieren: Der String-Datentyp implementierte das CollectionType-Protokoll. Es gab diverse globale Funktionen, die mit Collections umgehen konnten und die daher auf Zeichenketten angewendet werden konnten:

// Swift 1.n
let s = "abcäöü߀" + "a\u{308}"
count(s)                           //  9 Zeichen
count(s.unicodeScalars)            // 10 Unicode-Skalare
count(s.utf16)                     // 10 UTF16-Zeichen
count(s.utf8)                      // 17 Byte in UTF8-Codierung

let abc = "abc efg hij"
String(reverse(abc))               // "jih gfe cba"
let words = split(abc) {$0 == " "} // ["abc", "efg", "hij"]
let abc2 = " ".join(words)         // wieder "abc efg hij"

Eine wesentliche Neuerung in Swift 2.0 besteht nun darin, dass der String-Typ das CollectionType-Protokoll nicht mehr implementiert! Dafür gibt es die neue String-Eigenschaft characters. Sie liefert ein String.CharacterView-Objekt zurück, das wiederum das CollectionType-Protokoll implementiert.

Gleichzeitig hat Apple diverse globale Funktionen wie count durch entsprechende Methoden oder Eigenschaften ersetzt, die nun direkt auf das betreffende Objekt angewendet werden können. Im Sinne der objektorientierten Programmierung liefert das klareren Code. Die technische Voraussetzung dafür war wiederum, dass Swift nun Protocol Extensions unterstützt, über die ich — wenn es die Zeit zulässt –, demnächst ebenfalls hier bloggen werden.

Damit ergibt sich der folgende neue Code für die obigen Beispiele:

// Swift 2
let s = "abcäöü߀" + "a\u{308}"
s.characters.count                   //  9 Zeichen
s.unicodeScalars.count               // 10 Unicode-Skalare
s.utf16.count                        // 10 UTF16-Zeichen
s.utf8.count                         // 17 Byte in UTF8-Codierung

let abc="abc efg hij"
let words=split(abc.characters) {$0 == " "}.map { String($0) }
let abc2 = " ".join(words)         // wieder "abc efg hij"
String(Array(abc.characters.reverse()))

Noch eine Anmerkung zu split: Diese globale Funktion liefert ein Array von SubSlice-Elementen, also von Teilen einer Collection von einzelnen Zeichenketten. map wendet String auf jedes dieser SubSlice-Elemente an, um wieder reguläre Zeichenketten zu bilden.

Man kann vielleicht argumentieren, dass der Swift-2-Code im Sinne der objektorientierten Programmierung schlüssiger geworden ist. Er ist aber weder kürzer noch besser lesbar.