Strängbokstäver i Swift
att kunna uttrycka grundläggande värden, såsom strängar och heltal, med inline-bokstäver är en viktig funktion i de flesta programmeringsspråk. Men medan många andra språk har stöd för specifika bokstäver bakade i sin kompilator, tar Swift ett mycket mer dynamiskt tillvägagångssätt — med sitt eget typsystem för att definiera hur olika bokstäver ska hanteras, genom protokoll.
den här veckan, låt oss fokusera på strängbokstäver i synnerhet genom att ta en titt på de många olika sätt som de kan användas och hur vi — genom Swifts mycket protokollorienterade design-kan anpassa hur bokstäver tolkas, vilket gör att vi kan göra några riktigt intressanta saker.
Essential Developer: gå med i en gratis online-kraschkurs för iOS-utvecklare som vill bli black belt senior developers-det är: uppnå en expertnivå av praktiska färdigheter och bli en del av de högst betalda utvecklarna i världen.
grunderna
precis som på många andra språk uttrycks snabba strängar genom bokstäver omgivna av citattecken-och kan innehålla både speciella sekvenser( som nyrader), flyktiga tecken och interpolerade värden:
let string = "\(user.name) says \"Hi!\"\nWould you like to reply?"// John says "Hi!"// Would you like to reply?
även om funktionerna som används ovan redan ger oss mycket flexibilitet och troligen är tillräckliga för de allra flesta användningsfall, finns det situationer där mer kraftfulla sätt att uttrycka bokstäver kan komma till nytta. Låt oss ta en titt på några av dem, börjar med när vi behöver definiera en sträng som innehåller flera textrader.
Multiline literals
även om någon standardsträng bokstav kan delas upp i flera rader med \n
, är det inte alltid praktiskt — speciellt om vi vill definiera en större textbit som en inline bokstavlig.tack och lov, sedan Swift 4, kan vi också definiera flerradiga strängbokstäver med tre citattecken istället för bara en. Till exempel, här använder vi den förmågan att mata ut en hjälptext för ett Swift-skript, om användaren inte passerade några argument när han åberopade det på kommandoraden:
// 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)}
ovan använder vi det faktum att flerradiga strängbokstäver bevarar textens indrag, i förhållande till den avslutande uppsättningen citattecken, längst ner. De gör det också möjligt för oss att mycket mer fritt använda obestämda citattecken inom dem, eftersom de definieras av en uppsättning av tre citattecken, vilket gör gränserna för den bokstavliga mycket mindre benägna att bli tvetydiga.
båda ovanstående två egenskaper gör multiline literals ett bra verktyg för att definiera inline HTML — till exempel i någon form av webbsida genereringsverktyg, eller när rendering delar av en app innehåll med hjälp av webbvyer — så här:
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> """ }}
ovanstående teknik kan också vara riktigt användbar när man definierar strängbaserade testdata. Låt oss till exempel säga att appens inställningar måste exporteras som XML och att vi vill skriva ett test som verifierar den funktionaliteten. I stället för att behöva definiera XML som vi vill verifiera mot i en separat fil-kan vi använda en multiline-sträng bokstavlig för att infoga den i vårt test:
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> """) }}
fördelen med att definiera testdata inline, som vi gör ovan, är att det blir mycket lättare att snabbt upptäcka eventuella fel som gjorts när testet skrivs — eftersom testkoden och den förväntade utmatningen placeras bredvid varandra. Men om vissa testdata överstiger en handfull rader i längd, eller om samma data måste användas på flera platser, kan det fortfarande vara värt att flytta den till sin egen fil.
Raw strings
nytt i Swift 5, raw strings gör det möjligt för oss att stänga av alla dynamiska strängbokstavsfunktioner (som interpolering och tolkning av specialtecken, som \n
), till förmån för att helt enkelt behandla en bokstav som en rå sekvens av tecken. Råa strängar definieras genom att omge en sträng bokstavlig med pund tecken (eller ”hashtags”, som barnen kallar dem):
let rawString = #"Press "Continue" to close this dialog."#
precis som hur vi ovan använde en multiline — bokstav för att definiera testdata, är råa strängbokstäver särskilt användbara när vi vill inline-strängar som måste innehålla specialtecken-som citattecken eller backslashes. Här är ett annat testrelaterat exempel, där vi använder en raw-sträng bokstavlig för att definiera en JSON-sträng för att koda en User
instans från:
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") }}
ovan använder vi Typ inferensbaserad avkodnings-API från ”typ inferensdriven serialisering i Swift”.
medan raw-strängar inaktiverar funktioner som string interpolation som standard finns det ett sätt att åsidosätta det genom att lägga till ett annat pundtecken direkt efter interpolationens ledande backslash — så här:
extension URL { func html(withTitle title: String) -> String { return #"<a href="\#(absoluteString)">\#(title)</a>"# }}
slutligen är raw — strängar också särskilt användbara när man tolkar en sträng med en specifik syntax, speciellt om den syntaxen är starkt beroende av tecken som normalt skulle behöva fly inom en sträng bokstavlig-som reguljära uttryck. Genom att definiera reguljära uttryck med raw-strängar behövs ingen flykt, vilket ger oss uttryck som är lika läsbara som de blir:
// 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+"#)
även med ovanstående förbättringar är det tveksamt hur lätt att läsa (och felsöka) reguljära uttryck är — speciellt när de används i samband med ett mycket typsäkert språk som Swift. Det kommer sannolikt att komma ner till en viss utvecklares tidigare erfarenhet av reguljära uttryck, oavsett om de föredrar dem över att implementera mer anpassade strängparsningsalgoritmer, direkt i Swift.
uttrycka värden med strängbokstäver
medan alla strängbokstäver förvandlas tillString
värden som standard kan vi också använda dem för att uttrycka anpassade värden också. Som vi tittade på i ”typsäkra identifierare i Swift”, kan vi lägga till sträng bokstavligt stöd till en av våra egna typer, vilket gör att vi kan uppnå ökad typsäkerhet utan att offra bekvämligheten med att använda bokstäver.låt oss till exempel säga att vi har definierat ett Searchable
protokoll för att fungera som API för att söka efter någon form av Databas eller underliggande lagring som vår app använder — och att vi använder ett Query
enum för att modellera olika sätt att utföra en sådan sökning:
protocol Searchable { associatedtype Element func search(for query: Query) -> }enum Query { case matching(String) case notMatching(String) case matchingAny()}
ovanstående tillvägagångssätt ger oss mycket kraft och flexibilitet för hur vi ska utföra varje sökning, men det vanligaste användningsfallet är fortfarande sannolikt det enklaste — söker efter element som matchar en viss sträng — och det skulle vara riktigt trevligt om vi kunde göra det med en sträng bokstavlig.
den goda nyheten är att vi kan få det att hända, samtidigt som vi håller ovanstående API helt intakt, genom att göra Query
överensstämmer med ExpressibleByStringLiteral
:
extension Query: ExpressibleByStringLiteral { init(stringLiteral value: String) { self = .matching(value) }}
på det sättet är vi nu fria att utföra matchande sökningar utan att behöva skapa ett Query
värde manuellt — allt vi behöver göra är att skicka en sträng bokstavlig som om API vi ringer faktiskt accepterade ett String
direkt. Här använder vi den förmågan att implementera ett test som verifierar att en UserStorage
typ implementerar korrekt sin sökfunktionalitet:
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, ) }}
anpassade strängbokstavsuttryck kan i många situationer låta oss undvika att behöva välja mellan typsäkerhet och bekvämlighet när vi arbetar med strängbaserade typer, till exempel frågor och identifierare. Det kan vara ett bra verktyg att använda för att uppnå en API-design som skalar bra från det enklaste användningsfallet, hela vägen till att täcka kantfall och erbjuda mer kraft och anpassningsbarhet när det behövs.
Anpassad interpolering
en sak som alla ”smaker” av Swift-strängbokstäver har gemensamt är deras stöd för interpoleringsvärden. Medan vi alltid har kunnat anpassa hur en viss typ interpoleras genom att överensstämma med CustomStringConvertible
— Swift 5 introducerar nya sätt att implementera anpassade API: er ovanpå stränginterpoleringsmotorn.
som ett exempel, låt oss säga att vi vill spara en viss sträng genom att eventuellt tillämpa ett prefix och suffix på det. Helst vill vi helt enkelt interpolera dessa värden för att bilda den sista strängen, så här:
func save(_ text: String, prefix: String?, suffix: String?) { let text = "\(prefix)\(text)\(suffix)" textStorage.store(text)}
men eftersom både prefix
och suffix
är alternativ, helt enkelt med hjälp av deras beskrivning kommer inte att ge det resultat vi letar efter — och kompilatorn kommer även att ge oss en varning:
String interpolation produces a debug description for an optional value
medan vi alltid har möjlighet att packa upp var och en av dessa två alternativ innan vi interpolerar dem, låt oss ta en titt på hur vi kunde göra båda dessa saker på en gång med anpassad interpolering. Vi börjar med att utvidga String.StringInterpolation
med en ny appendInterpolation
överbelastning som accepterar valfritt värde:
extension String.StringInterpolation { mutating func appendInterpolation<T>(unwrapping optional: T?) { let string = optional.map { "\($0)" } ?? "" appendLiteral(string) }}
ovanstående unwrapping:
parameteretikett är viktigt, eftersom det är vad vi ska använda för att berätta kompilatorn att använda den specifika interpoleringsmetoden — så här:
func save(_ text: String, prefix: String?, suffix: String?) { let text = "\(unwrapping: prefix)\(text)\(unwrapping: suffix)" textStorage.store(text)}
även om det bara är syntaktiskt socker ser ovanstående väldigt snyggt ut! Men det skrapar knappt ytan på vad anpassade stränginterpoleringsmetoder kan göra. De kan vara både generiska och icke-generiska, acceptera ett antal argument, använda standardvärden och i stort sett allt annat som ”normala” metoder kan göra.
Här är ett annat exempel där vi aktiverar vår metod för att konvertera en URL till en HTML-länk från tidigare för att också användas i samband med stränginterpolering:
extension String.StringInterpolation { mutating func appendInterpolation(linkTo url: URL, _ title: String) { let string = url.html(withTitle: title) appendLiteral(string) }}
med ovanstående på plats kan vi nu enkelt generera HTML-länkar från en URL som denna:
webView.loadHTMLString( "If you're not redirected, \(linkTo: url, "tap here").", baseURL: nil)
det coola med anpassad stränginterpolering är hur kompilatorn tar var och en av våra appendInterpolation
metoder och översätter dem till motsvarande Interpolerings — API: er-vilket ger oss fullständig kontroll över hur samtalsplatsen kommer att se ut, till exempel genom att ta bort externa parameteretiketter, som vi gjorde för title
ovan.
Vi fortsätter att undersöka fler sätt att använda anpassad stränginterpolering, till exempel med tillskrivna strängar och andra typer av textmetadata, i kommande artiklar.
Support Swift by Sundell genom att kolla in denna sponsor:
Essential Developer: gå med i en gratis online crash course för iOS-utvecklare som vill bli black belt senior developers — det vill säga: uppnå en expertnivå av praktiska färdigheter och bli en del av de högst betalda utvecklarna i världen.
slutsats
medan vissa av Swifts mer avancerade strängbokstavsfunktioner bara är användbara i mycket specifika situationer, som de i den här artikeln, är det trevligt att ha dem tillgängliga när det behövs — särskilt eftersom det är möjligt att helt undvika dem och bara använda strängar "the old-fashioned way"
.
String literals är ett annat område där Swifts protokollorienterade design verkligen lyser. Genom att delegera mycket av hur bokstäver tolkas och hanteras till implementatörer av protokoll, snarare än hårdkodning av dessa beteenden i kompilatorn själv, kan vi som tredjepartsutvecklare kraftigt anpassa hur bokstäver hanteras-samtidigt som standardvärdena är så enkla som de kan vara.