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:
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' constantFalla 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.
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 14Imprime 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: Equatable | Se parece en la igualdad, pero el struct pide Equatable y no trae copy ni desestructuración |
val / var en cada propiedad | let / var en cada propiedad y en quien guarda el valor | Un struct en un let es inmutable entero |
plantilla.toList() (copia defensiva) | self.plantilla = plantilla | El 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 21Después intenta subir salon directamente y lee el mensaje del compilador.
Ver solución
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' constantSubir 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.
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.swiftComprueba 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;Equatablelo da el compilador comparando todas las propiedades (por esoEquipotuvo que serEquatableen la lección anterior).golesLocal: Int = 0en uninit: un valor por defecto, como los de Valores por defecto e inout y comodorsal: Int? = nil.mutating func: método de unstructque se cambia a sí mismo.conGolLocal()yconGolVisitante()devuelven un partido nuevo, yregistrarGolLocal()yregistrarGolVisitante()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
// 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:
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) // 1Intenta 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
// 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`: 1Intenta primero. Cuando termines, compara con la solución.Mostrar el código
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).
- Crea
Tests/TorneoTests/StructYClassTests.swift: clic derecho sobreTests → TorneoTests→ New File from Template… → Swift File → Next, escribeStructYClassTestsy pulsa Create. O, en la Terminal,touch Tests/TorneoTests/StructYClassTests.swift. - Pulsa el botón de abajo.
- Haz clic dentro del fichero, selecciona todo con Cmd+A y pega con Cmd+V.
Punto de control
- Build. Desde
~/Marcador-Swift/Torneo,xcrun swift buildtermina enBuild complete!. - Run.
xcrun swift run torneo-clitermina 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 testtermina conTest run with 28 tests in 4 suites passed(ignora la líneaExecuted 0 testsde XCTest): los 16 anteriores y los 12 que copiaste. - Viste el error
cannot use mutating member on immutable valueal cambiarvar alFinalporlety lo devolviste avar. - Sabes decir por qué
Antes: 0-0, después: 2-1(elPartidoes un valor: se copió) y por quétrueen «¿Mismo equipo local…?» (copiar elPartidocopió la flecha hacia elEquipo, no elEquipo).
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.Escribes
let q = Marcador()y luegoq.sumar(), dondeMarcadores unstructysumar()esmutating. ¿Qué pasa?2.¿Qué es el copy-on-write de un array?
3.
Partidoes unstructcon dos propiedades de tipoEquipo(unaclass). Copiasvar copia = partido. ¿Qué compartenpartidoycopia?4.Con
let p = Partido(local: a, visitante: b), ¿cuál de estas llamadas compila?