defer: ejecutar al salir del ámbito
Qué garantiza defer, por qué los bloques se ejecutan en orden inverso, sus usos legítimos para cerrar recursos y restaurar invariantes, y las trampas de captura y de ámbito.
defer responde a una pregunta que todo lenguaje con salidas múltiples tiene que contestar: si una función puede terminar por cinco sitios distintos, ¿cómo garantizas que la limpieza ocurra en los cinco? La respuesta perezosa es repetir el cierre antes de cada return, y funciona hasta que alguien añade el sexto. defer invierte el problema: escribes la limpieza junto a la apertura, una sola vez, y el lenguaje se encarga de ejecutarla salgas por donde salgas.
- Precisar cuándo se ejecuta un bloque
defery qué salidas cubre. - Explicar por qué varios
deferse ejecutan en orden inverso al de declaración. - Reconocer los usos legítimos: recursos, invariantes y estado temporal.
- Evitar las trampas de captura de valores y de ámbito dentro de bucles.
La garantía: se ejecuta al salir, siempre
Un bloque defer no se ejecuta donde lo escribes: se ejecuta cuando el flujo abandona el ámbito que lo contiene, y da igual cómo lo abandone. Retorno normal, retorno temprano de un guard, error propagado con throw, break que sale de un bucle: en todos los casos el bloque corre antes de que el ámbito desaparezca.
func leerConfiguracion(desde ruta: String) throws -> [String: String] {
let manejador = try FileHandle(forReadingFrom: URL(filePath: ruta))
defer { try? manejador.close() } // se cierra pase lo que pase
guard let datos = try manejador.readToEnd() else {
throw ConfigError.vacio // aqui tambien cierra
}
return try parsear(datos) // y aqui, incluso si parsear lanza
}
La ganancia clave es de proximidad: la línea que abre el recurso y la que lo cierra son vecinas. El lector no necesita recorrer la función entera comprobando que todas las salidas limpian, ni el autor futuro tiene que acordarse de nada cuando añada un return en medio. La corrección deja de depender de la disciplina.
defer se ata al bloque de llaves donde está escrito, no a la función. Uno dentro de un if, de un for o de un do se dispara al salir de ese bloque, no al final del método. Es una precisión que sorprende la primera vez y que resulta muy útil después: puedes acotar la vida de una restauración al fragmento exacto que la necesita.
El orden inverso es la única opción sensata
Cuando hay varios defer en el mismo ámbito, se ejecutan del último declarado al primero. Parece caprichoso hasta que se piensa en términos de recursos anidados:
func operacion() {
abrirTransaccion()
defer { cerrarTransaccion() } // 3.º en ejecutarse
let archivo = abrirArchivo()
defer { archivo.cerrar() } // 2.º en ejecutarse
bloquear(mutex)
defer { desbloquear(mutex) } // 1.º en ejecutarse
trabajar()
}
Si se ejecutaran en orden de declaración, cerraríamos la transacción mientras el archivo sigue abierto y el mutex tomado: se desmontaría el andamio empezando por la base. El orden inverso es el desmontaje correcto de una pila, el mismo principio por el que un destructor libera lo último adquirido primero. Adquisición hacia dentro, liberación hacia fuera.
flowchart TD A[Entrar al ambito] --> B[Adquirir recurso 1 y registrar defer 1] B --> C[Adquirir recurso 2 y registrar defer 2] C --> D[Adquirir recurso 3 y registrar defer 3] D --> E[Trabajo o salida temprana o error] E --> F[Ejecutar defer 3] F --> G[Ejecutar defer 2] G --> H[Ejecutar defer 1] H --> I[Salir del ambito]
Usos legítimos y usos que no lo son
defer brilla cuando hay una acción de apertura con una contraparte obligatoria de cierre. Fuera de ese patrón, casi siempre estorba.
Cerrar recursos
Ficheros, sockets, contextos gráficos, transacciones de base de datos, cerrojos. Todo lo que se adquiere y debe devolverse.
Restaurar invariantes
Poner una bandera a cierto al entrar y devolverla a falso al salir, retirar un observador, restaurar un ajuste global que se tocó de forma temporal.
final class Cargador {
private var estaCargando = false
func cargar() async throws -> [Articulo] {
estaCargando = true
defer { estaCargando = false } // se restaura incluso si algo lanza
return try await servicio.pedirArticulos()
}
}
Lo que no debe hacerse es usar defer como mecanismo de control de flujo o para ordenar lógica de negocio. Un defer que muta el valor que se va a devolver, o que decide algo importante, es una acción a distancia: el lector ve un return y no sospecha que aún queda código por correr. La regla práctica es que un defer debe ser corto, sin condiciones y sin sorpresas; si no cabe en dos o tres líneas, extráelo a una función con nombre y llama a esa función desde el defer.
El ámbito manda: bucles, bloques y capturas
La confusión más frecuente no es sobre qué hace defer, sino sobre cuándo lo hace. El bloque se ata a las llaves más cercanas que lo envuelven, de modo que la misma línea cambia de significado según dónde la coloques:
for fila in filas {
defer { contador += 1 } // se ejecuta al final de CADA vuelta
guard fila.esValida else { continue }
procesar(fila)
}
do {
let cerrojo = tomar(mutex)
defer { soltar(cerrojo) } // se suelta al cerrar este do, no al final del metodo
seccionCritica()
}
// aqui el mutex ya esta libre
La segunda trampa es la captura. El bloque no fotografía los valores al declararse: lee las variables cuando se ejecuta, es decir, al salir del ámbito. Por eso el ejemplo siguiente imprime el valor final y no el inicial, un detalle que sorprende a quien espera el comportamiento de un argumento pasado por valor:
var i = 0
defer { print(i) } // imprime 10, no 0
i = 10
La tercera es que un defer no puede propagar errores hacia fuera ni retornar: no admite try sin más ni return, porque el ámbito ya se está desmontando y no hay nadie a quien entregarle el resultado. De ahí el try? del primer ejemplo. Y un defer colocado tras un return nunca llega a registrarse, así que jamás corre; conviene declararlos siempre lo antes posible, pegados a la adquisición que compensan.
Un defer debe ser corto, incondicional y sin sorpresas. Si necesita ramas, comprobaciones o más de tres líneas, extrae el trabajo a una función con nombre y limita el bloque a llamarla. Un defer largo escondido al principio de un método es exactamente el tipo de acción a distancia que la construcción pretendía eliminar.
Detrás de defer hay una tesis sobre dónde debe vivir la garantía de limpieza. En C esa garantía vive en la cabeza de quien escribe, y de ahí nacen el goto cleanup y décadas de fugas de recursos por un return añadido a las prisas. C++ la trasladó al sistema de tipos con RAII, atándola al destructor de un objeto: potentísimo, pero exige inventar un tipo envoltorio para cada recurso, aunque la limpieza sea una sola línea usada una sola vez. Java la trasladó a la sintaxis con try-finally y luego con try-with-resources, al precio de un bloque que sangra el trabajo real y separa apertura de cierre por todo el cuerpo. defer elige un punto intermedio deliberado: la garantía la impone el lenguaje, pero la unidad de agrupación no es un tipo ni un bloque envolvente, sino la vecindad textual entre adquirir y liberar. Ese detalle es el que hace que el patrón escale, porque el coste de añadir una limpieza correcta es exactamente una línea junto a la que la provocó, y ningún refactor posterior puede separarlas sin que salte a la vista. La lección general trasciende a Swift: cuando una corrección depende de que un humano recuerde algo lejano en el tiempo o en el espacio del código, la solución no es documentarlo mejor ni revisarlo con más cuidado, sino cambiar la estructura para que recordar deje de ser necesario. defer es esa idea reducida a cinco letras.
- Escribe una función con tres
deferque impriman uno, dos y tres, y verifica en consola el orden real de ejecución. - Añade un
guardcon salida temprana antes de losdefery otro después; observa cuáles se ejecutan en cada caso. - Mete un
deferdentro de un bucleforde cinco vueltas y luego muévelo justo antes del bucle. Explica la diferencia en el número de ejecuciones. - Declara
var i = 0, escribedefer { print(i) }, incrementaia diez y comprueba qué se imprime al salir. - Reescribe una función que cierre un recurso antes de cada uno de sus tres
return, usando un únicodefer, y compara cuántos puntos de fallo tenía cada versión.