Literales de cadena en Swift
Ser capaz de expresar valores básicos, como cadenas e enteros, utilizando literales en línea es una característica esencial en la mayoría de los lenguajes de programación. Sin embargo, mientras que muchos otros lenguajes tienen soporte para literales específicos incorporados en su compilador, Swift adopta un enfoque mucho más dinámico, utilizando su propio sistema de tipos para definir cómo se deben manejar varios literales, a través de protocolos.
Esta semana, centrémonos en los literales de cadena en particular, echando un vistazo a las muchas formas diferentes en que se pueden usar y cómo, a través del diseño altamente orientado al protocolo de Swift, podemos personalizar la forma en que se interpretan los literales, lo que nos permite hacer algunas cosas realmente interesantes.
Desarrollador esencial: Únase a un curso intensivo en línea gratuito para desarrolladores de iOS que desean convertirse en desarrolladores senior de cinturón negro — es decir: alcanza un nivel experto de habilidades prácticas y forma parte de los desarrolladores mejor pagados del mundo.
Lo básico
Al igual que en muchos otros idiomas, las cadenas Swift se expresan a través de literales rodeados de comillas, y pueden contener secuencias especiales (como líneas nuevas), caracteres escapados y valores interpolados:
let string = "\(user.name) says \"Hi!\"\nWould you like to reply?"// John says "Hi!"// Would you like to reply?
Si bien las características utilizadas anteriormente ya nos proporcionan mucha flexibilidad, y lo más probable es que sean suficientes para la gran mayoría de los casos de uso, hay situaciones en las que formas más poderosas de expresar literales pueden ser útiles. Echemos un vistazo a algunos de ellos, comenzando con cuando necesitamos definir una cadena que contenga varias líneas de texto.
Literales multilíneas
Aunque cualquier literal de cadena estándar se puede dividir en varias líneas usando \n
, eso no siempre es práctico, especialmente si buscamos definir un fragmento de texto más grande como un literal en línea.
Afortunadamente, desde Swift 4, también podemos definir literales de cadena multilínea usando tres comillas en lugar de solo una. Por ejemplo, aquí estamos usando esa capacidad para generar un texto de ayuda para un script Swift, en caso de que el usuario no pase ningún argumento al invocarlo en la línea de comandos:
// 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)}
Arriba hacemos uso del hecho de que los literales de cadena multilínea conservan la sangría de su texto, en relación con el conjunto de comillas de terminación, en la parte inferior. También nos permiten usar con mucha más libertad las comillas no grabadas dentro de ellas, ya que están definidas por un conjunto de tres comillas, lo que hace que los límites de lo literal sean mucho menos ambiguos.
Las dos características anteriores hacen que los literales multilínea sean una gran herramienta para definir HTML en línea, por ejemplo, en algún tipo de herramienta de generación de páginas web, o al representar partes del contenido de una aplicación utilizando vistas web, como esta:
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 técnica anterior también puede ser muy útil al definir datos de prueba basados en cadenas. Por ejemplo, digamos que la configuración de nuestra aplicación debe ser exportable como XML, y que queremos escribir una prueba que verifique esa funcionalidad. En lugar de tener que definir el XML con el que queremos verificar en un archivo separado, podemos usar un literal de cadena multilínea para insertarlo en nuestra prueba:
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> """) }}
El beneficio de definir los datos de prueba en línea, como lo hacemos anteriormente, es que se vuelve mucho más fácil detectar rápidamente cualquier error cometido al escribir la prueba, ya que el código de prueba y la salida esperada se colocan uno al lado del otro. Sin embargo, si algunos datos de prueba exceden un puñado de líneas de longitud, o si los mismos datos deben usarse en varios lugares, aún puede valer la pena moverlos a su propio archivo.
Cadenas sin procesar
Nuevo en Swift 5, las cadenas sin procesar nos permiten desactivar todas las funciones de literal de cadena dinámica (como la interpolación y la interpretación de caracteres especiales, como \n
), a favor de simplemente tratar un literal como una secuencia de caracteres sin procesar. Las cadenas sin procesar se definen rodeando un literal de cadena con signos de libra (o «hashtags», como los llaman los niños):
let rawString = #"Press "Continue" to close this dialog."#
al igual que cómo hemos utilizado anteriormente un multilínea literal para definir los datos de prueba, raw literales de cadena son particularmente útiles cuando queremos inline cadenas que deben contener caracteres especiales — tales como comillas o barras diagonales inversas. Aquí hay otro ejemplo relacionado con la prueba, en el que usamos un literal de cadena sin procesar para definir una cadena JSON para codificar una instancia User
de:
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") }}
Más arriba usamos la API de decodificación basada en inferencia de tipos de «serialización basada en inferencia de tipos en Swift».
Mientras que las cadenas sin procesar deshabilitan características como la interpolación de cadenas de forma predeterminada, hay una forma de anularlo agregando otro signo de libra justo después de la barra invertida inicial de la interpolación, como esto:
extension URL { func html(withTitle title: String) -> String { return #"<a href="\#(absoluteString)">\#(title)</a>"# }}
Finalmente, las cadenas sin procesar también son particularmente útiles cuando se interpreta una cadena utilizando una sintaxis específica, especialmente si esa sintaxis se basa en gran medida en caracteres que normalmente tendrían que escaparse dentro de un literal de cadena, como expresiones regulares. Al definir expresiones regulares utilizando cadenas sin procesar, no es necesario escapar, lo que nos da expresiones que son tan legibles como se obtienen:
// 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+"#)
Incluso con las mejoras anteriores, es cuestionable lo fácil que son leer (y depurar) las expresiones regulares, especialmente cuando se usan en el contexto de un lenguaje altamente seguro de tipos como Swift. Lo más probable es que se reduzca a la experiencia previa de cualquier desarrollador con expresiones regulares, ya sea que las prefieran o no en lugar de implementar algoritmos de análisis de cadenas más personalizados, directamente en Swift.
Expresar valores usando literales de cadena
Mientras que todos los literales de cadena se convierten en valores String
de forma predeterminada, también podemos usarlos para expresar valores personalizados. Al igual que echamos un vistazo en «Identificadores seguros de tipos en Swift», agregar soporte literal de cadena a uno de nuestros propios tipos puede permitirnos lograr una mayor seguridad de tipos, sin sacrificar la conveniencia de usar literales.
Por ejemplo, digamos que hemos definido un protocolo Searchable
para que actúe como API para buscar cualquier tipo de base de datos o almacenamiento subyacente que utilice nuestra aplicación — y que estamos utilizando una enumeración Query
para modelar diferentes formas de realizar dicha búsqueda:
protocol Searchable { associatedtype Element func search(for query: Query) -> }enum Query { case matching(String) case notMatching(String) case matchingAny()}
El enfoque anterior nos da mucha potencia y flexibilidad en cuanto a cómo realizaremos cada búsqueda, pero el caso de uso más común sigue siendo probablemente el más simple: buscar elementos que coincidan con una cadena dada, y sería realmente bueno si pudiéramos hacerlo usando un literal de cadena.
La buena noticia es que podemos hacer que eso suceda, aunque manteniendo por encima de la API completamente intacta, haciendo Query
ajustarse a ExpressibleByStringLiteral
:
extension Query: ExpressibleByStringLiteral { init(stringLiteral value: String) { self = .matching(value) }}
De esta manera, ahora somos libres de realizar búsquedas coincidentes sin tener que crear un valor Query
manualmente: todo lo que necesitamos hacer es pasar un literal de cadena como si la API a la que estamos llamando realmente aceptara un String
directamente. Aquí estamos usando esa capacidad para implementar una prueba que verifique que un tipo UserStorage
implementa correctamente su funcionalidad de búsqueda:
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, ) }}
Las expresiones literales de cadena personalizadas pueden, en muchas situaciones, evitar tener que elegir entre seguridad de tipo y comodidad al trabajar con tipos basados en cadenas, como consultas e identificadores. Puede ser una gran herramienta de usar para lograr un diseño de API que se escale bien desde el caso de uso más simple, hasta cubrir los casos de borde y ofrecer más potencia y personalización cuando sea necesario.
Interpolación personalizada
Una cosa que todos los «sabores» de literales de cadena Swift tienen en común es su soporte para interpolar valores. Si bien siempre hemos sido capaces de personalizar cómo se interpola un tipo determinado ajustándonos a CustomStringConvertible
, Swift 5 introduce nuevas formas de implementar API personalizadas justo encima del motor de interpolación de cadenas.
Como ejemplo, digamos que queremos guardar una cadena determinada opcionalmente aplicándole un prefijo y un sufijo. Idealmente, nos gustaría simplemente interpolar esos valores para formar la cadena final, como esta:
func save(_ text: String, prefix: String?, suffix: String?) { let text = "\(prefix)\(text)\(suffix)" textStorage.store(text)}
Sin embargo, dado que ambos prefix
y suffix
son opcionales, simplemente usar su descripción no producirá el resultado que estamos buscando, y el compilador incluso nos dará una advertencia:
String interpolation produces a debug description for an optional value
Aunque siempre tenemos la opción de desenvolver cada uno de esos dos opcionales antes de interpolarlos, echemos un vistazo a cómo podríamos hacer ambas cosas de una sola vez usando interpolación personalizada. Comenzaremos extendiendo String.StringInterpolation
con una nueva sobrecarga appendInterpolation
que acepta cualquier valor opcional:
extension String.StringInterpolation { mutating func appendInterpolation<T>(unwrapping optional: T?) { let string = optional.map { "\($0)" } ?? "" appendLiteral(string) }}
La etiqueta de parámetro anterior unwrapping:
es importante, ya que es lo que usaremos para indicar el compilador para usar ese método de interpolación específico, como este:
func save(_ text: String, prefix: String?, suffix: String?) { let text = "\(unwrapping: prefix)\(text)\(unwrapping: suffix)" textStorage.store(text)}
Aunque es solo azúcar sintáctica, ¡lo anterior se ve muy bien! Sin embargo, eso apenas roza la superficie de lo que pueden hacer los métodos de interpolación de cadenas personalizados. Pueden ser genéricos y no genéricos, aceptar cualquier número de argumentos, usar valores predeterminados y prácticamente cualquier otra cosa que los métodos «normales» puedan hacer.
Este es otro ejemplo en el que habilitamos nuestro método para convertir una URL en un enlace HTML de antes para que también se use en el contexto de la interpolación de cadenas:
extension String.StringInterpolation { mutating func appendInterpolation(linkTo url: URL, _ title: String) { let string = url.html(withTitle: title) appendLiteral(string) }}
Con lo anterior en su lugar, ahora podemos generar fácilmente enlaces HTML desde una URL como esta:
webView.loadHTMLString( "If you're not redirected, \(linkTo: url, "tap here").", baseURL: nil)
Lo bueno de la interpolación de cadenas personalizada es cómo el compilador toma cada uno de nuestros métodos appendInterpolation
y los traduce en las API de interpolación correspondientes, lo que nos da un control completo sobre cómo se verá el sitio de la llamada, por ejemplo, eliminando etiquetas de parámetros externos, como hicimos para title
arriba.
Seguiremos buscando más formas de usar la interpolación de cadenas personalizadas, por ejemplo, con cadenas atribuidas y otros tipos de metadatos de texto, en próximos artículos.
Apoye a Swift by Sundell echando un vistazo a este patrocinador:
Desarrollador esencial: Únase a un curso intensivo en línea gratuito para desarrolladores de iOS que desean convertirse en desarrolladores senior de cinturón negro, es decir: alcance un nivel experto de habilidades prácticas y forme parte de los desarrolladores mejor pagados del mundo.
Conclusión
Aunque algunas de las capacidades literales de cadena más avanzadas de Swift solo son realmente útiles en situaciones muy específicas, como las de este artículo, es bueno tenerlas disponibles cuando sea necesario, especialmente porque es posible evitarlas completamente y solo usar cadenas "the old-fashioned way"
.
Literales de cadena es otra área en la que el diseño orientado al protocolo de Swift realmente brilla. Al delegar gran parte de cómo se interpretan y manejan los literales a los implementadores de protocolos, en lugar de codificar esos comportamientos en el propio compilador, nosotros, como desarrolladores de terceros, podemos personalizar en gran medida la forma en que se manejan los literales, al tiempo que mantenemos los valores predeterminados tan simples como pueden ser.