Saltar al contenido
K
Swift: el lenguajePaso a paso28 min Intermedio

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 (como if 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 el else.
  • _ antes de un parámetro, como en saludar(_ socio: Socio?), quita la etiqueta al llamar: saludar(pepa) en vez de saludar(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:

ejemplo mínimo: _, guard let, la coma e Int(texto) (con comentarios)
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)
nil

Kotlin frente a Swift

Kotlin (tu código de Kotlin)Swift
if (jugador == null) return … y a partir de ahí jugador es no nuloguard 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:

  1. conGuard, que abre los tres niveles uno detrás de otro, con una salida temprana por nivel.
  2. conCadena, que recorre la cadena de opcionales de una vez dentro de una sola salida temprana. Pista: mira cómo cobrar abre 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 dorsal
Ver solución
solución del ejercicio
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 dorsal

La 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.

Marcador-Swift/Torneo/
Sources/
Torneo/Jugador.swiftse amplía
torneo-cli/main.swiftse modifica
Tests/TorneoTests/JugadorTests.swiftse crea y se rellena con «Copiar fichero»
Un fichero nuevo y dos que cambian.

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
Jugador.swift: la primera línea del fichero
import Foundation   // trae las utilidades de texto de Apple, como trimmingCharacters
Jugador.swift: debajo del struct
// `_` 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:

ejemplo mínimo: una lista, un for y map ?? sobre el resultado
// 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 dorsal
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
main.swift: al final del fichero
// 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.

  1. Crea Tests/TorneoTests/JugadorTests.swift: en Xcode, clic derecho sobre Tests → TorneoTests → New File from Template… → Swift File → Next, escribe JugadorTests y 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.
  2. Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
  3. Haz clic dentro del fichero, selecciona todo con Cmd+A y pega con Cmd+V.
JugadorTests.swift

(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.

el test que enseña algo
    @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 build termina con Build complete!.
  • Desde ~/Marcador-Swift/Torneo, xcrun swift run torneo-cli termina con Ana (#9), Luis (-), Marta (#1), Sin jugador, «10» -> 10, « 7 » -> 7, «diez» -> no es un dorsal y «» -> no es un dorsal.
  • Desde ~/Marcador-Swift/Torneo, xcrun swift test termina con Test run with 11 tests in 2 suites passed (la línea Executed 0 tests de XCTest se ignora, como en la lección anterior): los 4 de PuntosTests y los 7 de JugadorTests que copiaste.
  • Sabes decir por qué guard let acaba en return y en qué se parece a if (jugador == null) return de Kotlin.

Comprueba lo que entendiste

4 preguntas · necesitas 3 para completar la lección

  1. 1.¿Qué hace guard let jugador else { return "Sin jugador" } al principio de una función?

  2. 2.¿Qué devuelve Int("diez") y de qué tipo es el resultado?

  3. 3.En guard let socio, let cuota = socio.cuota else { return "Sin cuota" }, ¿cuándo se ejecuta el else?

  4. 4.Escribes texto.trimmingCharacters(in: .whitespaces) sin import Foundation. ¿Qué pasa?

Faltan 4 de 4