Opcionales
Al terminar: Usar ?, !, if let, guard let, ?? y optional chaining, y compararlos con la nulabilidad de Kotlin.
Al terminar tendrás a Jugador en Swift, gemelo del de Kotlin: un struct con un dorsal: Int? porque en el fútbol amateur hay jugadores sin dorsal. Su dorsalOGuion() devolverá #9 o -, y sabrás qué mensaje da Swift cuando un nil se cuela donde no debía, que es distinto al de Kotlin.
Importa porque los opcionales están en todas partes: cualquier conversión que puede fallar, cualquier búsqueda en una colección y casi toda API de Apple devuelven uno. Y porque el fallo más caro de esta lección, ! sobre un nil, no se puede atrapar.
Temas: ?, !, if let, ??, map y optional chaining. guard let y la conversión de textos en números llegan en guard let y textos que no son números.
Un tipo que admite «nada»
Un Int nunca es nil. Un Int? es o un Int o nil, y el compilador no te deja usarlo como número hasta que decidas qué hacer con el caso vacío. Por dentro, Int? es un tipo llamado Optional con dos posibilidades (.none y .some(valor)); no hace falta usar esos nombres para trabajar con él.
| Herramienta | Qué hace | Si el valor es nil |
|---|---|---|
dorsal: Int? | El tipo admite nil | — |
if let numero = dorsal { … } | Abre el opcional; dentro, numero es un Int | No entra en el bloque |
dorsal ?? 0 | Valor de reserva | Usa el de la derecha |
camiseta?.texto | Optional chaining: sigue la cadena si hay valor (camiseta es un Camiseta?; el ejemplo de abajo) | Todo el resultado es nil |
dorsal! | Afirma «no es nil» | Detiene el programa |
Un detalle de sintaxis: if let dorsal { … } (sin = dorsal) es la forma corta: crea una constante con el mismo nombre. Hay una tercera forma de abrir un opcional, guard let, que sale de la función si falta el valor; la verás en la lección siguiente.
Optional chaining
?. es el equivalente del ?. de Kotlin. El código de Jugador de esta lección no lo usa (usa map y un ternario, como verás en el paso): el ?. se ve aquí, con un ejemplo que puedes ejecutar en tu Playground de Xcode, el que creaste en Tu primer paquete de Swift. El resultado de una cadena con ?. siempre es un opcional, aunque lo que consultes no lo sea: por eso el último print de abajo, el de .count, imprime Optional(2) y no 2.
El ejemplo usa tres cosas que se ven a fondo más adelante. struct es un tipo que agrupa datos y funciones (struct frente a class). var texto: String { … } es una propiedad calculada: un valor que se calcula cada vez que lo pides, como una función que se llama sin paréntesis. Y Camiseta(numero: 9) funciona sin que escribas ningún inicializador (el código que crea un valor del tipo): Swift crea uno automático para un struct, con un parámetro por cada propiedad.
struct Camiseta {
let numero: Int
var texto: String { "#\(numero)" } // una propiedad que se calcula al pedirla
}
struct Futbolista {
let nombre: String
let camiseta: Camiseta?
}
let ana = Futbolista(nombre: "Ana", camiseta: Camiseta(numero: 9))
let luis = Futbolista(nombre: "Luis", camiseta: nil)
print(ana.camiseta?.texto as Any) // Optional("#9")
print(luis.camiseta?.texto as Any) // nil
print(luis.camiseta?.texto ?? "-") // -
print(ana.camiseta?.texto.count as Any) // Optional(2)
let sinValor: Int? = nil
if let n = sinValor { print("n = \(n)") } else { print("no hay valor") }Aquí el tipo se llama Futbolista para no chocar con el Jugador del paso. as Any le dice a print que sabes que imprimes un opcional; sin él, el compilador avisa. Al ejecutarlo se imprime Optional("#9"), nil, -, Optional(2) y no hay valor.
map y ??: abrir y dar un valor de reserva en una línea
Dos piezas más que usa el ejercicio. map sobre un opcional aplica una función solo si hay valor y devuelve otro opcional; si es nil, no la ejecuta y devuelve nil. La función se escribe entre llaves, { … }: es una closure, un trozo de código que se pasa como si fuera un valor (Closures la cuenta a fondo), y $0 es el primer dato que recibe, aquí el valor ya abierto. Detrás, ?? pone el valor de reserva cuando lo de la izquierda es nil.
let entradas: Int? = 3
let sinEntradas: Int? = nil
// Con valor: map ejecuta la closure con $0 = 3 y da Optional("3 entradas").
// ?? solo se usa si el resultado es nil, así que aquí no hace nada.
print(entradas.map { "\($0) entradas" } ?? "ninguna")
// Sin valor: map no ejecuta nada y da nil; entonces ?? pone "ninguna".
print(sinEntradas.map { "\($0) entradas" } ?? "ninguna")La consola enseña 3 entradas y ninguna.
Kotlin frente a Swift
| Kotlin (tu código de Kotlin) | Swift |
|---|---|
val dorsal: Int? | let dorsal: Int? |
dorsal?.let { "#$it" } ?: "-" | dorsal.map { "#\($0)" } ?? "-" |
if (dorsal != null) { … dorsal … } con smart cast | if let numero = dorsal { … numero … } con un nombre nuevo |
a ?: "x" | a ?? "x" |
a?.length | a?.count |
dorsal!! lanza NullPointerException | dorsal! detiene el programa: no se atrapa |
nombre.takeIf { it.isNotBlank() }?.length | nombre.isEmpty ? nil : nombre.count |
El último es una diferencia de comportamiento: isNotBlank de Kotlin descarta también un nombre de solo espacios, y isEmpty de Swift solo descarta el texto vacío (" ".isEmpty es false).
Reto · Ejercicio: el descuento que puede faltar
En tu Playground, escribe dos funciones. textoDescuento recibe un descuento que puede faltar, con la etiqueta descuento, y devuelve «-20 %» si es 20, o «sin descuento» si falta, en una sola expresión. precioFinal recibe un precio y un descuento que puede faltar, y devuelve el precio con el descuento aplicado (el porcentaje sobre el precio); si falta el descuento, devuelve el precio tal cual. Prueba las dos con 20 y sin descuento, con un precio de 100. Lo que debe salir:
-20 %
sin descuento
80
100Ver solución
// map: si hay descuento, lo convierte en texto; ?? pone el de reserva si falta.
func textoDescuento(descuento: Int?) -> String {
descuento.map { "-\($0) %" } ?? "sin descuento"
}
// if let: dentro del bloque, porcentaje es un Int.
func precioFinal(precio: Int, descuento: Int?) -> Int {
if let porcentaje = descuento {
return precio - precio * porcentaje / 100
}
return precio
}
print(textoDescuento(descuento: 20))
print(textoDescuento(descuento: nil))
print(precioFinal(precio: 100, descuento: 20))
print(precioFinal(precio: 100, descuento: nil))map sobre un opcional aplica la función solo si hay valor, y ?? pone el texto de reserva cuando no lo hay. Para el precio hace falta el valor como número, así que if let lo abre y, si falta, se devuelve el precio sin tocar.
Paso · Jugador con dorsal opcional
Objetivo: Jugador con dorsal: Int?, dorsalOGuion(), esPortero() y tres funciones más que usan map, ??, ! y un ternario. Al terminar, xcrun swift test sigue dando 4 tests y habrás visto el fallo de ! sobre un nil.
1 · Crea Jugador.swift. Abre una Terminal y escribe cd ~/Marcador-Swift/Torneo (el paquete que creaste en Tu primer paquete de Swift). En el Project navigator de Xcode, clic derecho sobre la carpeta Sources → Torneo → New File from Template… → Swift File → Next, escribe Jugador y pulsa Create. Si el menú de tu versión de Xcode se llama distinto, hazlo desde la Terminal: touch Sources/Torneo/Jugador.swift, y Xcode lo muestra solo.
2 · El tipo. Piezas nuevas:
public struct Jugador: Equatable, Hashable { … }: unstructes un tipo con datos y funciones (struct frente a class lo cuenta a fondo). Tras los dos puntos vanEquatableyHashable, dos promesas que hacen que Swift escriba por ti la comparación con==. Escríbelo tal cual; Protocolos y extensiones explica qué son.public init(nombre: String, dorsal: Int? = nil): el inicializador, que crea el valor.selfes elJugadorque se está creando, y= niles un valor por defecto:Jugador(nombre: "Luis")no lleva dorsal. Va escrito a mano ypublicporque el automático de Swift esinternalytorneo-clino lo vería.
Las piezas juntas, con otro tipo: un socio del club que puede no pagar cuota (pruébalo en tu Playground).
// struct: un tipo con datos y funciones. Tras los dos puntos, los protocolos que cumple.
public struct Socio: Equatable, Hashable {
public let nombre: String // public: visible desde otros módulos
public let cuota: Int? // Int?: la cuota puede faltar (socio exento)
// El inicializador. `= nil` es el valor por defecto: Socio(nombre: "Tomás") no lleva cuota.
public init(nombre: String, cuota: Int? = nil) {
self.nombre = nombre // self.nombre es la propiedad; nombre, el parámetro
self.cuota = cuota
}
public func cuotaOTexto() -> String {
if let euros = cuota { // si hay cuota, euros es un Int dentro del bloque
return "\(euros) €"
}
return "exento" // aquí se llega si cuota era nil
}
}
print(Socio(nombre: "Pepa", cuota: 30).cuotaOTexto()) // 30 €
print(Socio(nombre: "Tomás").cuotaOTexto()) // exentoIntenta escribirlo tú: Jugador, un struct público que se pueda comparar con ==, con un nombre y un dorsal que puede faltar; se crea con los dos, y si no se da el dorsal, no tiene. Dos funciones: dorsalOGuion, que devuelve «#9» si tiene dorsal y «-» si no, y esPortero, que dice si su dorsal es el 1 (sin dorsal, no es portero). Las dos abren el dorsal antes de usarlo.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// Un `struct` agrupa datos y funciones. `Equatable` y `Hashable` piden a Swift
// que escriba él la comparación con `==` y lo necesario para usar Jugador en un Set.
public struct Jugador: Equatable, Hashable {
public let nombre: String
public let dorsal: Int? // `Int?`: el dorsal puede faltar
// El inicializador crea un Jugador. `= nil` es el dorsal por defecto,
// así `Jugador(nombre: "Luis")` no lleva dorsal.
// `self` es el propio Jugador que se está creando.
public init(nombre: String, dorsal: Int? = nil) {
self.nombre = nombre
self.dorsal = dorsal
}
// `if let` abre el dorsal: dentro del bloque, `numero` es un Int.
public func dorsalOGuion() -> String {
if let numero = dorsal {
return "#\(numero)"
}
return "-" // no hay dorsal
}
// El dorsal 1 es el del portero. Sin dorsal, no es portero.
public func esPortero() -> Bool {
if let numero = dorsal {
return numero == 1
}
return false
}
}3 · Tres funciones más. Piezas nuevas:
dorsal.map { "#\($0)" }y??: los de la teoría de arriba, ahora sobre el dorsal.nombre.isEmpty ? nil : nombre.count: el operador ternariocondición ? a : bdevuelveasi la condición es verdadera ybsi no. Aquí devuelvenilcon un nombre vacío.nombre.prefix(3)toma las 3 primeras letras del texto, yString(…)las convierte otra vez en unString.dorsal!endorsalForzado(): la función existe para que más abajo puedas ver el fallo de!.
Las tres, en el Socio del ejemplo anterior (van dentro de su struct):
// map: si cuota tiene valor, ejecuta la closure con ese valor como $0.
// Si es nil, no la ejecuta y da nil; entonces ?? pone "exento".
public func cuotaCorta() -> String {
cuota.map { "\($0) €" } ?? "exento"
}
// !: "te juro que hay valor". Con un socio exento, el programa se detiene.
public func cuotaForzada() -> Int {
cuota!
}
// condición ? a : b. Con más de 4 letras, las 3 primeras; si no, nil.
public func apodo() -> String? {
nombre.count > 4 ? String(nombre.prefix(3)) : nil
}Con Socio(nombre: "Tomás"), cuotaCorta() da exento y apodo() da Optional("Tom"); con Socio(nombre: "Pepa", cuota: 30), 30 € y nil.
Intenta escribirlo tú, dentro del struct, debajo de esPortero(): dorsalOGuionCorto, que da lo mismo que dorsalOGuion pero en una sola expresión, transformando el dorsal si existe y con un texto de reserva si no; dorsalForzado, que da el dorsal dando por hecho que existe; y longitudDelNombre, que da nada si el nombre está vacío y su longitud si no.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// `map` sobre un opcional: si hay valor, aplica la closure (`$0` es ese valor)
// y devuelve un opcional; si no, devuelve nil. `?? "-"` pone "-" cuando es nil.
public func dorsalOGuionCorto() -> String {
dorsal.map { "#\($0)" } ?? "-"
}
// `!` afirma "aquí hay valor". Si no lo hay, el programa se detiene.
public func dorsalForzado() -> Int {
dorsal!
}
// Ternario `condición ? a : b`: nil con un nombre vacío, su longitud si no.
public func longitudDelNombre() -> Int? {
nombre.isEmpty ? nil : nombre.count
}Compila con xcrun swift build desde ~/Marcador-Swift/Torneo: debe terminar con Build complete!.
Error típico: ! como atajo
Ahora que Jugador existe, puedes ver el fallo más caro de los opcionales. En los ejemplos siguientes, Jugador(nombre: "Luis") es un jugador sin dorsal, y dorsalForzado() es la función que acabas de escribir, la que aplica ! al dorsal.
let luis = Jugador(nombre: "Luis")
let dorsal: Int = luis.dorsalForzado() // compila, y se detiene al ejecutar
print("Dorsal: \(dorsal)")let luis = Jugador(nombre: "Luis")
print("Dorsal: \(luis.dorsalOGuion())") // Dorsal: -Falla porque ! no comprueba nada: promete al compilador que hay un valor y traslada el error del momento de compilar al de ejecutar. Úsalo solo donde un nil sería un fallo de tu programa y no un dato posible.
Provócalo: añade print(Jugador(nombre: "Luis").dorsalForzado()) al final de main.swift y ejecuta xcrun swift run torneo-cli desde ~/Marcador-Swift/Torneo.
Punto de control
Sources/Torneo/Jugador.swifttieneJugadorcondorsalOGuion,esPortero,dorsalOGuionCorto,dorsalForzadoylongitudDelNombre.- Desde
~/Marcador-Swift/Torneo,xcrun swift buildtermina conBuild complete!. xcrun swift testtermina conTest run with 4 tests in 1 suite passed(la líneaExecuted 0 testsde XCTest se ignora): los 4 dePuntosTests.- Provocaste el
Fatal error: Unexpectedly found nil while unwrapping an Optional valuecondorsalForzado()y borraste la línea demain.swift. - Sabes decir qué hace
if let, qué hace??y por qué!sobre unnilno se puede atrapar concatch.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Si
let luis = Jugador(nombre: "Luis")no tiene dorsal, ¿qué pasa al ejecutarluis.dorsalForzado()(que hacedorsal!)?2.¿Qué tipo tiene
ana.camiseta?.texto.count, sicamisetaes unCamiseta?ytextoes unString?3.¿Cuál es el equivalente en Swift de
a ?: "x"de Kotlin?4.Con
let dorsal: Int? = 9, ¿por qué se escribeprint(dorsal as Any)y noprint(dorsal)?5.Con
dorsal: Int?, ¿qué devuelvedorsal.map { "#($0)" }sidorsalvalenil?