mutating: la mutación como firma, no como permiso
Por qué un método que escribe en un `struct` debe declararse `mutating`, qué significa que `self` sea un parámetro implícito, y cómo eso hace que `let` y `var` sean decisiones verificables en compilación.
La palabra mutating parece burocracia: el compilador ya sabe que ese método escribe en una propiedad, ¿para qué obligarte a repetirlo? La respuesta es que no te está pidiendo una confirmación, sino una declaración pública. mutating forma parte de la firma del método, viaja hasta cada sitio de llamada y permite decidir allí —sin mirar el cuerpo— si la operación es legítima. Es la pieza que convierte let en una garantía demostrable en vez de una promesa de buena conducta.
- Explicar por qué un método de
structque escribe en sus propiedades exigemutating. - Describir
selfcomo parámetro implícito y qué cambia al marcarlomutating. - Predecir qué compila con
lety qué convar, y en qué línea aparece el error. - Manejar los casos límite: reasignar
self,mutatingen protocolos ynonmutating set.
El método recibe self, y por defecto es constante
Todo método de instancia recibe un parámetro invisible llamado self. En una estructura, ese parámetro se pasa como cualquier otro valor: por defecto es constante, así que el cuerpo no puede escribir en él ni en sus propiedades.
struct Pila {
var items: [Int] = []
// func mete(_ x: Int) { items.append(x) }
// ERROR: cannot use mutating member on immutable value: 'self' is immutable
mutating func mete(_ x: Int) { items.append(x) }
func cima() -> Int? { items.last }
}
Marcar el método mutating cambia la naturaleza de ese parámetro: self pasa a comportarse como inout. Se entrega con el valor actual, el cuerpo lo modifica y al retornar el resultado se escribe de vuelta sobre la variable que originó la llamada. De ahí se deduce todo lo demás sin necesidad de reglas nuevas.
Conviene notar que la restricción se activa por la escritura, no por el tipo del método. Leer propiedades, llamar a otros métodos no mutantes o devolver una copia modificada nunca requiere mutating. El patrón funcional es perfectamente idiomático y muchas veces preferible:
struct Vector {
var x, y: Double
func escalado(_ k: Double) -> Vector { Vector(x: x * k, y: y * k) } // devuelve
mutating func escalar(_ k: Double) { x *= k; y *= k } // muta
}
La biblioteca estándar codifica esta dualidad en su convención de nombres: participio para la versión que devuelve —sorted, reversed, appending— e imperativo para la que muta —sort, reverse, append—.
La mutabilidad la decide el llamador
Como mutating está en la firma, el compilador puede rechazar la llamada mirando únicamente la variable receptora. El error no aparece dentro del método: aparece en el sitio de uso.
let fija = Pila()
// fija.mete(1) // ERROR: cannot use mutating member on immutable value: 'fija' is a 'let' constant
var suelta = Pila()
suelta.mete(1) // OK
Esto convierte la elección entre let y var en una decisión con consecuencias reales, no en un gesto estilístico. Declarar let no significa “no pienso cambiarlo”: significa que ninguna operación capaz de cambiarlo es invocable, y el compilador lo verifica.
La misma lógica se propaga a lugares menos obvios. Un elemento de una colección constante, una propiedad de solo lectura o el valor recibido en un parámetro corriente son todos contextos inmutables, y ninguno admite métodos mutantes.
struct Caja { var pila = Pila() }
let caja = Caja()
// caja.pila.mete(1) // ERROR: la constancia es transitiva hacia dentro
func procesar(_ p: Pila) {
// p.mete(1) // ERROR: los parámetros son constantes salvo que sean inout
}
Con let
Solo métodos no mutantes y lectura de propiedades. La constancia alcanza a todo el agregado, incluidas las subestructuras anidadas.
Con var
Se admiten métodos mutating, asignación a propiedades y operadores compuestos. La escritura de vuelta ocurre sobre esta misma variable.
Reemplazar self por completo
Si self se comporta como inout, nada impide asignarle un valor entero en lugar de tocar sus campos. Es legal y a menudo más claro, sobre todo cuando el nuevo estado se calcula de golpe.
struct Racional {
var num: Int, den: Int
mutating func normalizar() {
let g = maximoComunDivisor(abs(num), abs(den))
self = Racional(num: num / g, den: den / g)
}
}
En las enumeraciones esta forma es la única posible, porque un case no tiene campos que asignar por separado. Un autómata escrito como enum avanza reasignando self:
enum Semaforo {
case rojo, ambar, verde
mutating func avanzar() {
switch self {
case .rojo: self = .verde
case .verde: self = .ambar
case .ambar: self = .rojo
}
}
}
flowchart TB m[Metodo de instancia de un struct] --> d[Por defecto self llega como constante] m --> mu[Con mutating self llega como inout] d --> l[Invocable sobre let y sobre var] mu --> v[Exige que el receptor sea var] mu --> w[Al retornar se escribe de vuelta el self modificado]
mutating en protocolos y el contrato con las clases
Un requisito de protocolo declarado mutating es una promesa débil: dice que la implementación puede necesitar mutar. Las estructuras y enumeraciones lo satisfacen con un método mutating; las clases, que nunca lo necesitan, lo satisfacen con un método normal.
protocol Reiniciable {
mutating func reiniciar()
}
struct Sesion: Reiniciable {
var pasos = 0
mutating func reiniciar() { pasos = 0 }
}
final class Cache: Reiniciable {
var datos: [String] = []
func reiniciar() { datos = [] } // sin mutating: la clase no lo precisa
}
La asimetría importa al diseñar el protocolo. Omitir mutating no es una simplificación: es una prohibición. Un requisito sin la marca no puede ser implementado por ninguna estructura que necesite escribir en sí misma, así que el protocolo queda restringido de hecho a tipos de referencia. Ante la duda, márcalo: las clases no pagan nada por ello.
El reverso existe en las propiedades calculadas. Un set es implícitamente mutante, pero puede declararse nonmutating set cuando la escritura no toca el agregado —porque va a parar a un almacenamiento externo, a un objeto compartido o a una base de datos—. Es exactamente lo que hacen los envoltorios de estado de SwiftUI para que una vista, que es un struct recibido como constante, pueda escribir.
struct Puntero {
let destino: Referencia
var valor: Int {
get { destino.valor }
nonmutating set { destino.valor = newValue } // no modifica el struct
}
}
En C++ los métodos mutan salvo que escribas const, y el resultado histórico es conocido: la corrección por const se aplica a medias, se propaga tarde y se abandona en cuanto molesta. Swift dio la vuelta al eje. El valor por defecto es el seguro —no mutar—, y lo excepcional es lo que se anota. Ese giro tiene tres consecuencias que se refuerzan entre sí. Primera: la marca no puede olvidarse, porque su ausencia produce un error inmediato en el propio cuerpo del método, no un fallo silencioso en algún llamador remoto. Segunda: como mutating vive en la firma, el análisis del sitio de llamada es puramente local; el compilador decide si x.metodo es legal sin abrir el método, y por eso let puede prometer inmutabilidad total sobre un árbol de valores arbitrariamente profundo. Y tercera: la marca convierte una propiedad del código en una propiedad del tipo, que viaja por los protocolos, se comprueba en los genéricos y sobrevive a los límites de módulo. mutating no es un permiso que pides al compilador; es información que le das, y a cambio él te devuelve una garantía que ningún lenguaje con mutación por defecto puede ofrecer.
Un método de struct recibe self como constante; mutating lo convierte en inout y obliga a que el receptor sea var. El error se detecta en el sitio de llamada porque la marca forma parte de la firma. Se puede reasignar self entero, y en las enumeraciones es la forma habitual de mutar. En un protocolo, omitir mutating prohíbe la implementación mutante y restringe el requisito a clases.
- Escribe una
structcon un método que modifique una propiedad y observa el error exacto antes de añadirmutating. - Ofrece la pareja completa:
ordenarmutante yordenadoque devuelve, con la convención de nombres estándar. - Modela un semáforo con
enumy avanza de estado reasignandoself. - Define un protocolo con un requisito sin
mutatinge intenta conformar una estructura que necesite escribir: lee el error. - Implementa una propiedad con
nonmutating setque escriba en un objeto compartido y llámala sobre una constante.