if, switch y las expresiones
El switch exhaustivo de Swift, los patrones que admite cada caso, la cláusula where para afinar sin romper la exhaustividad, y el giro que convierte if y switch en expresiones que producen valor.
El control de flujo de Swift parece familiar si vienes de C, pero cada decisión de diseño está tomada en contra de una clase entera de errores: la condición exige un Bool verdadero y no un entero disfrazado, el switch obliga a cubrir todos los casos y jamás cae de uno a otro por accidente, y desde Swift 5.9 tanto if como switch pueden ser expresiones que producen un valor. Lo que en otros lenguajes es una sentencia que muta variables, aquí es una construcción que devuelve.
- Entender por qué la condición de
ifexigeBoolestricto y prescinde de paréntesis. - Dominar la exhaustividad del
switchy el repertorio de patrones de cada caso. - Filtrar casos con
wheresabiendo qué le cuesta al compilador. - Usar
ifyswitchcomo expresiones para inicializar constantes sinvartemporales.
La condición sin paréntesis y sin verdad implícita
El if de Swift no lleva paréntesis alrededor de la condición y sí exige llaves aunque el cuerpo sea de una sola línea. Las dos reglas van juntas: la ausencia de paréntesis obliga a que el delimitador del cuerpo sea inequívoco, y las llaves obligatorias cierran de raíz el bug clásico del else colgante.
let temperatura = 31
if temperatura > 30 {
print("calor")
} else if temperatura > 15 {
print("templado")
} else {
print("frio")
}
La restricción más importante es otra: la condición debe ser de tipo Bool. No hay verdad implícita. Un entero distinto de cero no es cierto, un opcional con valor no es cierto, una cadena no vacía no es cierta. Escribir if lista.count no compila; hay que decir if !lista.isEmpty. Con esto desaparece toda la familia de errores del if (x = 0) de C, porque una asignación no produce un Bool que la condición pueda aceptar.
En muchos lenguajes if objeto significa a la vez comprobar existencia y usar el valor. En Swift el opcional es un tipo, no una ausencia mágica, así que la condición no lo acepta: hay que abrirlo explícitamente con if let nombre = usuario.nombre o compararlo con nil. La comprobación y el desempaquetado son un solo acto declarado, no un efecto colateral del sistema de tipos.
El switch exhaustivo y sus patrones
Un switch sobre un conjunto cerrado de valores debe cubrirlos todos. Si falta uno, el error no es un aviso: el programa no compila. Y no existe caída implícita de un caso al siguiente; cada rama termina sola, y si quieres el comportamiento contrario lo pides con fallthrough.
enum Semaforo { case rojo, ambar, verde }
func accion(_ luz: Semaforo) {
switch luz {
case .rojo: print("detente")
case .ambar: print("prepara")
case .verde: print("avanza")
}
}
Lo que va tras case no es un valor, es un patrón. Eso abre el abanico: rangos, tuplas, listas de valores separados por coma, enlace de partes con let y comprobación de tipo con is o as.
let punto = (0, 5)
switch punto {
case (0, 0):
print("origen")
case (_, 0):
print("sobre el eje x")
case (0, _):
print("sobre el eje y")
case let (x, y) where x == y:
print("en la diagonal")
default:
print("en algun otro sitio")
}
Añadir default a un switch sobre un enum propio silencia para siempre la exhaustividad: cuando mañana agregues un caso nuevo, el compilador ya no te llevará hasta este archivo. Reserva default para dominios abiertos (Int, String) y enumera los casos uno a uno cuando el dominio es tuyo y cerrado. La exhaustividad solo trabaja para ti si le dejas trabajar.
where: el filtro dentro del caso
La cláusula where añade una condición extra a un patrón que ya encajó. Sirve para partir un mismo caso en varias reacciones sin salir del switch:
enum Pedido {
case pendiente
case enviado(dias: Int)
case entregado
}
func mensaje(_ p: Pedido) -> String {
switch p {
case .enviado(let dias) where dias > 7:
return "retraso grave"
case .enviado(let dias) where dias > 3:
return "va con demora de \(dias) dias"
case .enviado:
return "en camino"
case .pendiente, .entregado:
return "sin novedad"
}
}
Hay un detalle que conviene interiorizar: el compilador no razona sobre el contenido de un where. Para él, un caso con guarda puede fallar siempre, así que no cuenta como cobertura. Por eso el ejemplo necesita el case .enviado desnudo al final; sin él, el switch no sería exhaustivo aunque a ojo humano las tres guardas agoten las posibilidades.
flowchart TD
A[Valor a analizar] --> B{Cuantas formas tiene}
B -->|Dos ramas booleanas| C[if o if expresion]
B -->|Conjunto cerrado de casos| D[switch exhaustivo]
D --> E[Patron que enlaza partes con let]
E --> F{Hace falta afinar}
F -->|Si| G[Anade where y un caso de respaldo]
F -->|No| H[Listo y verificado por el compilador]if y switch como expresiones
Desde Swift 5.9 ambas construcciones pueden aparecer donde se espera un valor. El beneficio no es cosmético: elimina la var temporal que se declaraba vacía y se rellenaba después, y con ella la posibilidad de olvidar una rama.
// Antes: variable mutable esperando a que alguien la llene
var etiqueta: String
switch codigo {
case 200: etiqueta = "ok"
case 404: etiqueta = "no encontrado"
default: etiqueta = "desconocido"
}
// Ahora: constante inicializada de una vez
let etiqueta = switch codigo {
case 200: "ok"
case 404: "no encontrado"
default: "desconocido"
}
let limite = if esPremium { 1_000 } else { 100 }
Las reglas son estrictas a propósito: cada rama debe producir un único valor, todas deben coincidir en tipo, el if usado como expresión necesita siempre su else, y el switch sigue obligado a ser exhaustivo. No es una expresión permisiva estilo operador ternario encadenado; es una construcción que solo compila cuando el conjunto de resultados está completo.
Sentencia
Ejecuta efectos y muta estado. Necesita una var previa y confía en que ninguna rama se quede sin asignar.
Expresion
Produce un valor. Permite let, exige tipos compatibles y cobertura total, y hace imposible el estado a medio inicializar.
Cuando el compilador rechaza un switch incompleto no te está regañando por descuidado: está negándose a aceptar una función que no es total. Un switch exhaustivo sobre un tipo suma es, literalmente, la prueba por casos de la lógica clásica trasladada al código; cada rama es un lema, y la exhaustividad es lo que convierte la colección de lemas en un teorema sobre todos los valores posibles. Esta es la razón profunda por la que modelar con enums y decidir con switch es tan estable frente al cambio: el día que añades un caso al tipo, no rompes el programa, sino que reabres la demostración, y el compilador te entrega la lista exacta de puntos donde falta argumento. Con if encadenados y un else final esa lista no existe: el else traga en silencio lo que no previste y el fallo aparece meses después, en producción, disfrazado de comportamiento raro. Añadir default por comodidad es exactamente eso, renunciar a la demostración a cambio de escribir tres líneas menos hoy. Y cuando además usas el switch como expresión, la garantía se refuerza: ya no solo cubres todos los casos, sino que cada uno debe entregar un valor del mismo tipo, de modo que “olvidé asignar en esa rama” deja de ser un estado que el lenguaje sepa representar.
- Modela un enum
Respuestaconexito(codigo: Int),fallo(codigo: Int, motivo: String)ysinRed, y escribe unswitchexhaustivo sin usardefault. - Añade una rama con
whereque trate aparte los códigos de éxito mayores que 200, y comprueba qué error aparece si eliminas el caso de respaldo. - Reescribe el
switchcomo expresión asignada a unletde tipoString. - Haz un
switchsobre la tupla(Bool, Bool)que cubra las cuatro combinaciones sindefault, y luego añade un caso quinto para ver cómo el compilador lo marca como inalcanzable. - Convierte una cadena de tres
ifconvartemporal en unifexpresión conelseobligatorio, y explica qué error impedía compilar antes.