Equatable y Hashable
Al terminar: Hacer que tus tipos se puedan comparar y usar como claves con Equatable y Hashable.
Ya tienes el reglamento como protocolo. Falta aprovechar dos cosas más de Swift: las extensiones sobre tipos que ya existen (Jugador, Equipo, hasta un Int) y los protocolos Equatable y Hashable, que dicen cuándo dos valores son «el mismo» y permiten usarlos como clave de un diccionario. Al terminar, torneo-cli imprimirá la etiqueta de Ana, el capitán de Toros y cuántos goles marcó Ana contando con un diccionario.
Temas: extensiones de un tipo, Equatable, Hashable y Dictionary(grouping:by:).
Los ejemplos y el ejercicio se escriben en tu Playground, el de Tu primer paquete de Swift: reutiliza el mismo y borra lo anterior.
Extensiones de un tipo que ya existe
Una extensión (extension) añade métodos y propiedades calculadas a un tipo desde fuera: el tipo puede ser tuyo (escrito en otro fichero) o de Swift, como Int o String. No puede añadir propiedades guardadas (con memoria propia). Y lo private de un tipo solo lo ve una extensión que esté en el mismo fichero que el tipo; una extensión escrita en otro fichero recibe 'x' is inaccessible due to 'private' protection level.
struct Libro {
let titulo: String
let paginas: Int
}
// Una extensión de un tipo tuyo: añade un método sin tocar la declaración de Libro.
extension Libro {
func ficha() -> String { "\(titulo) (\(paginas) págs.)" }
}
// Una extensión de un tipo de Swift: añade una propiedad CALCULADA a todos los Int.
extension Int {
var enEuros: String { "\(self) €" } // `self` es el número sobre el que se llama
}
let dune = Libro(titulo: "Dune", paginas: 600)
print(dune.ficha())
print(12.enEuros)Al ejecutarlo, verás:
Dune (600 págs.)
12 €Equatable y Hashable: dos protocolos que ya usas
Equatable significa «se puede comparar con ==». Hashable significa «se puede convertir en un número de identificación (hash)»; sin él, un valor no puede ser clave de un Dictionary (el Map de Kotlin: una colección de pares clave → valor) ni entrar en un Set (una colección sin repetidos).
| Tipo | Equatable / Hashable |
|---|---|
struct (como Jugador) | Se escribe : Equatable, Hashable y el compilador escribe == y el hash comparando todas las propiedades |
enum | Igual que el struct; un enum sin valores asociados ya lo es sin escribir nada |
class (como Equipo) | No se sintetiza: hay que escribir == y hash(into:) |
Un struct sin Equatable no se puede comparar (pruébalo en tu Playground):
struct Fila { let nombre: String }
let a = Fila(nombre: "a"), b = Fila(nombre: "a")
print(a == b)El compilador responde error: binary operator '==' cannot be applied to two 'Fila' operands. Escribe struct Fila: Equatable y compila.
Una clase no tiene == hasta que lo escribes. Equipo (el extension Equipo: Hashable que pegaste en struct frente a class) lo define con === (la misma referencia) y para el hash usa ObjectIdentifier(self), un identificador único de cada objeto. Es lo que Kotlin hace por defecto con == en una clase normal.
Agrupar con un diccionario
Con Hashable, un valor puede ser clave. Dictionary(grouping: lista, by: { $0 }) crea un diccionario cuya clave es lo que devuelve la closure (aquí, el propio elemento) y cuyo valor es la lista de los elementos que comparten esa clave. .mapValues(\.count) cambia cada lista por su tamaño; \.count es una ruta de propiedad (key path), un atajo de { $0.count } que ya usaste en Closures con listas y verás con calma en Colecciones y key paths. Y buscar en un diccionario devuelve un opcional (la clave puede no estar), así que ?? 0 da el valor de respaldo.
let entradas = ["1234ABC", "9876XYZ", "1234ABC"]
// La clave es la propia matrícula; el valor, la lista de entradas con esa matrícula.
let agrupadas = Dictionary(grouping: entradas, by: { $0 })
print(agrupadas["1234ABC"] as Any) // `as Any` solo evita el aviso de Swift al imprimir un opcional
// Cada lista, por su tamaño: cuántas veces entró cada matrícula.
let visitas = agrupadas.mapValues(\.count)
print(visitas["1234ABC"] ?? 0)
print(visitas["0000AAA"] ?? 0) // no está: ?? da el respaldoOptional(["1234ABC", "1234ABC"])
2
0Reto · Ejercicio: el pedido de fruta
En tu Playground, haz un struct para una fruta, con un nombre y un color, que se pueda comparar y servir de clave de un diccionario. Con una extensión, dale una etiqueta, «fresa (roja)». Con otra extensión sobre los números enteros, añade un texto en kilos: «3 kg». Con un pedido de tres frutas (fresa roja, kiwi verde y otra fresa roja), imprime: si una fresa roja es igual a otra fresa roja, la etiqueta de la fresa, cuántas fresas hay en el pedido y cuántas peras verdes (agrupando el pedido por fruta), y el texto en kilos del 3. Lo que debe salir:
true
fresa (roja)
2
0
3 kgVer solución
struct Fruta: Hashable {
let nombre: String
let color: String
}
extension Fruta {
func etiqueta() -> String { "\(nombre) (\(color))" }
}
extension Int {
var enKilos: String { "\(self) kg" }
}
let fresa = Fruta(nombre: "fresa", color: "roja")
let pedido = [fresa, Fruta(nombre: "kiwi", color: "verde"), Fruta(nombre: "fresa", color: "roja")]
print(fresa == Fruta(nombre: "fresa", color: "roja"))
print(fresa.etiqueta())
let cuantas = Dictionary(grouping: pedido, by: { $0 }).mapValues { $0.count }
print(cuantas[fresa] ?? 0)
print(cuantas[Fruta(nombre: "pera", color: "verde")] ?? 0)
print(3.enKilos)Salida: true, fresa (roja), 2, 0 y 3 kg. Escribir : Hashable basta: el compilador compara el nombre y el color, y como Hashable incluye a Equatable, == también funciona.
Paso · Formato.swift y los goles de Ana
Objetivo. Añadir dos extensiones de formato (a Jugador y a Equipo), contar con un diccionario los goles de Ana y copiar los nueve tests del reglamento. Al ejecutar torneo-cli verás, además de las dos líneas de la lección anterior, la etiqueta de Ana y sus goles.
El título de cada bloque de abajo es la ruta del fichero dentro de ~/Marcador-Swift, la misma que ves en el árbol de arriba.
Cómo crear el fichero. En el Project navigator (el panel de la izquierda de Xcode; se muestra u oculta con Cmd+1), despliega Sources, clic derecho sobre la carpeta Torneo (la de dentro de Sources) → New File from Template… → Swift File → Next, escribe Formato y pulsa Create. Si el menú de tu versión de Xcode se llama distinto, crea el fichero desde la Terminal, en ~/Marcador-Swift/Torneo: touch Sources/Torneo/Formato.swift.
Jugador ya trae lo que necesita: su dorsalOGuion() (de Opcionales) y su Equatable, Hashable, que escribiste en esa misma lección. Equipo es Hashable desde struct frente a class.
Lo que exigen los tests que copiarás. Formato.swift: la etiqueta de un jugador devuelve «Ana (#9)» (nombre y dorsal o guion), y el capitanONinguno de un equipo devuelve el nombre del capitán o «sin capitán».
Intenta escribirlo tú. Añade a Jugador una función que dé el texto con su nombre y su dorsal, y a Equipo una propiedad calculada con el nombre del capitán o «sin capitán». En main.swift: imprime la etiqueta de Ana y el capitán de Toros; y cuenta, agrupando los goleadores de alFinal por jugador, los goles de Ana. Lo que debes ver, detrás de las dos líneas de la lección anterior:
Etiqueta: Ana (#9); capitán de Toros: sin capitán
Goles de Ana: 2El bloque de main.swift va justo antes de let torneo = Torneo(…), detrás de las líneas del reglamento.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// Una extensión añade cosas a un tipo que ya existe, sin tocar su fichero.
// En Kotlin sería `fun Jugador.etiqueta()`.
// Puede añadir métodos, propiedades CALCULADAS y adopciones de protocolo,
// pero no propiedades guardadas. Y solo ve lo `private` del tipo si está
// en el MISMO fichero que el tipo (estas dos viven en otro, así que no lo ven).
extension Jugador {
// Texto con el nombre y el dorsal: «Ana (#9)».
public func etiqueta() -> String { "\(nombre) (\(dorsalOGuion()))" }
}
extension Equipo {
// `capitan` es opcional: `?.nombre` da el nombre o `nil`, y `??` pone el texto de respaldo.
public var capitanONinguno: String { capitan?.nombre ?? "sin capitán" }
}print("Etiqueta: \(ana.etiqueta()); capitán de Toros: \(toros.capitanONinguno)")
// Hashable: un jugador puede ser clave de un diccionario o entrar en un Set.
let golesPorJugador = Dictionary(grouping: alFinal.goleadores(), by: { $0 }).mapValues(\.count)
print("Goles de Ana: \(golesPorJugador[ana] ?? 0)")Compila y ejecuta antes de copiar nada. Desde la Terminal, en ~/Marcador-Swift/Torneo:
xcrun swift build
xcrun swift run torneo-cliLos tests
ReglamentoTests.swift son nueve comprobaciones de lo que escribiste en las dos lecciones, del mismo tipo que las anteriores, así que no los escribes: se rellena 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 el fichero, pulsar el botón, seleccionar todo, pegar.
- En el Project navigator, clic derecho sobre
Tests → TorneoTests→ New File from Template… → Swift File → Next →ReglamentoTests→ Create. (Plan B:touch Tests/TorneoTests/ReglamentoTests.swiftdesde~/Marcador-Swift/Torneo.) - Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
- Haz clic dentro de
ReglamentoTests.swift, selecciona todo con Cmd+A y pega con Cmd+V.
De los nueve, uno enseña algo que el resto no: demuestra el error típico de la lección anterior con un test en vez de con un print.
// Un tipo de prueba que escribe su propio resumen(), que NO es un requisito.
private struct ReglamentoConDerrotaCastigada: Reglamento {
let puntosPorVictoria = 3
let puntosPorEmpate = 1
let puntosPorDerrota = -1
func resumen() -> String { "el mío" }
}
@Test func unMetodoQueNoEsRequisitoSeDespachaEstaticamente() {
let concreto = ReglamentoConDerrotaCastigada()
let comoProtocolo: any Reglamento = concreto
#expect(concreto.resumen() == "el mío") // por el tipo concreto
#expect(comoProtocolo.resumen() == "3/1/-1") // por el protocolo: sale de la extensión
}@Test marca la prueba y #expect(condición) comprueba que se cumple, como viste en La regla de los puntos y tu primer test. La prueba pasa precisamente porque las dos líneas dicen cosas distintas: es la trampa de la lección anterior, comprobada. private struct hace el tipo visible solo dentro de ese fichero.
Punto de control
xcrun swift build(desde~/Marcador-Swift/Torneo) compila sin errores.xcrun swift run torneo-cliimprimeReglamento Liga (3/1/0),Con reglas antiguas, una victoria vale 2,Etiqueta: Ana (#9); capitán de Toros: sin capitányGoles de Ana: 2.xcrun swift testtermina conTest run with 51 tests in 7 suites passed: los 42 de la lección anterior más los 9 nuevos deReglamentoTests.- Sabes decir por qué un struct necesita
: Equatable, Hashabley una clase comoEquiponecesita que escribas la igualdad y el hash.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.
Jugadores un struct yEquipouna clase. ¿Qué hace falta para que los dos seanHashable?2.¿Qué NO puede añadir una extensión a un tipo que ya existe?
3.
Equipodefine su igualdad con===y su hash conObjectIdentifier(self). ¿Por qué?4.Con
let visitas = Dictionary(grouping: entradas, by: { $0 }).mapValues(\.count), ¿qué devuelvevisitas["0000AAA"] ?? 0si esa matrícula no aparece enentradas?5.¿Qué hace
Dictionary(grouping: pedido, by: { $0 })?