Parámetros inout en Swift: mutación controlada y diseño de API
Arturo Rivas Arias
🔒 Swift está diseñado para favorecer la inmutabilidad por defecto. Los parámetros de una función son constantes, lo que evita efectos secundarios accidentales y simplifica el razonamiento sobre el código. Pero hay situaciones en las que necesitas que una función modifique directamente el valor que recibe, no que devuelva una copia transformada. Para eso existe inout.
🔄 El mecanismo interno de inout no es paso por referencia al estilo de C o C++, aunque lo parezca. Swift implementa un modelo llamado copy-in copy-out: al llamar a la función, el valor se copia en un almacenamiento local; la función opera sobre esa copia; y al retornar, el valor modificado se escribe de vuelta en la variable original. El compilador puede optimizar este proceso usando directamente la dirección de memoria original cuando es seguro hacerlo, pero el contrato semántico sigue siendo el de copia.
⚙️ Entender esto tiene consecuencias prácticas importantes. Si pasas la misma variable dos veces como argumento inout a una función, Swift lo rechaza en compilación porque las dos copias locales escribirían de vuelta al mismo destino con resultados indeterminados. Esta restricción no es arbitraria: es una consecuencia directa del modelo de seguridad de memoria del lenguaje.
var contador = 0
func incrementar(_ a: inout Int, _ b: inout Int) {
a += 1
b += 1
}
// ❌ Errores en compilación:
// - Overlapping accesses to 'contador', but modification requires exclusive access; consider copying to a local variable
// - Inout arguments are not allowed to alias each other
incrementar(&contador, &contador)
🎨 El & en la llamada no es un operador de dirección de memoria como en C. Es una señal explícita para el desarrollador del código: este valor será mutado. Esa intencionalidad es parte del diseño. Cuando lees procesar(&configuracion), sabes sin entrar en la implementación que configuracion puede salir de ahí con un valor distinto.
🧩 La relación entre inout y mutating en structs es más estrecha de lo que parece. Un método marcado como mutating en un struct es equivalente a tener self como parámetro inout. El compilador los trata de forma análoga: en ambos casos, la mutación es local y se escribe de vuelta al almacenamiento original al terminar. Esta simetría explica por qué puedes combinarlos de forma natural.
struct Configuracion {
var intentosMaximos: Int
var timeoutSegundos: Double
mutating func aplicarPerfil(_ perfil: PerfilRed) {
intentosMaximos = perfil.reintentos
timeoutSegundos = perfil.timeout
}
}
enum PerfilRed {
case lento, estandar, rapido
var reintentos: Int {
switch self {
case .lento: return 5
case .estandar: return 3
case .rapido: return 1
}
}
var timeout: Double {
switch self {
case .lento: return 30
case .estandar: return 10
case .rapido: return 3
}
}
}
func ajustar(_ config: inout Configuracion, segun perfil: PerfilRed) {
config.aplicarPerfil(perfil)
}
var config = Configuracion(intentosMaximos: 3, timeoutSegundos: 10)
ajustar(&config, segun: .lento)
// config.intentosMaximos == 5, config.timeoutSegundos == 30
🎯 La decisión entre usar inout o devolver un valor nuevo no es trivial y tiene peso en el diseño de la API. Devolver un valor nuevo expresa transformación: entra algo, sale algo distinto, el original no cambia. inout expresa mutación: el propio valor existente se actualiza. Cuando la mutación es el objetivo, inout comunica esa intención mejor que una asignación posterior.
// Expresa transformación: el original no se toca
func normalizando(_ texto: String) -> String {
texto.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
// Expresa mutación directa sobre el estado existente
func normalizar(_ texto: inout String) {
texto = texto.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
La primera forma permite composición: puedes encadenarla con otras transformaciones. La segunda es más directa cuando trabajas con estructuras de estado que se actualizan en múltiples pasos y no tiene sentido crear copias intermedias.
⚠️ inout tiene restricciones importantes que el compilador impone para garantizar la seguridad. No puedes capturar un parámetro inout en un closure que escape del ámbito de la función, porque la escritura de vuelta ocurre cuando la función retorna, y en ese momento el closure podría ejecutarse más tarde sobre un almacenamiento que ya no existe o que ha cambiado.
// ❌ No permitido: Escaping closure captures 'inout' parameter 'valor'
func registrarCallback(_ valor: inout Int) {
DispatchQueue.main.async {
valor += 1 // Error: escaping closure captures inout parameter
}
}
// ✅ Si necesitas mutación asíncrona, usa un actor o pasa el valor por otro medio
actor Contador {
private var valor: Int = 0
func incrementar() {
valor += 1
}
}
🔢 Un caso donde inout brilla especialmente es en operaciones sobre colecciones que deben modificarse en el mis ámbito, evitando copias intermedias costosas. Las colecciones de Swift ya usan copy-on-write, pero pasar un array grande como inout permite que el optimizador trabaje directamente sobre el almacenamiento original cuando las condiciones lo permiten.
func eliminarDuplicados(_ elementos: inout [String]) {
var vistos = Set<String>()
elementos = elementos.filter { vistos.insert($0).inserted }
}
var etiquetas = ["swift", "ios", "swift", "apple", "ios", "xcode"]
eliminarDuplicados(&etiquetas)
// etiquetas == ["swift", "ios", "apple", "xcode"]
🚫 El error más habitual con inout es usarlo para ahorrar una línea de código o como atajo cuando un valor de retorno sería más claro. Si la función conceptualmente calcula algo a partir de una entrada, devolver el resultado es siempre la opción más legible y escalable. inout solo justifica su presencia cuando la mutación del valor existente es exactamente el contrato que quieres expresar, no cuando es un detalle de implementación que el llamador no debería ver.
👨💻 Dominar inout es, en el fondo, entender qué contrato o API estableces con quien usa tu código. La sintaxis es mínima, pero el mensaje que transmite es concreto: este argumento entra, cambia, y sale modificado. Cuando ese es exactamente el comportamiento que necesitas, inout lo comunica con una claridad que ningún comentario puede igualar.