wandres.dev
VALOR Y REFERENCIA · la semántica que define Swift

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.

⏱ 16 min

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.

🎯 Al terminar esta lección sabrás
  • Explicar por qué un método de struct que escribe en sus propiedades exige mutating.
  • Describir self como parámetro implícito y qué cambia al marcarlo mutating.
  • Predecir qué compila con let y qué con var, y en qué línea aparece el error.
  • Manejar los casos límite: reasignar self, mutating en protocolos y nonmutating 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
    }
}
Swift invirtió el valor por defecto: la mutación es lo que se declara

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.

📝
Lo esencial de mutating

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.

⚔️ Haz visible la firma
  1. Escribe una struct con un método que modifique una propiedad y observa el error exacto antes de añadir mutating.
  2. Ofrece la pareja completa: ordenar mutante y ordenado que devuelve, con la convención de nombres estándar.
  3. Modela un semáforo con enum y avanza de estado reasignando self.
  4. Define un protocolo con un requisito sin mutating e intenta conformar una estructura que necesite escribir: lee el error.
  5. Implementa una propiedad con nonmutating set que escriba en un objeto compartido y llámala sobre una constante.