Letterali stringa in Swift
Essere in grado di esprimere valori di base, come stringhe e numeri interi, utilizzando letterali in linea è una caratteristica essenziale nella maggior parte dei linguaggi di programmazione. Tuttavia, mentre molti altri linguaggi hanno il supporto per letterali specifici inseriti nel loro compilatore, Swift adotta un approccio molto più dinamico, utilizzando il proprio sistema di tipi per definire come i vari letterali dovrebbero essere gestiti, attraverso i protocolli.
Questa settimana, concentriamoci sui letterali di stringa in particolare, dando un’occhiata ai molti modi diversi in cui possono essere utilizzati e come noi — attraverso il design altamente orientato al protocollo di Swift-siamo in grado di personalizzare il modo in cui i letterali vengono interpretati, il che ci consente di fare alcune cose davvero interessanti.
Sviluppatore essenziale: partecipa a un corso intensivo online gratuito per gli sviluppatori iOS che vogliono diventare sviluppatori senior black-belt-ovvero: raggiungi un livello esperto di abilità pratiche e diventa parte degli sviluppatori più pagati al mondo.
Le basi
Proprio come in molte altre lingue, le stringhe Swift sono espresse attraverso letterali circondati da virgolette e possono contenere sia sequenze speciali (come newline), caratteri di escape e valori interpolati:
let string = "\(user.name) says \"Hi!\"\nWould you like to reply?"// John says "Hi!"// Would you like to reply?
Mentre le funzionalità utilizzate sopra ci forniscono già molta flessibilità e sono molto probabilmente sufficienti per la stragrande maggioranza dei casi d’uso, ci sono situazioni in cui modi più potenti di esprimere letterali possono tornare utili. Diamo un’occhiata ad alcuni di questi, a partire da quando abbiamo bisogno di definire una stringa contenente più righe di testo.
Letterali multilinea
Sebbene qualsiasi letterale stringa standard possa essere suddiviso in più righe usando\n
, questo non è sempre pratico, specialmente se stiamo cercando di definire un pezzo di testo più grande come letterale in linea.
Per fortuna, da Swift 4, siamo anche in grado di definire letterali di stringhe multilinea usando tre virgolette invece di una sola. Ad esempio, qui stiamo usando questa funzionalità per produrre un testo di aiuto per uno script Swift, nel caso in cui l’utente non abbia passato alcun argomento quando lo ha richiamato sulla riga di comando:
// 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)}
Sopra facciamo uso del fatto che i letterali di stringa multilinea conservano il rientro del loro testo, relativo al set di terminazione delle virgolette, in basso. Ci permettono anche di usare molto più liberamente le virgolette senza escape al loro interno, poiché sono definite da un insieme di tre virgolette, rendendo molto meno probabile che i limiti del letterale diventino ambigui.
Entrambe le due caratteristiche di cui sopra rendono letterali multilinea un ottimo strumento per la definizione di HTML inline — per esempio in qualche forma di strumento di generazione di pagine web, o quando il rendering di parti del contenuto di un app utilizzando viste web — come questo:
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> """ }}
La tecnica di cui sopra può anche essere molto utile Ad esempio, diciamo che le impostazioni della nostra app devono essere esportabili come XML e che vogliamo scrivere un test che verifichi tale funzionalità. Anziché definire XML che si desidera verificare in un file separato — si può usare un multiplo del valore letterale stringa inline nel nostro 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> """) }}
Il vantaggio di definizione dei dati di prova in linea, come sopra, è che diventa molto più facile individuare rapidamente eventuali errori durante la scrittura di test — dal momento che il codice di test e i risultati attesi sono poste l’una accanto all’altra. Tuttavia, se alcuni dati di test superano una manciata di linee di lunghezza o se gli stessi dati devono essere utilizzati in più posizioni, può comunque valere la pena spostarli nel proprio file.
Stringhe raw
Novità di Swift 5, le stringhe raw ci consentono di disattivare tutte le funzionalità letterali delle stringhe dinamiche (come l’interpolazione e l’interpretazione di caratteri speciali, come\n
), a favore di trattare semplicemente un letterale come una sequenza di caratteri grezzi. Le stringhe raw sono definite circondando una stringa letterale con segni di sterlina (o “hashtag”, come li chiamano i bambini):
let rawString = #"Press "Continue" to close this dialog."#
Proprio come abbiamo usato un letterale multilinea per definire i dati di test, i letterali di stringa grezzi sono particolarmente utili quando vogliamo stringhe in linea che devono contenere caratteri speciali, come virgolette o barre rovesciate. Ecco un altro esempio relativo al test, in cui usiamo un letterale stringa grezza per definire una stringa JSON per codificare un’istanza User
da:
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") }}
Sopra usiamo l’API di decodifica basata sull’inferenza di tipo da “Serializzazione basata sull’inferenza di tipo in Swift”.
Mentre stringhe raw disattivare funzionalità come stringa di interpolazione per impostazione predefinita, c’è un modo per ignorare che con l’aggiunta di un altro cancelletto subito dopo l’interpolazione leader backslash come questo:
extension URL { func html(withTitle title: String) -> String { return #"<a href="\#(absoluteString)">\#(title)</a>"# }}
Infine, prime corde sono anche particolarmente utile per interpretare una stringa utilizzando una sintassi specifica, soprattutto se la sintassi si basa pesantemente su di caratteri che normalmente devono essere sfuggito all’interno di una stringa letterale — come le espressioni regolari. Definendo le espressioni regolari usando stringhe non elaborate, non è necessaria alcuna escape, dandoci espressioni che sono leggibili come ottengono:
// 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+"#)
Anche con i miglioramenti di cui sopra, è discutibile quanto siano facili da leggere (ed eseguire il debug) le espressioni regolari, specialmente se usate nel contesto di un linguaggio altamente sicuro come Swift. Molto probabilmente si ridurrà alla precedente esperienza di qualsiasi sviluppatore con le espressioni regolari, indipendentemente dal fatto che preferiscano o meno l’implementazione di più algoritmi di analisi delle stringhe personalizzati, direttamente in Swift.
Esprimere valori usando i valori letterali di stringa
Mentre tutti i valori letterali di stringa vengono trasformati in valoriString
per impostazione predefinita, possiamo anche usarli per esprimere valori personalizzati. Come abbiamo dato un’occhiata a “Identificatori di tipo sicuro in Swift”, l’aggiunta di un supporto letterale di stringa a uno dei nostri tipi può consentirci di ottenere una maggiore sicurezza dei tipi, senza sacrificare la comodità di utilizzare i letterali.
Ad esempio, diciamo che abbiamo definito un Searchable
protocollo per fungere da API per la ricerca di qualsiasi tipo di database o storage sottostante utilizzato dalla nostra app — e che stiamo usando un Query
enum per modellare diversi modi per eseguire tale ricerca:
protocol Searchable { associatedtype Element func search(for query: Query) -> }enum Query { case matching(String) case notMatching(String) case matchingAny()}
L’approccio di cui sopra ci dà molta potenza e flessibilità su come eseguiremo ogni ricerca, ma il caso d’uso più comune è ancora probabilmente il più semplice — la ricerca di elementi corrispondenti a una determinata stringa — e sarebbe davvero bello se fossimo in grado di farlo usando una stringa letterale.
La buona notizia è che possiamo farlo accadere, pur mantenendo completamente intatta l’API di cui sopra, rendendo Query
conforme a ExpressibleByStringLiteral
:
extension Query: ExpressibleByStringLiteral { init(stringLiteral value: String) { self = .matching(value) }}
In questo modo siamo ora liberi di eseguire ricerche corrispondenti senza dover creare manualmente un valoreQuery
— tutto ciò che dobbiamo fare è passare una stringa letterale come se l’API che stiamo chiamando accettasse effettivamente unString
direttamente. Qui stiamo usando questa capacità per implementare un test che verifica che un tipoUserStorage
implementi correttamente la sua funzionalità di ricerca:
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, ) }}
Le espressioni letterali di stringa personalizzate possono in molte situazioni evitare di dover scegliere tra sicurezza e convenienza del tipo quando si lavora con tipi basati su stringhe, come query e identificatori. Può essere un ottimo strumento da utilizzare per ottenere un design API che si adatti bene al caso d’uso più semplice, fino a coprire i casi limite e offrire più potenza e personalizzazione quando necessario.
Interpolazione personalizzata
Una cosa che tutti i “sapori” dei letterali Swift string hanno in comune è il loro supporto per l’interpolazione dei valori. Mentre siamo sempre stati in grado di personalizzare il modo in cui un determinato tipo viene interpolato conformandosi a CustomStringConvertible
— Swift 5 introduce nuovi modi di implementare API personalizzate direttamente sul motore di interpolazione delle stringhe.
Ad esempio, diciamo che vogliamo salvare una determinata stringa applicando opzionalmente un prefisso e un suffisso ad essa. Idealmente vorremmo semplicemente interpolare quei valori per formare la stringa finale, in questo modo:
func save(_ text: String, prefix: String?, suffix: String?) { let text = "\(prefix)\(text)\(suffix)" textStorage.store(text)}
Tuttavia, dal momento che entrambi prefix
e suffix
sono opzionali, semplicemente utilizzando la loro descrizione non produrre il risultato che stiamo cercando — e il compilatore dare anche noi un messaggio di avviso:
String interpolation produces a debug description for an optional value
Mentre abbiamo sempre la possibilità di svolgimento di ciascuna di queste due optionals prima di interpolazione di loro, diamo un’occhiata a come si potrebbe fare entrambe le cose in un colpo personalizzati utilizzando l’interpolazione. Iniziamo con estensione String.StringInterpolation
con un nuovo appendInterpolation
overload che accetta qualsiasi valore opzionale:
extension String.StringInterpolation { mutating func appendInterpolation<T>(unwrapping optional: T?) { let string = optional.map { "\($0)" } ?? "" appendLiteral(string) }}
sopra unwrapping:
parametro etichetta è importante, come è quello che useremo per indicare al compilatore di utilizzare quello specifico metodo di interpolazione, come questo:
func save(_ text: String, prefix: String?, suffix: String?) { let text = "\(unwrapping: prefix)\(text)\(unwrapping: suffix)" textStorage.store(text)}
anche se è solo “zucchero sintattico”, di cui sopra, sembra davvero pulito! Tuttavia, ciò graffia a malapena la superficie di ciò che i metodi di interpolazione delle stringhe personalizzati possono fare. Possono essere sia generici che non generici, accettare qualsiasi numero di argomenti, utilizzare valori predefiniti e praticamente qualsiasi altra cosa che i metodi “normali” possono fare.
Ecco un altro esempio in cui abilitiamo il nostro metodo per convertire un URL in un link HTML da prima per essere utilizzato anche nel contesto dell’interpolazione delle stringhe:
extension String.StringInterpolation { mutating func appendInterpolation(linkTo url: URL, _ title: String) { let string = url.html(withTitle: title) appendLiteral(string) }}
Con quanto sopra, ora possiamo facilmente generare collegamenti HTML da un URL come questo:
webView.loadHTMLString( "If you're not redirected, \(linkTo: url, "tap here").", baseURL: nil)
La cosa interessante dell’interpolazione di stringhe personalizzata è come il compilatore prende ciascuno dei nostri metodi appendInterpolation
e li traduce in API di interpolazione corrispondenti — dandoci il controllo completo su come sarà il sito di chiamata, ad esempio rimuovendo etichette di parametri esterni, come abbiamo fatto per title
sopra.
Continueremo a esaminare altri modi di utilizzare l’interpolazione di stringhe personalizzata, ad esempio con stringhe attribuite e altri tipi di metadati di testo, nei prossimi articoli.
Supporta Swift di Sundell controllando questo sponsor:
Sviluppatore essenziale: partecipa a un corso accelerato online gratuito per sviluppatori iOS che vogliono diventare sviluppatori senior black-belt-ovvero: raggiungere un livello esperto di abilità pratiche e diventare parte degli sviluppatori più pagati al mondo.
Conclusione
Mentre alcune delle funzionalità letterali stringa più avanzate di Swift sono davvero utili solo in situazioni molto specifiche, come quelle in questo articolo, è bello averle disponibili quando necessario, soprattutto perché è possibile evitarle completamente e utilizzare solo le stringhe "the old-fashioned way"
.
I letterali di stringa sono un’altra area in cui il design orientato al protocollo di Swift brilla davvero. Delegando gran parte del modo in cui i letterali vengono interpretati e gestiti dagli implementatori dei protocolli, piuttosto che codificare quei comportamenti nel compilatore stesso, noi come sviluppatori di terze parti siamo in grado di personalizzare pesantemente il modo in cui i letterali vengono gestiti, pur mantenendo i valori predefiniti il più semplici possibile.