guard let y textos que no son números
Al terminar: Usar guard let y convertir textos en números sin que la app se rompa.
Al terminar, Jugador.swift tendrá además presentar(_:), que usa guard let, y dorsalDesdeTexto(_:), que convierte un texto en un dorsal si puede. Y torneo-cli imprimirá una lista de jugadores y cuatro conversiones de texto, con once tests que pasan.
Importa porque los textos que escribe una persona (un formulario, un campo de la pantalla) casi nunca son números fiables, y Int(texto) te devuelve un opcional en vez de romper la app. Y porque guard let es la forma que más verás en Swift para abrir un opcional al empezar una función.
Temas: guard let, el comodín _ en un parámetro, Int(texto) e import Foundation.
guard let: abrir el opcional para el resto de la función
if let abre el valor solo dentro de sus llaves. Cuando lo que quieres es lo contrario («si falta, me voy; si no, sigo con el valor»), if let obliga a meter todo el resto de la función dentro de las llaves. guard let lo evita: abre el opcional para el resto de la función y, si es nil, ejecuta su else, que está obligado a salir de la función (con return; más adelante verás que también se puede salir con throw). Si el else no sale, el compilador no compila. Piensa en un portero de discoteca: comprueba la entrada nada más llegar y, si no hay, te manda fuera; los que pasan ya no se vuelven a comprobar.
Tres detalles de sintaxis:
guard let socio else { … }es la forma corta (comoif let socio): crea una constante con el mismo nombre.guard let cuota = socio.cuota else { … }le pone un nombre nuevo.- Una coma abre varios opcionales de una vez:
guard let socio, let cuota = socio.cuota else { … }. Si falta cualquiera de los dos, sale por elelse. _antes de un parámetro, como ensaludar(_ socio: Socio?), quita la etiqueta al llamar:saludar(pepa)en vez desaludar(socio: pepa). Se explica a fondo en Funciones y switch.
Textos que no son números: Int(texto)
Int(texto) es un inicializador que puede fallar: devuelve un Int?, con el número si el texto lo es y nil si no (con "diez" o con un texto vacío). No detiene el programa ni inventa un 0: quien lo usa decide qué hacer con el nil. Para quitar los espacios de los bordes antes de convertir, texto.trimmingCharacters(in: .whitespaces) viene de Foundation, la librería de Apple con utilidades de texto y fechas: hay que escribir import Foundation en la primera línea del fichero. .whitespaces lleva un punto delante y no escribe el tipo: Swift lo deduce por el contexto.
Todo junto, con un Socio del club que puede no pagar cuota:
import Foundation // primera línea del fichero: trae trimmingCharacters
struct Socio {
let nombre: String
let cuota: Int? // la cuota puede faltar (socio exento)
}
// `_` quita la etiqueta: se llama saludar(pepa), no saludar(socio: pepa).
func saludar(_ socio: Socio?) -> String {
guard let socio else { return "Nadie en la puerta" } // si es nil, sale aquí
return "Hola, \(socio.nombre)" // desde aquí, socio es un Socio, no un Socio?
}
// La coma abre dos opcionales en un solo guard: si falta cualquiera, sale.
func cobrar(_ socio: Socio?) -> String {
guard let socio, let cuota = socio.cuota else { return "Sin cuota que cobrar" }
return "\(socio.nombre) paga \(cuota) €"
}
// Quita los espacios de los bordes e intenta convertir a Int: nil si no es un número.
func edadDesdeTexto(_ texto: String) -> Int? {
Int(texto.trimmingCharacters(in: .whitespaces))
}
let pepa = Socio(nombre: "Pepa", cuota: 30)
print(saludar(pepa))
print(saludar(nil))
print(cobrar(pepa))
print(cobrar(Socio(nombre: "Tomás", cuota: nil)))
print(edadDesdeTexto(" 42 ") as Any)
print(edadDesdeTexto("cuarenta") as Any)Al ejecutarlo en tu Playground, la consola enseña:
Hola, Pepa
Nadie en la puerta
Pepa paga 30 €
Sin cuota que cobrar
Optional(42)
nilKotlin frente a Swift
| Kotlin (tu código de Kotlin) | Swift |
|---|---|
if (jugador == null) return … y a partir de ahí jugador es no nulo | guard let jugador else { return … } |
try { texto.trim().toInt() } catch (…) { null } | Int(texto.trimmingCharacters(in: .whitespaces)) |
En Kotlin, dorsalDesdeTexto atrapaba la excepción de toInt() (Ficheros y depurador). En Swift no hace falta atrapar nada: Int(…) ya devuelve nil.
Reto · Ejercicio: dos niveles de opcional
En tu Playground, define tres tipos: Camiseta, con un número y un texto calculado que lo da como «#9» (la propiedad calculada de Opcionales); Futbolista, con un nombre y una camiseta que puede faltar; y Equipo, con un nombre y un capitán que puede faltar.
Escribe una función que reciba un equipo, que también puede faltar, y devuelva el nombre del equipo, el del capitán y el dorsal, como «Rayo FC: Ana #9»; si falta el equipo, el capitán o la camiseta, devuelve «sin capitán con dorsal». Hazla dos veces:
conGuard, que abre los tres niveles uno detrás de otro, con una salida temprana por nivel.conCadena, que recorre la cadena de opcionales de una vez dentro de una sola salida temprana. Pista: mira cómocobrarabre dos opcionales a la vez.
Prueba las dos con el Rayo FC, cuya capitana es Ana con la camiseta 9, y la primera también sin equipo. Lo que debe salir:
Rayo FC: Ana #9
Rayo FC: Ana #9
sin capitán con dorsalVer solución
struct Camiseta {
let numero: Int
var texto: String { "#\(numero)" }
}
struct Futbolista {
let nombre: String
let camiseta: Camiseta?
}
struct Equipo {
let nombre: String
let capitan: Futbolista?
}
func conGuard(_ equipo: Equipo?) -> String {
guard let equipo else { return "sin capitán con dorsal" }
guard let capitan = equipo.capitan else { return "sin capitán con dorsal" }
guard let camiseta = capitan.camiseta else { return "sin capitán con dorsal" }
return "\(equipo.nombre): \(capitan.nombre) \(camiseta.texto)"
}
func conCadena(_ equipo: Equipo?) -> String {
guard let equipo, let texto = equipo.capitan?.camiseta?.texto else {
return "sin capitán con dorsal"
}
return "\(equipo.nombre): \(equipo.capitan?.nombre ?? "") \(texto)"
}
let rayo = Equipo(nombre: "Rayo FC", capitan: Futbolista(nombre: "Ana", camiseta: Camiseta(numero: 9)))
print(conGuard(rayo)) // Rayo FC: Ana #9
print(conCadena(rayo)) // Rayo FC: Ana #9
print(conGuard(nil)) // sin capitán con dorsalLa primera versión abre un nivel por guard y sale en cuanto falta uno. La segunda usa una coma para abrir dos opcionales en un solo guard let, y la cadena equipo.capitan?.camiseta?.texto se corta en el primer nil y da un solo String?.
Paso · presentar, dorsalDesdeTexto y los tests
Objetivo: presentar(_:) con guard let, y dorsalDesdeTexto(_:), que convierte un texto en un dorsal si puede. Al terminar, xcrun swift run torneo-cli imprime al final Ana (#9), Luis (-), Marta (#1), Sin jugador y cuatro líneas con la conversión de textos.
1 · Abre Jugador.swift. Abre una Terminal y escribe cd ~/Marcador-Swift/Torneo. En el Project navigator de Xcode, despliega Sources → Torneo y abre Jugador.swift, el que escribiste en Opcionales.
2 · Dos funciones fuera del tipo. Las dos van fuera del struct, y la primera línea del fichero es el import Foundation.
Intenta escribirlo tú: importa Foundation en la primera línea del fichero; y, debajo del struct (fuera de sus llaves), dos funciones públicas que reciben su dato sin etiqueta: presentar, que recibe un jugador que puede faltar y devuelve «Ana (#9)» o, si no hay jugador, «Sin jugador», saliendo pronto en ese caso; y dorsalDesdeTexto, que quita los espacios de los bordes de un texto y devuelve el número, o nada si no lo es.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import Foundation // trae las utilidades de texto de Apple, como trimmingCharacters// `_` quita la etiqueta: se llama `presentar(ana)`.
// `guard let` abre el Jugador? para el resto de la función; si es nil, sale con `return`.
public func presentar(_ jugador: Jugador?) -> String {
guard let jugador else { return "Sin jugador" }
return "\(jugador.nombre) (\(jugador.dorsalOGuion()))"
}
// Quita los espacios de los bordes y convierte a Int; si no es un número, devuelve nil.
public func dorsalDesdeTexto(_ texto: String) -> Int? {
Int(texto.trimmingCharacters(in: .whitespaces))
}3 · main.swift. El bloque de abajo va al final del fichero, después del último bucle. […] con tres jugadores es una lista (el tipo de lista se explica en Colecciones y key paths); for jugador in jugadores { … } la recorre.
La lista, el for y el map con texto de reserva, con los socios:
// Una lista entre corchetes, con una coma tras cada elemento.
let socios = [
Socio(nombre: "Pepa", cuota: 30),
Socio(nombre: "Tomás", cuota: nil), // sin cuota: nil a propósito
]
for socio in socios { print(saludar(socio)) } // Hola, Pepa / Hola, Tomás
for texto in ["5", "cinco"] {
// Si hay edad, String($0) la pasa a texto; si es nil, ?? pone el de reserva.
let edad = edadDesdeTexto(texto).map { String($0) } ?? "no es una edad"
print("\(texto) -> \(edad)") // 5 -> 5 / cinco -> no es una edad
}Intenta escribirlo tú: una lista de tres jugadores, jugadores (Ana con el 9, Luis sin dorsal y Marta con el 1); preséntalos uno a uno, presenta también la ausencia de jugador, y convierte en dorsales los textos «10», « 7 », «diez» y el texto vacío, con «no es un dorsal» cuando no se pueda. Lo que debes ver al final de la salida:
Ana (#9)
Luis (-)
Marta (#1)
Sin jugador
«10» -> 10
« 7 » -> 7
«diez» -> no es un dorsal
«» -> no es un dorsalIntenta primero. Cuando termines, compara con la solución.Mostrar el código
// Una lista de tres jugadores. Luis no lleva dorsal.
let jugadores = [
Jugador(nombre: "Ana", dorsal: 9),
Jugador(nombre: "Luis"),
Jugador(nombre: "Marta", dorsal: 1),
]
for jugador in jugadores { print(presentar(jugador)) }
print(presentar(nil)) // "Sin jugador": le pasamos nil a propósito
// Cuatro textos: dos son números (uno con espacios) y dos no.
for texto in ["10", " 7 ", "diez", ""] {
// `.map { String($0) }`: si hay dorsal, lo convierte en texto; si no, nil.
// `?? "no es un dorsal"` da el texto de reserva.
let dorsal = dorsalDesdeTexto(texto).map { String($0) } ?? "no es un dorsal"
print("«\(texto)» -> \(dorsal)")
}Compila y ejecuta desde ~/Marcador-Swift/Torneo: xcrun swift build y xcrun swift run torneo-cli. Al final de la salida deben verse Ana (#9), Luis (-), Marta (#1), Sin jugador y las cuatro líneas de los textos.
4 · Los tests. Los siete tests de JugadorTests.swift no los escribes: son más comprobaciones del mismo tipo que las de PuntosTests, y lo que enseñan es el código de Jugador, que sí escribes tú. Para ese tipo de fichero hay un botón, Copiar fichero, que copia el fichero ya escrito al portapapeles (explicado con calma en Clases y objetos); no escribe nada en tu proyecto. El orden es siempre el mismo: crear (o abrir) el fichero, pulsar el botón, seleccionar todo y pegar.
- Crea
Tests/TorneoTests/JugadorTests.swift: en Xcode, clic derecho sobreTests → TorneoTests→ New File from Template… → Swift File → Next, escribeJugadorTestsy pulsa Create. O, en la Terminal,touch Tests/TorneoTests/JugadorTests.swift; el fichero aparece en el Project navigator, y un clic sobre su nombre lo abre en el editor. - Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
- Haz clic dentro del fichero, selecciona todo con Cmd+A y pega con Cmd+V.
(Atajo de Terminal: tras pulsar el botón, pbpaste > Tests/TorneoTests/JugadorTests.swift escribe el portapapeles en el fichero; pbpaste es «pegar» desde la Terminal.)
Uno de ellos enseña algo nuevo: compara con nil sin abrir el opcional.
@Test func dorsalDesdeTextoDevuelveNilSiNoEsNumero() {
#expect(dorsalDesdeTexto("10") == 10)
#expect(dorsalDesdeTexto(" 7 ") == 7)
#expect(dorsalDesdeTexto("diez") == nil)
#expect(dorsalDesdeTexto("") == nil)
}== 10 compara un Int? con un Int: Swift lo permite y da false si el opcional es nil. Y == nil pregunta si está vacío.
Punto de control
- Desde
~/Marcador-Swift/Torneo,xcrun swift buildtermina conBuild complete!. - Desde
~/Marcador-Swift/Torneo,xcrun swift run torneo-clitermina conAna (#9),Luis (-),Marta (#1),Sin jugador,«10» -> 10,« 7 » -> 7,«diez» -> no es un dorsaly«» -> no es un dorsal. - Desde
~/Marcador-Swift/Torneo,xcrun swift testtermina conTest run with 11 tests in 2 suites passed(la líneaExecuted 0 testsde XCTest se ignora, como en la lección anterior): los 4 dePuntosTestsy los 7 deJugadorTestsque copiaste. - Sabes decir por qué
guard letacaba enreturny en qué se parece aif (jugador == null) returnde Kotlin.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.¿Qué hace
guard let jugador else { return "Sin jugador" }al principio de una función?2.¿Qué devuelve
Int("diez")y de qué tipo es el resultado?3.En
guard let socio, let cuota = socio.cuota else { return "Sin cuota" }, ¿cuándo se ejecuta elelse?4.Escribes
texto.trimmingCharacters(in: .whitespaces)sinimport Foundation. ¿Qué pasa?