Enums: el partido de eventos
Al terminar: Resolver el partido a partir de una lista de eventos con enums.
Al terminar esta lección Partido guarda en Swift lo que pasó —goles, tarjetas, cambios— y el marcador sale de ahí, igual que en tu versión de Kotlin. Hasta ahora un Partido guardaba su marcador en dos números; desde ahora guarda sus eventos y calcula el marcador, así que no puede contradecirse. xcrun swift run torneo-cli imprimirá la crónica de un partido y su resultado.
Temas: if case, count { … }, Equatable en un enum, métodos que cambian el valor frente a métodos que devuelven una copia, y deducir un dato en lugar de guardarlo.
Guardar el marcador o deducirlo
Si Partido guarda golesLocal = 2 y a la vez una lista de eventos con tres goles locales, ¿cuál dice la verdad? Con dos fuentes de datos, pueden contradecirse. La solución es guardar una sola: la lista de lo que pasó. El marcador deja de ser un dato y pasa a ser una cuenta: «cuántos de estos eventos son goles del Rayo». Por eso en el paso se borra todo lo que guardaba el marcador: golesLocal y golesVisitante como datos, y conGolLocal(), conGolVisitante(), registrarGolLocal() y registrarGolVisitante().
Para escribir esa cuenta hacen falta unas piezas que todavía no has visto.
Las piezas que vas a escribir
if case let .gol(_, _, equipoDelGol) = evento { … } else { … }: unswitchde un solo caso. Entra en elifsolo si el evento es un gol y liga su equipo. Como elifes una expresión, cada rama es el valor:equipoDelGol == equiposi es un gol yfalsesi no. Si solo quieres saber qué caso es y no necesitas sus datos, no ligas nada:if case .gol = evento(sinletni paréntesis).eventos.count { evento in … }: cuenta los elementos para los que el bloque devuelvetrue. Lo que va entre llaves,evento in …, es una closure: una pequeña función sin nombre, dondeeventoes su parámetro (cada elemento de la lista) y lo que sigue aines lo que hace; se explica a fondo en Closures. Aquí devuelvetrueofalse. Se lee en dos pasos: «por cada evento, ¿es un gol de este equipo?» y «cuántos dijeron que sí».EquatableenEventoDePartido: ya lo declaraste en la lección anterior. Es lo que permite compararequipoDelGol == equipoy quePartido: Equatablesiga compilando.mutating func registrar(_:)yfunc registrando(_:): convención de nombres de Swift. Un verbo (registrar) cambia el propio valor y un gerundio (registrando) devuelve una copia. Kotlin solo tenía la segunda, con el nombreregistrar.var copia = self: dentro de un método de unstruct,selfse copia como cualquier valor.- Encadenar en varias líneas: un punto al principio de la línea siguiente sigue actuando sobre lo anterior.
- Un
if/else ifcomo expresión enresultado(): cada rama es el valor, sinreturn. .map(\.rawValue): ya lo viste en la lección anterior; aquí saca el texto de cada color de tarjeta.
Las mismas piezas, en otro dominio: los movimientos de una cuenta del banco. No es tu código de Marcador; pruébalo en tu Playground de Xcode (el de Tu primer paquete de Swift; reutilízalo: borra lo anterior, pega el código y ejecútalo).
enum Movimiento: Equatable { // Equatable: lo que guardan los casos (Int, String) ya lo es
case ingreso(dia: Int, importe: Int)
case retiro(dia: Int, importe: Int)
case comision(dia: Int, concepto: String)
var dia: Int { // el dato que comparten los tres casos
switch self {
// Tres patrones en una rama, separados por comas; cada uno liga dia y _ ignora el resto.
case .ingreso(let dia, _), .retiro(let dia, _), .comision(let dia, _):
dia
}
}
}
enum Estado: Equatable { case enPositivo, enRojo } // dos casos simples en una línea, separados por coma
struct Cuenta: Equatable {
private(set) var movimientos: [Movimiento] = []
func ingresosDe(masDe minimo: Int) -> Int {
// count { … } cuenta los elementos para los que el bloque devuelve true.
movimientos.count { movimiento in
// if case: entra solo si es un ingreso, y liga su importe.
if case let .ingreso(_, importe) = movimiento { importe > minimo } else { false }
}
}
mutating func anotar(_ movimiento: Movimiento) { // verbo: cambia self
movimientos.append(movimiento)
}
func anotando(_ movimiento: Movimiento) -> Cuenta { // gerundio: devuelve una copia
var copia = self // self es un struct: esto es una copia
copia.anotar(movimiento) // se cambia la copia, no self
return copia
}
func estado(saldo: Int) -> Estado {
if saldo >= 0 { .enPositivo } else { .enRojo } // el if es el valor; .enRojo es un Estado
}
}
let vacia = Cuenta()
let llena = vacia // cada línea que empieza por punto sigue la cadena
.anotando(.ingreso(dia: 1, importe: 500))
.anotando(.retiro(dia: 3, importe: 80))
.anotando(.ingreso(dia: 9, importe: 40))
print(vacia.movimientos.count, llena.movimientos.count) // 0 3: vacia no cambió
print(llena.movimientos.map(\.dia)) // [1, 3, 9]: map con un key path
print(llena.ingresosDe(masDe: 100)) // 1: solo el de 500
print(llena.estado(saldo: 460) == .enPositivo) // trueImprime 0 3, [1, 3, 9], 1 y true.
Reto · Ejercicio: la cesta de la compra
En tu Playground (borra antes lo anterior), un artículo de la cesta puede ser una fruta (con su nombre y los kilos) o una bebida (con su nombre y los litros). Haz una cesta que guarde sus artículos (se leen desde fuera, solo cambian dentro) y que tenga: una función que mete un artículo en la propia cesta; otra que devuelve una copia con el artículo metido, sin tocar la original; los kilos de fruta que lleva; y cuántas bebidas lleva. Parte de una cesta vacía, saca de ella una copia con 2 kg de manzanas y 6 l de agua (encadenando), mete en esa copia 1 kg de peras e imprime: cuántos artículos tiene la vacía y cuántos la llena (en una línea), los kilos de fruta y el número de bebidas.
0 3
3
1Ver solución
enum Articulo: Equatable {
case fruta(nombre: String, kilos: Int)
case bebida(nombre: String, litros: Int)
}
struct Cesta {
private(set) var articulos: [Articulo] = []
mutating func meter(_ articulo: Articulo) {
articulos.append(articulo)
}
func metiendo(_ articulo: Articulo) -> Cesta {
var copia = self
copia.meter(articulo)
return copia
}
func kilosDeFruta() -> Int {
var total = 0
for articulo in articulos {
if case let .fruta(_, kilos) = articulo { total += kilos }
}
return total
}
func cuantasBebidas() -> Int {
articulos.count { articulo in
if case .bebida = articulo { true } else { false }
}
}
}
let vacia = Cesta()
var llena = vacia
.metiendo(.fruta(nombre: "manzanas", kilos: 2))
.metiendo(.bebida(nombre: "agua", litros: 6))
llena.meter(.fruta(nombre: "peras", kilos: 1))
print(vacia.articulos.count, llena.articulos.count)
print(llena.kilosDeFruta())
print(llena.cuantasBebidas())Imprime 0 3, 3 y 1. meter lleva mutating porque cambia la propia cesta (por eso llena es var); metiendo trabaja sobre una copia y la original, vacia, se queda con 0 artículos.
Paso · El partido hecho de eventos
Objetivo. Reescribir Partido para que guarde sus eventos y deduzca el marcador, y ver la crónica de un partido en main.swift. Al terminar, xcrun swift run torneo-cli imprime la crónica y su resultado.
El título de cada bloque es la ruta del fichero dentro de ~/Marcador-Swift, la misma que ves en el árbol de arriba.
Lo que se borra: el marcador guardado
Partido guardaba golesLocal y golesVisitante y tenía conGolLocal(), conGolVisitante(), registrarGolLocal() y registrarGolVisitante(). Todo eso se borra: el marcador sale de los eventos. Para reescribir Partido.swift entero, ábrelo (doble clic en el Project navigator), selecciona todo con Cmd+A y escribe encima. En main.swift (Sources → torneo-cli) se sustituye todo el trozo que va desde la línea var alFinal = inicio hasta la línea print("¿Mismo equipo local en las dos copias? …"), ambas incluidas (son las tres llamadas a registrarGol…, el print de «Antes», el comentario y el último print); el resto del fichero no cambia.
Lo que tiene que ofrecer Partido. Los tests que copiarás usan estos nombres y textos exactos:
| Tipo | Qué lleva |
|---|---|
struct Partido: Equatable | local, visitante, eventos (se lee fuera, se cambia dentro); init(local:visitante:eventos:) con eventos vacío por defecto; golesDe(_ equipo:) -> Int; golesLocal y golesVisitante calculados; mutating registrar(_:); registrando(_:) -> Partido; resultado() -> Resultado |
Intenta escribirlo tú, en este orden:
- El nuevo
Partido: guarda sus eventos (empieza sin ninguno) y cuenta los goles de cada equipo a partir de ellos;registrarañade un evento al propio partido,registrandodevuelve una copia con el evento añadido, yresultadocompara los goles. - La crónica en
main.swift(sustituye el trozo indicado arriba): a partir deinicio, registra en cadena un gol de Ana en el 12, una amarilla a Iván en el 30, un gol de Iván para Toros en el 55, otro de Ana en el 80 y, en el 85, el cambio de Ana por el segundo jugador de la lista; imprime cuántos eventos hay antes y después, cada evento descrito, el marcador con el titular y los textos de los colores de tarjeta.
Lo que debes ver, justo antes de la última línea de la salida (Partidos vistos desde …):
Antes: 0 eventos, después: 5
12' Gol de Ana (Rayo FC)
30' Tarjeta amarilla para Iván
55' Gol de Iván (Toros)
80' Gol de Ana (Rayo FC)
85' Cambio: sale Ana, entra Luis
Marcador: 2-1 -> Gana Rayo FC
Colores de tarjeta: ["AMARILLA", "ROJA"]Cuando termines, abre el bloque y compara con la solución.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
public struct Partido: Equatable {
public let local: Equipo
public let visitante: Equipo
public private(set) var eventos: [EventoDePartido]
public init(local: Equipo, visitante: Equipo, eventos: [EventoDePartido] = []) {
self.local = local
self.visitante = visitante
self.eventos = eventos
}
public func golesDe(_ equipo: Equipo) -> Int {
eventos.count { evento in
// `if case` es un `switch` de un solo caso: mira si el evento es un
// gol y, si lo es, liga el equipo.
if case let .gol(_, _, equipoDelGol) = evento { equipoDelGol == equipo } else { false }
}
}
public var golesLocal: Int { golesDe(local) }
public var golesVisitante: Int { golesDe(visitante) }
// Convención de nombres de Swift: el que CAMBIA `self` es un verbo
// (`registrar`) y el que devuelve una copia usa un gerundio
// (`registrando`). Kotlin solo tenía el segundo, con el nombre `registrar`.
public mutating func registrar(_ evento: EventoDePartido) {
eventos.append(evento)
}
public func registrando(_ evento: EventoDePartido) -> Partido {
var copia = self
copia.registrar(evento)
return copia
}
public func resultado() -> Resultado {
if golesLocal > golesVisitante {
.victoria(ganador: local, perdedor: visitante)
} else if golesLocal < golesVisitante {
.victoria(ganador: visitante, perdedor: local)
} else {
.empate
}
}
}// Parte del partido de antes (`inicio`, sin eventos) y registra cinco eventos.
// El punto inicial (`.gol(…)`) funciona porque Swift sabe que `registrando` espera un `EventoDePartido`.
let alFinal = inicio
.registrando(.gol(minuto: 12, jugador: ana, equipo: rayo)) // gol de Ana en el 12'
.registrando(.tarjeta(minuto: 30, jugador: ivan, color: .amarilla)) // amarilla a Iván
.registrando(.gol(minuto: 55, jugador: ivan, equipo: toros)) // gol de Iván para Toros
.registrando(.gol(minuto: 80, jugador: ana, equipo: rayo)) // otro de Ana
.registrando(.cambio(minuto: 85, sale: ana, entra: jugadores[1])) // Ana sale, entra Luis
// `inicio` no cambia: `registrando` devuelve una copia con el evento añadido.
print("Antes: \(inicio.eventos.count) eventos, después: \(alFinal.eventos.count)")
// Una línea por evento, con el texto que construye `describir`.
for evento in alFinal.eventos { print(describir(evento)) }
// Los goles salen de los eventos; `resultado()` los compara y `titular` da el texto.
print("Marcador: \(alFinal.golesLocal)-\(alFinal.golesVisitante) -> \(alFinal.resultado().titular)")
// `allCases` trae los dos colores y `rawValue` el texto de cada uno.
print("Colores de tarjeta: \(ColorDeTarjeta.allCases.map(\.rawValue))")Los tests no los escribes: son comprobaciones de los enums y del Partido que acabas de crear, del mismo tipo que las de las lecciones anteriores. Hay dos ficheros. EnumsTests.swift es nuevo, y StructYClassTests.swift ya existe y se sustituye por una versión adaptada a los eventos (sus 12 tests siguen siendo los mismos). Se rellenan con el botón Copiar fichero, que copia el contenido al portapapeles y no escribe nada en tu proyecto (se explica en Clases y objetos). El orden es siempre el mismo: crear o abrir el fichero, pulsar el botón, seleccionar todo, pegar.
- En el Project navigator, clic derecho sobre la carpeta
Tests → TorneoTests→ New File from Template… → Swift File → Next →EnumsTests→ Create. (Plan B:touch Tests/TorneoTests/EnumsTests.swiftdesde~/Marcador-Swift/Torneo.) Se abre con unas pocas líneas de plantilla. - Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
- Haz clic dentro de
EnumsTests.swift, selecciona todo con Cmd+A y pega con Cmd+V: sustituye lo que puso Xcode.
Ahora el que ya existe: abre StructYClassTests.swift, pulsa su botón, selecciona todo con Cmd+A y pega con Cmd+V. Pisa tu versión anterior, y es lo que quieres.
Punto de control
- Build. Desde
~/Marcador-Swift/Torneo,xcrun swift buildtermina enBuild complete!. - Run.
xcrun swift run torneo-clitermina con estas líneas:
Antes: 0 eventos, después: 5
12' Gol de Ana (Rayo FC)
30' Tarjeta amarilla para Iván
55' Gol de Iván (Toros)
80' Gol de Ana (Rayo FC)
85' Cambio: sale Ana, entra Luis
Marcador: 2-1 -> Gana Rayo FC
Colores de tarjeta: ["AMARILLA", "ROJA"]
Partidos vistos desde `torneo`: 1- Test.
xcrun swift testtermina conTest run with 34 tests in 5 suites passed: los 28 anteriores y los 6 deEnumsTests.swift(StructYClassTests.swiftse sustituye por una versión con los mismos 12). - Provocado. En
resultado()cambia el primer>por<:xcrun swift testfalla (enEnumsTests, el test del resultado de una victoria y un empate). Devuélvelo a como estaba. - Sabes decir por qué
Partidoya no guarda los goles y qué diferencia hay entreregistraryregistrando.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.En
golesDe, la líneaif case let .gol(_, _, equipoDelGol) = evento { … } else { false }mira un evento. ¿Qué hace?2.¿Qué cuenta
eventos.count { evento in … }?3.Tienes
let alFinal = inicio.registrando(.gol(…)). ¿Qué pasa coninicio?4.¿Por qué
Partidoya no guardagolesLocalygolesVisitantecomo datos?