Zeichenfolgenliterale in Swift
Die Möglichkeit, grundlegende Werte wie Zeichenfolgen und Ganzzahlen mithilfe von Inline-Literalen auszudrücken, ist in den meisten Programmiersprachen ein wesentliches Merkmal. Während jedoch viele andere Sprachen die Unterstützung für bestimmte Literale in ihren Compiler integriert haben, verfolgt Swift einen viel dynamischeren Ansatz — indem es sein eigenes Typsystem verwendet, um zu definieren, wie verschiedene Literale über Protokolle gehandhabt werden sollen.
Diese Woche konzentrieren wir uns insbesondere auf String—Literale, indem wir einen Blick auf die vielen verschiedenen Möglichkeiten werfen, wie sie verwendet werden können und wie wir – durch Swifts stark protokollorientiertes Design — die Art und Weise anpassen können Literale werden interpretiert, was uns einige wirklich interessante Dinge tun lässt.
Essential Developer: Nehmen Sie an einem kostenlosen Online-Crashkurs für iOS-Entwickler teil, die Black-Belt-Senior-Entwickler werden möchten – das ist: erreichen Sie ein Expertenniveau an praktischen Fähigkeiten und werden Sie Teil der bestbezahlten Entwickler der Welt.
Die Grundlagen
Wie in vielen anderen Sprachen werden Swift-Zeichenfolgen durch Literale ausgedrückt, die von Anführungszeichen umgeben sind — und können sowohl spezielle Sequenzen (z. B. Zeilenumbrüche) als auch Escapezeichen und interpolierte Werte enthalten:
let string = "\(user.name) says \"Hi!\"\nWould you like to reply?"// John says "Hi!"// Would you like to reply?
Während die oben verwendeten Funktionen uns bereits viel Flexibilität bieten und höchstwahrscheinlich für die überwiegende Mehrheit der Anwendungsfälle ausreichen, gibt es Situationen, in denen leistungsfähigere Ausdrucksmöglichkeiten für Literale nützlich sein können. Schauen wir uns einige davon an, beginnend damit, wann wir eine Zeichenfolge definieren müssen, die mehrere Textzeilen enthält.
Mehrzeilige Literale
Obwohl jedes Standard—String-Literal mit \n
in mehrere Zeilen aufgeteilt werden kann, ist dies nicht immer praktisch – insbesondere wenn wir einen größeren Text als Inline-Literal definieren möchten.
Zum Glück können wir seit Swift 4 auch mehrzeilige String-Literale mit drei Anführungszeichen anstelle von nur einem definieren. Hier verwenden wir beispielsweise diese Funktion, um einen Hilfetext für ein Swift-Skript auszugeben, falls der Benutzer beim Aufrufen in der Befehlszeile keine Argumente übergeben hat:
// We're comparing against 1 here, since the first argument passed// to any command line tool is the current path of execution.guard CommandLine.arguments.count > 1 else { print(""" To use this script, pass the following: - A string to process - The maximum length of the returned string """) // Exit the program with a non-zero code to indicate failure. exit(1)}
Oben nutzen wir die Tatsache, dass mehrzeilige Zeichenfolgenliterale die Einrückung ihres Textes relativ zu den abschließenden Anführungszeichen unten beibehalten. Sie ermöglichen es uns auch, viel freiere Anführungszeichen in ihnen zu verwenden, da sie durch einen Satz von drei Anführungszeichen definiert sind, wodurch die Grenzen des Literals viel weniger wahrscheinlich mehrdeutig werden.
Beide oben genannten Eigenschaften machen mehrzeilige Literale zu einem großartigen Werkzeug zum Definieren von Inline-HTML — zum Beispiel in einer Form von Webseitenerzeugungstool oder beim Rendern von Teilen des Inhalts einer App mit Webansichten – wie folgt:
extension Article { var html: String { // If we want to break a multiline literal into separate // lines without causing an *actual* line break, then we // can add a trailing '\' to one of our lines. let twitterLink = """ <a href="https://twitter.com/\(author.twitterHandle)">\ @\(author.twitterHandle)</a> """ return """ <article> <h1>\(title)</h1> <div class="author"> <p>\(author.name)</p> \(twitterLink) </div> <div class="body">\(body)</div> </article> """ }}
Die obige Technik kann auch sehr nützlich sein, wenn String-basierte Testdaten definiert werden. Angenommen, die Einstellungen unserer App müssen als XML exportierbar sein und wir möchten einen Test schreiben, der diese Funktionalität überprüft. Anstatt die XML—Datei, gegen die wir überprüfen möchten, in einer separaten Datei definieren zu müssen, können wir ein mehrzeiliges Zeichenfolgenliteral verwenden, um es in unseren Test einzubinden:
class SettingsTests: XCTestCase { func testXMLConversion() { let settings = Settings( messageLimit: 7, enableSync: true, signature: "Sent from my Swift app" ) XCTAssertEqual(settings.xml, """ <?xml version="1.0" encoding="UTF-8"?> <settings> <messagelimit>7</messagelimit> <enablesync>1</enablesync> <signature>Sent from my Swift app</signature> </settings> """) }}
Der Vorteil der Inline—Definition von Testdaten, wie wir es oben tun, besteht darin, dass es viel einfacher wird, Fehler beim Schreiben des Tests schnell zu erkennen – da der Testcode und die erwartete Ausgabe direkt nebeneinander platziert werden. Wenn einige Testdaten jedoch eine Handvoll Zeilen lang sind oder dieselben Daten an mehreren Stellen verwendet werden müssen, kann es sich dennoch lohnen, sie in eine eigene Datei zu verschieben.
Rohzeichenfolgen
Neu in Swift 5 können wir mit Rohzeichenfolgen alle dynamischen Zeichenfolgenliteralfunktionen (z. B. Interpolation und Interpretation von Sonderzeichen wie \n
) deaktivieren, anstatt ein Literal einfach als rohe Zeichenfolge zu behandeln. Rohe Zeichenfolgen werden definiert, indem ein Zeichenfolgenliteral mit Rautenzeichen (oder „Hashtags“, wie die Kinder sie nennen) umgeben wird:
let rawString = #"Press "Continue" to close this dialog."#
Genau wie wir oben ein mehrzeiliges Literal verwendet haben, um Testdaten zu definieren, sind rohe String—Literale besonders nützlich, wenn wir Strings inline einfügen möchten, die Sonderzeichen enthalten müssen – wie Anführungszeichen oder Backslashes. Hier ist ein weiteres testbezogenes Beispiel, in dem wir ein rohes Zeichenfolgenliteral verwenden, um eine JSON-Zeichenfolge zum Codieren einer User
-Instanz zu definieren von:
class UserTests: XCTestCase { func testDecoding() throws { let json = #"{"id": 37, "name": "John"}"# let data = Data(json.utf8) let user = try data.decoded() as User XCTAssertEqual(user.id, 37) XCTAssertEqual(user.name, "John") }}
Oben verwenden wir die typinferenzbasierte Dekodierungs-API von „Typinferenzbasierte Serialisierung in Swift“.
Während Rohzeichenfolgen standardmäßig Funktionen wie die Interpolation von Zeichenfolgen deaktivieren, gibt es eine Möglichkeit, dies zu überschreiben, indem Sie direkt nach dem führenden Backslash der Interpolation ein weiteres Plumpszeichen hinzufügen — wie folgt:
extension URL { func html(withTitle title: String) -> String { return #"<a href="\#(absoluteString)">\#(title)</a>"# }}
Schließlich sind Rohzeichenfolgen auch besonders nützlich, wenn Sie eine Zeichenfolge mit einer bestimmten Syntax interpretieren, insbesondere wenn diese Syntax stark von Zeichen abhängt, die normalerweise in einem Zeichenfolgenliteral maskiert werden müssten — z. B. reguläre Ausdrücke. Durch die Definition regulärer Ausdrücke mit rohen Zeichenfolgen ist kein Escaping erforderlich, sodass Ausdrücke so lesbar wie möglich sind:
// This expression matches all words that begin with either an// uppercase letter within the A-Z range, or with a number.let regex = try NSRegularExpression( pattern: #"(()|(\d))\w+"#)
Selbst mit den oben genannten Verbesserungen ist es fraglich, wie einfach reguläre Ausdrücke zu lesen (und zu debuggen) sind — insbesondere wenn sie im Kontext einer hochgradig typsicheren Sprache wie Swift verwendet werden. Es wird höchstwahrscheinlich auf die bisherigen Erfahrungen eines bestimmten Entwicklers mit regulären Ausdrücken ankommen, unabhängig davon, ob er sie der Implementierung benutzerdefinierterer String-Parsing-Algorithmen direkt in Swift vorzieht oder nicht.
Werte mit String-Literalen ausdrücken
Während alle String-Literale standardmäßig in String
Werte umgewandelt werden, können wir sie auch verwenden, um benutzerdefinierte Werte auszudrücken. Wie wir uns in „Typsichere Bezeichner in Swift“ angesehen haben, können wir durch Hinzufügen von String-Literal-Unterstützung zu einem unserer eigenen Typen eine erhöhte Typsicherheit erreichen, ohne den Komfort der Verwendung von Literalen zu beeinträchtigen.
Nehmen wir zum Beispiel an, wir haben ein Searchable
-Protokoll definiert, das als API für die Suche nach Datenbanken oder zugrunde liegenden Speichern dient, die unsere App verwendet – und dass wir eine Query
-Aufzählung verwenden, um verschiedene Möglichkeiten zur Durchführung einer solchen Suche zu modellieren:
protocol Searchable { associatedtype Element func search(for query: Query) -> }enum Query { case matching(String) case notMatching(String) case matchingAny()}
Der obige Ansatz gibt uns viel Kraft und Flexibilität, wie wir jede Suche durchführen werden, aber der häufigste Anwendungsfall ist wahrscheinlich immer noch der einfachste — die Suche nach Elementen, die zu einer bestimmten Zeichenfolge passen — und es wäre wirklich schön, wenn wir das mit einem Zeichenfolgenliteral tun könnten.
Die gute Nachricht ist, dass wir dies erreichen können, während die obige API vollständig intakt bleibt, indem wir Query
ExpressibleByStringLiteral
:
extension Query: ExpressibleByStringLiteral { init(stringLiteral value: String) { self = .matching(value) }}
Auf diese Weise können wir jetzt übereinstimmende Suchen durchführen, ohne einen Query
—Wert manuell erstellen zu müssen – alles, was wir tun müssen, ist ein String-Literal zu übergeben, als ob die API, die wir aufrufen, tatsächlich einen String
direkt akzeptiert hätte. Hier verwenden wir diese Funktion, um einen Test zu implementieren, der überprüft, ob ein UserStorage
-Typ seine Suchfunktion korrekt implementiert:
class UserStorageTests: XCTestCase { func testSearch() { let storage = UserStorage.inMemory let user = User(id: 3, name: "Amanda") storage.insert(user) let matches = storage.search(for: "anda") XCTAssertEqual(matches, ) }}
Benutzerdefinierte Zeichenfolgenliteralausdrücke können in vielen Situationen vermeiden, dass wir bei der Arbeit mit zeichenfolgenbasierten Typen wie Abfragen und Bezeichnern zwischen Typsicherheit und Komfort wählen müssen. Es kann ein großartiges Werkzeug sein, um ein API-Design zu erreichen, das vom einfachsten Anwendungsfall bis hin zur Abdeckung von Randfällen gut skalierbar ist und bei Bedarf mehr Leistung und Anpassbarkeit bietet.
Benutzerdefinierte Interpolation
Eine Sache, die alle „Varianten“ von Swift-String-Literalen gemeinsam haben, ist ihre Unterstützung für die Interpolation von Werten. Während wir immer anpassen konnten, wie ein bestimmter Typ interpoliert wird, indem wir CustomStringConvertible
— Swift 5 führt neue Möglichkeiten zur Implementierung benutzerdefinierter APIs direkt über der String-Interpolations-Engine ein.
Nehmen wir als Beispiel an, dass wir eine bestimmte Zeichenfolge speichern möchten, indem wir optional ein Präfix und ein Suffix darauf anwenden. Im Idealfall möchten wir diese Werte einfach interpolieren, um die endgültige Zeichenfolge wie folgt zu bilden:
func save(_ text: String, prefix: String?, suffix: String?) { let text = "\(prefix)\(text)\(suffix)" textStorage.store(text)}
Da beide prefix
und suffix
Optionen sind, wird die einfache Verwendung ihrer Beschreibung nicht das gewünschte Ergebnis liefern — und der Compiler gibt uns sogar eine Warnung:
String interpolation produces a debug description for an optional value
Während wir immer die Möglichkeit haben, jede dieser beiden Optionen vor der Interpolation zu entpacken, werfen wir einen Blick darauf, wie wir beide Dinge auf einmal mit benutzerdefinierter Interpolation ausführen können. Wir beginnen mit der Erweiterung von String.StringInterpolation
mit einer neuen appendInterpolation
Überladung, die einen beliebigen optionalen Wert akzeptiert:
extension String.StringInterpolation { mutating func appendInterpolation<T>(unwrapping optional: T?) { let string = optional.map { "\($0)" } ?? "" appendLiteral(string) }}
Die obige unwrapping:
Parameterbezeichnung ist wichtig, da wir damit den Compiler anweisen, diese spezifische Interpolation zu verwenden methode — so:
func save(_ text: String, prefix: String?, suffix: String?) { let text = "\(unwrapping: prefix)\(text)\(unwrapping: suffix)" textStorage.store(text)}
Obwohl es nur syntaktischer Zucker ist, sieht das obige wirklich ordentlich aus! Das kratzt jedoch kaum an der Oberfläche dessen, was benutzerdefinierte String-Interpolationsmethoden können. Sie können sowohl generisch als auch nicht generisch sein, eine beliebige Anzahl von Argumenten akzeptieren, Standardwerte verwenden und so ziemlich alles andere, was „normale“ Methoden können.
Hier ist ein weiteres Beispiel, in dem wir unsere Methode zum Konvertieren einer URL in einen HTML-Link von zuvor aktivieren, um sie auch im Kontext der String-Interpolation zu verwenden:
extension String.StringInterpolation { mutating func appendInterpolation(linkTo url: URL, _ title: String) { let string = url.html(withTitle: title) appendLiteral(string) }}
Mit dem oben genannten können wir jetzt problemlos HTML-Links aus einer URL wie dieser generieren:
webView.loadHTMLString( "If you're not redirected, \(linkTo: url, "tap here").", baseURL: nil)
Das Coole an der benutzerdefinierten String—Interpolation ist, wie der Compiler jede unserer appendInterpolation
-Methoden in entsprechende Interpolations-APIs übersetzt – was uns die vollständige Kontrolle darüber gibt, wie die Aufrufseite aussehen wird, zum Beispiel durch Entfernen externer Parameterbeschriftungen, wie wir es oben für title
getan haben.
In den kommenden Artikeln werden wir weitere Möglichkeiten zur Verwendung der benutzerdefinierten Zeichenfolgeninterpolation untersuchen, z. B. mit attributierten Zeichenfolgen und anderen Arten von Textmetadaten.
Unterstützen Sie Swift von Sundell, indem Sie sich diesen Sponsor ansehen:
Essential Developer: Nehmen Sie an einem kostenlosen Online-Crashkurs für iOS—Entwickler teil, die Black-Belt-Senior-Entwickler werden möchten – das heißt: Erreichen Sie ein Expertenniveau an praktischen Fähigkeiten und werden Sie Teil der bestbezahlten Entwickler der Welt.
Fazit
Während einige der fortgeschritteneren String—Literal-Funktionen von Swift nur in sehr spezifischen Situationen, wie denen in diesem Artikel, wirklich nützlich sind, ist es schön, sie bei Bedarf zur Verfügung zu haben – zumal es möglich ist, sie vollständig zu vermeiden und nur Strings "the old-fashioned way"
.
String-Literale sind ein weiterer Bereich, in dem Swifts protokollorientiertes Design wirklich glänzt. Indem wir einen Großteil der Interpretation und Handhabung von Literalen an Implementierer von Protokollen delegieren, anstatt diese Verhaltensweisen im Compiler selbst fest zu codieren, können wir als Entwickler von Drittanbietern die Art und Weise, wie Literale behandelt werden, stark anpassen – während wir die Standardeinstellungen so einfach wie möglich halten.