wandres.dev
SINTAXIS Y CONTROL · la gramática completa

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.

⏱ 16 min

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.

🎯 Al terminar esta lección sabrás
  • Entender por qué la condición de if exige Bool estricto y prescinde de paréntesis.
  • Dominar la exhaustividad del switch y el repertorio de patrones de cada caso.
  • Filtrar casos con where sabiendo qué le cuesta al compilador.
  • Usar if y switch como expresiones para inicializar constantes sin var temporales.

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.

ℹ️
El opcional no se cuela por la puerta de atras

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")
}
💡
Cuidado con el default prematuro

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.

La exhaustividad es una demostracion, no una comodidad

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.

⚔️ Piensa en patrones, no en cadenas de if
  1. Modela un enum Respuesta con exito(codigo: Int), fallo(codigo: Int, motivo: String) y sinRed, y escribe un switch exhaustivo sin usar default.
  2. Añade una rama con where que trate aparte los códigos de éxito mayores que 200, y comprueba qué error aparece si eliminas el caso de respaldo.
  3. Reescribe el switch como expresión asignada a un let de tipo String.
  4. Haz un switch sobre la tupla (Bool, Bool) que cubra las cuatro combinaciones sin default, y luego añade un caso quinto para ver cómo el compilador lo marca como inalcanzable.
  5. Convierte una cadena de tres if con var temporal en un if expresión con else obligatorio, y explica qué error impedía compilar antes.