String literals in Swift
Het kunnen uitdrukken van basiswaarden, zoals strings en integers, met behulp van inline literals is een essentieel kenmerk in de meeste programmeertalen. Echter, terwijl veel andere talen de ondersteuning voor specifieke literals in hun compiler hebben ingebouwd, volgt Swift een veel dynamischer aanpak-met behulp van zijn eigen type systeem om te bepalen hoe verschillende literals moeten worden behandeld, door middel van protocollen.
deze week, laten we ons concentreren op string literals in het bijzonder, door een kijkje te nemen op de vele verschillende manieren waarop ze kunnen worden gebruikt en hoe we — door Swift ‘ s zeer protocol-georiënteerde ontwerp — in staat zijn om de manier waarop literals worden geïnterpreteerd aan te passen, waardoor we een aantal echt interessante dingen kunnen doen.
Essential Developer: doe mee aan een gratis online spoedcursus voor iOS-ontwikkelaars die senior ontwikkelaars willen worden — dat wil zeggen: behaal een deskundig niveau van praktische vaardigheden en maak deel uit van de best betaalde ontwikkelaars ter wereld.
de basis
net als in veel andere talen worden Swift strings uitgedrukt door letterwoorden omgeven door aanhalingstekens — en kunnen ze zowel speciale reeksen (zoals newlines), escaped characters en geïnterpoleerde waarden bevatten:
let string = "\(user.name) says \"Hi!\"\nWould you like to reply?"// John says "Hi!"// Would you like to reply?
hoewel de functies die hierboven worden gebruikt ons al veel flexibiliteit bieden, en waarschijnlijk genoeg zijn voor de overgrote meerderheid van de use cases, zijn er situaties waarin krachtigere manieren om letterwoorden uit te drukken van pas kunnen komen. Laten we eens kijken naar een aantal van die, te beginnen met wanneer we nodig hebben om een string met meerdere regels tekst te definiëren.
Multiline literals
hoewel elke standaardtekst letterlijk kan worden opgesplitst in meerdere regels met \n
, is dat niet altijd praktisch — vooral als we een groter stuk tekst willen definiëren als een inline Letter.
gelukkig, sinds Swift 4, zijn we ook in staat om multiline string literals te definiëren met behulp van drie aanhalingstekens in plaats van slechts één. Hier gebruiken we bijvoorbeeld die mogelijkheid om een hulptekst voor een Swift-script uit te voeren, voor het geval de gebruiker geen argumenten heeft doorgegeven bij het aanroepen van het op de opdrachtregel:
// 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)}
hierboven maken we gebruik van het feit dat multiline string literals de inspringing van hun tekst behouden, ten opzichte van de afsluitende set aanhalingstekens, onderaan. Ze stellen ons ook in staat om veel vrijer niet-afgeschermde aanhalingstekens binnen hen te gebruiken, omdat ze worden gedefinieerd door een verzameling van drie aanhalingstekens, waardoor de grenzen van het letterlijke veel minder waarschijnlijk ambigu worden.
beide bovenstaande kenmerken maken multiline literals een geweldig hulpmiddel voor het definiëren van inline HTML-bijvoorbeeld in een vorm van webpagina generatie tool, of bij het renderen van delen van de inhoud van een app met behulp van web views — als volgt:
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> """ }}
de bovenstaande techniek kan ook erg handig zijn bij het definiëren van string-gebaseerde testgegevens. Bijvoorbeeld, laten we zeggen dat de instellingen van onze app moeten worden exporteerbaar als XML, en dat we willen een test die deze functionaliteit verifieert schrijven. In plaats van de XML te moeten definiëren waarmee we willen verifiëren in een apart bestand — We kunnen een letterlijke tekenreeks gebruiken om het in onze test te inline:
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> """) }}
het voordeel van het definiëren van testgegevens inline, zoals we hierboven doen, is dat het veel gemakkelijker wordt om snel fouten te herkennen bij het schrijven van de test — omdat de testcode en de verwachte uitvoer vlak naast elkaar worden geplaatst. Als sommige testgegevens echter meer dan een handvol regels lang zijn, of als dezelfde gegevens op meerdere plaatsen moeten worden gebruikt, kan het nog steeds de moeite waard zijn om deze naar zijn eigen bestand te verplaatsen.
Raw strings
nieuw in Swift 5, raw strings stellen ons in staat om alle dynamische string letterlijke functies uit te schakelen (zoals interpolatie, en het interpreteren van speciale tekens, zoals \n
), ten gunste van het simpelweg behandelen van een letterlijke als een ruwe reeks tekens. Raw strings worden gedefinieerd door het omringen van een string letterlijke met pound tekens (of “hashtags”, zoals de kinderen ze noemen):
let rawString = #"Press "Continue" to close this dialog."#
net zoals we hierboven een multiline letterlijke tekst gebruikten om testgegevens te definiëren, zijn ruwe tekenreeksen bijzonder handig als we tekenreeksen willen inline die speciale tekens moeten bevatten — zoals aanhalingstekens of backslashes. Hier is een ander test-gerelateerd voorbeeld, waarin we een ruwe string letterlijk gebruiken om een JSON string te definiëren om een User
instantie te coderen van:
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") }}
hierboven gebruiken we de Type inference-based decoding API van “type inference-powered serialization in Swift”.
terwijl raw strings standaard functies zoals string — interpolatie uitschakelen, is er een manier om dat te overschrijven door een ander pound — teken toe te voegen direct na de leidende backslash van de interpolatie-zoals dit:
extension URL { func html(withTitle title: String) -> String { return #"<a href="\#(absoluteString)">\#(title)</a>"# }}
tenslotte zijn raw strings ook bijzonder nuttig bij het interpreteren van een string met behulp van een specifieke syntaxis, vooral als die syntaxis sterk afhankelijk is van tekens die normaal gesproken binnen een string-letterlijke moeten worden weggelaten-zoals reguliere expressies. Door reguliere expressies te definiëren met behulp van ruwe strings, is ontsnappen niet nodig, wat ons uitdrukkingen geeft die zo leesbaar zijn als ze krijgen:
// 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+"#)
zelfs met de bovenstaande verbeteringen, is het twijfelachtig hoe makkelijk te lezen (en debuggen) reguliere expressies zijn — vooral wanneer gebruikt in de context van een zeer typeveilige taal zoals Swift. Het komt waarschijnlijk neer op eerdere ervaring van een bepaalde ontwikkelaar met reguliere expressies, of ze ze liever hebben dan het implementeren van meer aangepaste string parsing algoritmen, direct in Swift.
waarden uitdrukken met behulp van string literals
terwijl alle string literals worden omgezet in String
waarden standaard, kunnen we ze ook gebruiken om aangepaste waarden uit te drukken. Zoals we hebben gekeken in “Type-safe identifiers in Swift”, het toevoegen van string letterlijke ondersteuning aan een van onze eigen types kan ons laten bereiken verhoogde type veiligheid, zonder in te boeten het gemak van het gebruik van literals.
bijvoorbeeld, laten we zeggen dat we een Searchable
protocol hebben gedefinieerd om op te treden als de API voor het doorzoeken van elke database of onderliggende opslag die onze app gebruikt — en dat we een Query
enum gebruiken om verschillende manieren te modelleren om zo ‘ n zoekopdracht uit te voeren:
protocol Searchable { associatedtype Element func search(for query: Query) -> }enum Query { case matching(String) case notMatching(String) case matchingAny()}
de bovenstaande benadering geeft ons veel kracht en flexibiliteit over hoe we elke zoekopdracht zullen uitvoeren, maar de meest voorkomende use case is waarschijnlijk nog steeds de eenvoudigste — zoeken naar elementen die overeenkomen met een gegeven string — en het zou echt leuk zijn als we in staat zouden zijn om dat te doen met behulp van een string letterlijke.
het goede nieuws is dat we dat kunnen laten gebeuren, terwijl we de bovenstaande API volledig intact houden, door Query
conform te maken met ExpressibleByStringLiteral
:
extension Query: ExpressibleByStringLiteral { init(stringLiteral value: String) { self = .matching(value) }}
Op die manier zijn we nu vrij om matching zoekopdrachten uit te voeren zonder een Query
waarde handmatig aan te maken — alles wat we moeten doen is een string letterlijke doorgeven alsof de API die we aanroepen daadwerkelijk een String
direct accepteert. Hier gebruiken we die mogelijkheid om een test te implementeren die controleert of een UserStorage
type de zoekfunctionaliteit correct implementeert:
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, ) }}
Custom string letterlijke expressies kunnen in veel situaties laten voorkomen dat we moeten kiezen tussen type veiligheid en gemak bij het werken met tekenreeksgebaseerde types, zoals queries en identifiers. Het kan een geweldig hulpmiddel zijn om te gebruiken om een API-ontwerp te bereiken dat goed schaalt van de eenvoudigste use case, helemaal tot het bedekken van randgevallen en het aanbieden van meer kracht en aanpasbaarheid wanneer dat nodig is.
aangepaste interpolatie
een ding dat alle “smaken” van Swift string literals gemeen hebben is hun ondersteuning voor interpolatie waarden. Hoewel we altijd in staat zijn geweest om aan te passen hoe een bepaald type wordt geïnterpoleerd door te voldoen aan CustomStringConvertible
— introduceert Swift 5 nieuwe manieren om aangepaste API ‘ s te implementeren bovenop de string interpolatie-engine.
als voorbeeld, laten we zeggen dat we een gegeven tekenreeks willen opslaan door er optioneel een voorvoegsel en achtervoegsel op toe te passen. Idealiter zouden we die waarden gewoon willen interpoleren om de laatste string te vormen, zoals dit:
func save(_ text: String, prefix: String?, suffix: String?) { let text = "\(prefix)\(text)\(suffix)" textStorage.store(text)}
Echter, aangezien beide prefix
en suffix
zijn optioneel, gewoon met behulp van hun beschrijving zal niet tot het resultaat waar we naar op zoek zijn, en de compiler gaven ons zelfs een waarschuwing:
String interpolation produces a debug description for an optional value
Hoewel we altijd nog de optie van het uitpakken van elk van deze twee opties voor te interpoleren hen, laten we eens kijken hoe we het kunnen doen van die dingen in een keer met behulp van aangepaste interpolatie. We beginnen door de uitbreiding String.StringInterpolation
met een nieuw appendInterpolation
overbelasting die aanvaardt geen enkele optionele waarde:
extension String.StringInterpolation { mutating func appendInterpolation<T>(unwrapping optional: T?) { let string = optional.map { "\($0)" } ?? "" appendLiteral(string) }}
boven unwrapping:
parameter label is belangrijk, want het is wat we zullen gebruiken om te vertellen dat de compiler het gebruik van een specifieke interpolatie methode — zoals deze:
func save(_ text: String, prefix: String?, suffix: String?) { let text = "\(unwrapping: prefix)\(text)\(unwrapping: suffix)" textStorage.store(text)}
Hoewel het slechts syntactische suiker, is de bovengenoemde ziet er echt netjes! Echter, dat nauwelijks krassen op het oppervlak van wat aangepaste string interpolatie methoden kunnen doen. Ze kunnen zowel generiek als niet-generiek zijn, een willekeurig aantal argumenten accepteren, standaardwaarden gebruiken en vrijwel alles wat “normale” methoden kunnen doen.
Hier is een ander voorbeeld waarin we onze methode voor het converteren van een URL naar een HTML-link van tevoren in staat stellen om ook te worden gebruikt in de context van string interpolatie:
extension String.StringInterpolation { mutating func appendInterpolation(linkTo url: URL, _ title: String) { let string = url.html(withTitle: title) appendLiteral(string) }}
met het bovenstaande op zijn plaats, kunnen we nu eenvoudig HTML-links genereren van een URL als deze:
webView.loadHTMLString( "If you're not redirected, \(linkTo: url, "tap here").", baseURL: nil)
Het leuke van aangepaste string interpolatie is hoe de compiler elk van onze appendInterpolation
methoden neemt en ze vertaalt in corresponderende interpolatie API ‘ s — waardoor we volledige controle hebben over hoe de aanroep site eruit zal zien, bijvoorbeeld door het verwijderen van externe parameter labels, zoals we deden voor title
hierboven.
We gaan verder met het zoeken naar meer manieren om aangepaste string interpolatie te gebruiken, bijvoorbeeld met toegewezen strings en andere soorten tekst metadata, in aankomende artikelen.
Support Swift by Sundell door deze sponsor uit te checken:
Essential Developer: doe mee aan een gratis online spoedcursus voor iOS-ontwikkelaars die senior ontwikkelaars van black — belt willen worden-dat wil zeggen: behaal een deskundig niveau van praktische vaardigheden en word een van de best betaalde ontwikkelaars ter wereld.
conclusie
hoewel sommige van Swift ‘ s meer geavanceerde string letterlijke mogelijkheden alleen echt nuttig zijn in zeer specifieke situaties, zoals die in dit artikel, is het leuk om ze beschikbaar te hebben wanneer dat nodig is — vooral omdat het mogelijk is om ze volledig te vermijden en alleen strings"the old-fashioned way"
te gebruiken.
String literals is een ander gebied waarin Swift ‘ s protocol-georiënteerde ontwerp echt schittert. Door veel van hoe literals worden geïnterpreteerd en behandeld te delegeren aan implementatoren van protocollen, in plaats van hard-codering van dat gedrag in de compiler zelf, zijn wij als derden ontwikkelaars in staat om de manier waarop literals worden behandeld sterk aan te passen-terwijl nog steeds het houden van de standaardwaarden zo eenvoudig als ze kunnen zijn.