Saltar al contenido
K
Swift: el lenguajePaso a paso40 min Intermedio

mutating y copias: Partido y Torneo

Al terminar: Modelar Partido y Torneo con structs mutating y entender las copias.

Al terminar esta lección Marcador tendrá en Swift sus tres tipos del dominio: Partido como struct, y Equipo y Torneo como class. Sabrás decir por qué let y var mandan sobre un struct, qué hace mutating, qué se copia cuando un struct guarda una clase y qué es el copy-on-write. Es la segunda mitad de la pregunta de entrevista de Swift más repetida.

Importa porque un struct en un let no se puede cambiar, y porque copiar un struct que guarda una clase copia la flecha, no el objeto. Temas: mutating, let y var sobre un struct, copias y copy-on-write.

let y var mandan sobre un struct

En Kotlin, val y var los pone cada propiedad de la clase. En Swift, cuando el tipo es un struct, los pone quien lo guarda: un struct dentro de un let no se puede modificar entero. Y para cambiar un struct desde dentro de sus propios métodos hay que avisarlo con mutating.

Un método que cambia el propio struct se marca mutating (lo verás en acción en el error típico de más abajo). En una class la palabra no existe: cualquier método puede cambiar las propiedades de self (el propio objeto), porque el objeto es uno solo.

Error típico: mutar un struct guardado en un let

Creas un partido, lo guardas en un let (porque «no va a cambiar») y luego intentas registrar un gol:

con el bug: mutating sobre un let
struct Marcador {
    var goles = 0
 
    mutating func sumar() {
        goles += 1
    }
}
 
let q = Marcador()
q.sumar()

No compila. El compilador dice:

error: cannot use mutating member on immutable value: 'q' is a 'let' constant

Falla porque llamar a un método mutating cambia el valor entero, y un let lo prohíbe. La solución es declarar var q = Marcador(). Si el tipo fuera una class, el let habría compilado sin quejarse: solo protege la flecha.

Dos formas de «cambiar» un struct: devolver otro o cambiar el propio

Un struct admite dos estilos, y los dos salen en el ejercicio de más abajo. El primero devuelve un valor nuevo y deja el original como estaba; no necesita mutating, así que se puede llamar sobre un let. El segundo es un método mutating, que cambia el propio valor y solo se llama sobre una var. Para hacer una copia dentro de un método, var nuevo = self (self es el propio valor) guarda una copia en una variable que sí puedes cambiar.

ejemplo mínimo: un struct con valor por defecto, una copia nueva y mutating
struct Prestamo: Equatable {
    let titulo: String
    private(set) var dias: Int               // se lee fuera; solo se cambia dentro
 
    // dias: Int = 7 es un valor por defecto: Prestamo(titulo: "Atlas") dura 7 días.
    init(titulo: String, dias: Int = 7) {
        self.titulo = titulo
        self.dias = dias
    }
 
    // Devuelve un préstamo NUEVO con una semana más; self no cambia.
    func conProrroga() -> Prestamo {
        Prestamo(titulo: titulo, dias: dias + 7)
    }
 
    // mutating: cambia el propio struct. Solo se puede llamar sobre una var.
    mutating func prorrogar() {
        dias += 7
    }
}
 
let p = Prestamo(titulo: "Atlas")   // un préstamo de la biblioteca
let otro = p.conProrroga()       // p sigue en 7; otro tiene 14
var mismo = p                    // una copia en una var
mismo.prorrogar()                // la copia pasa a 14; p sigue en 7
print(p.dias, otro.dias, mismo.dias)   // 7 14 14

Imprime 7 14 14: p no se tocó nunca, porque conProrroga() devolvió otro valor y prorrogar() solo cambió la copia mismo.

Un struct que guarda una clase copia la flecha, no el objeto

Partido es un struct y tiene dos propiedades de tipo Equipo, que es una class. Cuando copias un Partido, cada copia trae su propio marcador, pero las dos apuntan al mismo Equipo: copiar el struct copia la flecha, no el objeto al que apunta. Lo verás en la consola del paso de más abajo, en la línea ¿Mismo equipo local en las dos copias? true. Si alguien cambiara el capitán del equipo desde una copia, la otra lo vería.

Copy-on-write: la copia que no se hace hasta que hace falta

Si cada asignación copiara una lista (un array) de mil elementos, Swift sería lento. No lo hace: copiar un array no copia los elementos. Los dos comparten la misma memoria hasta que uno de los dos cambia; en ese momento, y solo entonces, el que cambia hace su copia. Eso es copy-on-write («copiar al escribir»).

El comportamiento que ves siempre es el de una copia, y para usar arrays solo necesitas saber eso. Por eso Torneo no necesita la copia defensiva (toList()) que escribiste en Inmutabilidad y referencias: private(set) var partidos: [Partido] deja leer el array desde fuera, y lo que se lee ya es un valor independiente.

Tu Kotlin y este Swift, línea a línea

Kotlin (tu módulo :torneo)Swift (tu paquete Torneo)Diferencia
data class Partido(...)struct Partido: EquatableSe parece en la igualdad, pero el struct pide Equatable y no trae copy ni desestructuración
val / var en cada propiedadlet / var en cada propiedad y en quien guarda el valorUn struct en un let es inmutable entero
plantilla.toList() (copia defensiva)self.plantilla = plantillaEl array ya se copia al asignar

Reto · Ejercicio: tres termostatos

En tu Playground, escribe un struct, Termostato, con unos grados que empiezan en 20, que se leen desde fuera y solo cambian dentro, un método que sube un grado al propio termostato y otro que devuelve un termostato nuevo con un grado más, sin tocar el original. Guarda uno, salon, en una constante; cópialo en una variable, cocina, y súbela dos veces; y crea pasillo con el método que devuelve uno nuevo a partir de salon. Imprime los grados de los tres en una línea:

20 22 21

Después intenta subir salon directamente y lee el mensaje del compilador.

Ver solución
solución del ejercicio
struct Termostato {
    private(set) var grados = 20
 
    // mutating: cambia el propio termostato.
    mutating func subir() {
        grados += 1
    }
 
    // Devuelve un termostato nuevo; self no cambia.
    func conUnGradoMas() -> Termostato {
        var nuevo = self   // una copia
        nuevo.subir()
        return nuevo
    }
}
 
let salon = Termostato()
var cocina = salon            // copia
cocina.subir()
cocina.subir()
let pasillo = salon.conUnGradoMas()
print(salon.grados, cocina.grados, pasillo.grados)   // 20 22 21
// salon.subir()   // error: cannot use mutating member on immutable value: 'salon' is a 'let' constant

Subir salon directamente no compila, porque es un let y subir() es mutating: el compilador dice cannot use mutating member on immutable value: 'salon' is a 'let' constant.

Paso · Partido y Torneo

Objetivo. Crear Partido (struct) y Torneo (clase), y comprobar en la consola cuál se copia y cuál se comparte. Al terminar, xcrun swift run torneo-cli imprime tres líneas nuevas al final, entre ellas Antes: 0-0, después: 2-1, y xcrun swift test da 28 tests.

Torneo/Sources/Torneo/
Partido.swiftse crea
Torneo.swiftse crea
Torneo/Sources/torneo-cli/
main.swiftse modifica
Torneo/Tests/TorneoTests/
StructYClassTests.swiftse crea y se rellena con «Copiar fichero»
Dos ficheros nuevos, uno modificado y un test que se rellena con «Copiar fichero». Rutas dentro de ~/Marcador-Swift.

Crea los dos ficheros. Abre una Terminal y escribe cd ~/Marcador-Swift/Torneo. En el Project navigator de Xcode, clic derecho sobre Sources → Torneo → New File from Template… → Swift File → Next, escribe el nombre (Partido) y pulsa Create; repite con Torneo. Si el menú de tu versión de Xcode se llama distinto, hazlo desde la Terminal:

touch Sources/Torneo/Partido.swift Sources/Torneo/Torneo.swift

Comprueba que los dos quedan dentro de Sources/Torneo. Los vas a rellenar de uno en uno, y por tramos pequeños.

Tramo 1 · Partido, un struct

Piezas nuevas:

  • public struct Partido: Equatable { … }: un tipo de valor; Equatable lo da el compilador comparando todas las propiedades (por eso Equipo tuvo que ser Equatable en la lección anterior).
  • golesLocal: Int = 0 en un init: un valor por defecto, como los de Valores por defecto e inout y como dorsal: Int? = nil.
  • mutating func: método de un struct que se cambia a sí mismo. conGolLocal() y conGolVisitante() devuelven un partido nuevo, y registrarGolLocal() y registrarGolVisitante() cambian el propio partido: dos formas de lo mismo, que verás comparadas.

Las tres piezas están en el ejemplo Prestamo de la teoría: el valor por defecto de init, la función que devuelve un valor nuevo (conProrroga) y el método mutating (prorrogar). Aquí con goles en vez de días.

Intenta escribirlo tú: Partido, un struct público y comparable con un equipo local y uno visitante, y sus golesLocal y golesVisitante, que se leen desde fuera y solo cambian dentro. Se crea con los dos equipos y, si no se dan, los goles empiezan en 0. Cuatro funciones: conGolLocal y conGolVisitante devuelven un partido nuevo con un gol más, sin tocar el original; registrarGolLocal y registrarGolVisitante suman el gol al propio partido.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Partido.swift: el fichero entero
// Un struct: al copiarlo, cada copia tiene su propio marcador.
// `Equatable`: el compilador compara todas las propiedades.
public struct Partido: Equatable {
    public let local: Equipo           // Equipo es una clase: se copia la flecha, no el objeto
    public let visitante: Equipo
    public private(set) var golesLocal: Int       // se lee fuera, se cambia solo dentro
    public private(set) var golesVisitante: Int
 
    // Los goles tienen valor por defecto: `Partido(local: a, visitante: b)` empieza 0-0.
    public init(local: Equipo, visitante: Equipo, golesLocal: Int = 0, golesVisitante: Int = 0) {
        self.local = local
        self.visitante = visitante
        self.golesLocal = golesLocal
        self.golesVisitante = golesVisitante
    }
 
    // Devuelve un partido NUEVO con un gol más; el original no cambia.
    public func conGolLocal() -> Partido {
        Partido(local: local, visitante: visitante, golesLocal: golesLocal + 1, golesVisitante: golesVisitante)
    }
 
    public func conGolVisitante() -> Partido {
        Partido(local: local, visitante: visitante, golesLocal: golesLocal, golesVisitante: golesVisitante + 1)
    }
 
    // `mutating`: cambia el propio partido. Solo se puede llamar sobre una `var`.
    public mutating func registrarGolLocal() {
        golesLocal += 1
    }
 
    public mutating func registrarGolVisitante() {
        golesVisitante += 1
    }
}

Tramo 2 · Torneo, una clase

Una lista que empieza vacía, se lee desde fuera y solo crece con un método, en otra clase:

ejemplo mínimo: una lista privada para escribir y append
public final class Diario {
    // [String] vacío al empezar; private(set): desde fuera solo se lee.
    public private(set) var entradas: [String] = []
 
    public init() {}                         // sin datos que pedir: init vacío
 
    public func anotar(_ texto: String) {
        entradas.append(texto)               // append añade al final de la lista
    }
}
 
let diario = Diario()
diario.anotar("lunes")
print(diario.entradas.count)                 // 1

Intenta escribirlo tú: Torneo, una clase pública de la que nadie hereda, con su nombre, sus equipos y una lista de partidos que empieza vacía, se lee desde fuera y solo cambia dentro. Se crea con las etiquetas nombre y equipos, y registrar, que recibe un partido sin etiqueta, lo añade a la lista.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo.swift: el fichero entero
// Una clase: el torneo es uno solo y se comparte.
public final class Torneo {
    public let nombre: String
    public let equipos: [Equipo]
 
    // Se lee desde fuera; solo `registrar` lo cambia. Empieza vacío.
    public private(set) var partidos: [Partido] = []
 
    public init(nombre: String, equipos: [Equipo]) {
        self.nombre = nombre
        self.equipos = equipos
    }
 
    // Añade un partido a la lista.
    public func registrar(_ partido: Partido) {
        partidos.append(partido)
    }
}

Tramo 3 · Verlo en la consola

Pieza nueva: inicio.local === alFinal.local compara la identidad de dos equipos, con el === de la lección anterior: «¿es el mismo objeto?».

Ya tienes rayo, ana, ivan y jugadores en main.swift, del paso anterior. Lo que se copia y lo que se comparte: el partido (un struct) frente al torneo (una class). Intenta escribirlo tú, al final de main.swift: crea el Toros con Iván y un partido inicio, Rayo contra Toros; copia ese partido en alFinal, regístrale dos goles del local y uno del visitante, e imprime el marcador de los dos y si el equipo local de las dos copias es el mismo objeto. Después crea el torneo con los dos equipos, guárdalo también en mismoTorneo, registra alFinal a través de mismoTorneo e imprime cuántos partidos ve torneo. Lo que debes ver:

Antes: 0-0, después: 2-1
¿Mismo equipo local en las dos copias? true
Partidos vistos desde `torneo`: 1
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
main.swift: fragmento, copiar y compartir
let toros = Equipo(nombre: "Toros", plantilla: [ivan])!
let inicio = Partido(local: rayo, visitante: toros)
var alFinal = inicio            // COPIA: Partido es un struct
alFinal.registrarGolLocal()
alFinal.registrarGolLocal()
alFinal.registrarGolVisitante()
print("Antes: \(inicio.golesLocal)-\(inicio.golesVisitante), después: \(alFinal.golesLocal)-\(alFinal.golesVisitante)")
// La copia del Partido copió la flecha hacia el Equipo, no el Equipo: es el mismo.
print("¿Mismo equipo local en las dos copias? \(inicio.local === alFinal.local)")
 
let torneo = Torneo(nombre: nombreDelTorneo, equipos: [rayo, toros])
let mismoTorneo = torneo        // MISMA referencia: Torneo es una class
mismoTorneo.registrar(alFinal)
print("Partidos vistos desde `torneo`: \(torneo.partidos.count)")

Ejecuta xcrun swift run torneo-cli: al final deben salir las tres líneas nuevas, después de las dos de la lección anterior (el punto de control de abajo muestra las cinco).

Ahora provócalo: cambia en main.swift var alFinal = inicio por let alFinal = inicio. registrarGolLocal() deja de compilar con cannot use mutating member on immutable value. Devuélvelo a var.

Los tests

StructYClassTests.swift (12 tests) comprueba lo que has visto arriba y no lo escribes: rellénalo con el botón Copiar fichero (copia al portapapeles el fichero ya escrito, como en guard let y textos que no son números).

  1. Crea Tests/TorneoTests/StructYClassTests.swift: clic derecho sobre Tests → TorneoTests → New File from Template… → Swift File → Next, escribe StructYClassTests y pulsa Create. O, en la Terminal, touch Tests/TorneoTests/StructYClassTests.swift.
  2. Pulsa el botón de abajo.
  3. Haz clic dentro del fichero, selecciona todo con Cmd+A y pega con Cmd+V.
StructYClassTests.swift

Punto de control

  • Build. Desde ~/Marcador-Swift/Torneo, xcrun swift build termina en Build complete!.
  • Run. xcrun swift run torneo-cli termina con estas líneas (Rayo FC (3 jugadores) cuenta los tres jugadores del ejemplo):
Rayo FC (3 jugadores), capitán: Ana
Toros con dorsales repetidos: nil
Antes: 0-0, después: 2-1
¿Mismo equipo local en las dos copias? true
Partidos vistos desde `torneo`: 1
  • Test. xcrun swift test termina con Test run with 28 tests in 4 suites passed (ignora la línea Executed 0 tests de XCTest): los 16 anteriores y los 12 que copiaste.
  • Viste el error cannot use mutating member on immutable value al cambiar var alFinal por let y lo devolviste a var.
  • Sabes decir por qué Antes: 0-0, después: 2-1 (el Partido es un valor: se copió) y por qué true en «¿Mismo equipo local…?» (copiar el Partido copió la flecha hacia el Equipo, no el Equipo).

Comprueba lo que entendiste

4 preguntas · necesitas 3 para completar la lección

  1. 1.Escribes let q = Marcador() y luego q.sumar(), donde Marcador es un struct y sumar() es mutating. ¿Qué pasa?

  2. 2.¿Qué es el copy-on-write de un array?

  3. 3.Partido es un struct con dos propiedades de tipo Equipo (una class). Copias var copia = partido. ¿Qué comparten partido y copia?

  4. 4.Con let p = Partido(local: a, visitante: b), ¿cuál de estas llamadas compila?

Faltan 4 de 4