Saltar al contenido
K
Swift: el lenguajePaso a paso45 min de lectura · 10 min de máquina Básico

some y any

Al terminar: Entender some y any con protocolos genéricos.

Ranking<T> ya ordena cualquier cosa. En esta lección los protocolos ganan un hueco de tipo (associatedtype), aprendes la diferencia entre some y any, y usas las dos para sacar la lista de goleadores con su puesto: 1. Ana (#9): 2.

Temas: tipos asociados y some frente a any.

Los ejemplos y el ejercicio se escriben en tu Playground, el de Tu primer paquete de Swift: reutiliza el mismo y borra lo anterior.

Tipos asociados: un hueco dentro de un protocolo

Un protocolo no lleva <T> como una interfaz de Kotlin. Lleva un tipo asociado (associated type): un hueco que cada tipo que adopta el protocolo rellena con el suyo.

ejemplo mínimo: associatedtype (con comentarios)
protocol Contenedor {
    associatedtype Elemento                 // el hueco: cada tipo dice qué guarda
    var todos: [Elemento] { get }
}
struct Nombres: Contenedor { let todos = ["Ana", "Luis"] }   // Elemento = String
struct Dorsales: Contenedor { let todos = [9, 7, 4] }        // Elemento = Int
 
// C.Elemento es «el tipo que ese C haya elegido».
func primero<C: Contenedor>(_ c: C) -> C.Elemento? { c.todos.first }
 
print(primero(Nombres()) as Any)
print(primero(Dorsales()) as Any)
Optional("Ana")
Optional(9)

primero(Nombres()) devuelve un String? y primero(Dorsales()) un Int?: el compilador sabe cuál en cada llamada.

some frente a any

Hasta aquí, un protocolo era una lista de cosas que un tipo sabe hacer. Para usarlo como tipo (en el tipo de un parámetro, de un valor devuelto o de una variable) Swift pide una palabra delante que diga cómo de concreto es el tipo que hay detrás. Son dos, y se entienden con un ejemplo de cada una.

some: un tipo concreto que decide quien escribe la función. La función promete «te doy algún Contenedor» y devuelve siempre el mismo tipo; el compilador lo sabe, quien llama no necesita saberlo. Usa el Contenedor de arriba, en el mismo Playground:

ejemplo mínimo: some
// Siempre devuelve un `Nombres`, aunque quien llama solo vea «un Contenedor».
func nombresOrdenados() -> some Contenedor { Nombres() }
print(nombresOrdenados().todos)   // ["Ana", "Luis"]

any: cualquier tipo que cumpla el protocolo, decidido al ejecutar. Sirve para guardar cosas de tipos distintos en el mismo sitio, como una lista que mezcla Nombres y Dorsales:

ejemplo mínimo: any
// Dos tipos distintos en la misma lista: con `some` no se podría.
let mezcla: [any Contenedor] = [Nombres(), Dorsales()]
print(mezcla.map { $0.todos.count })   // [2, 3]

Esta tabla resume la diferencia:

some Contenedorany Contenedor
Qué tipo esUno concreto, fijo, que decide quien implementaCualquiera que cumpla el protocolo, decidido en ejecución
¿Conserva Elemento?SíNo: se pierde el tipo concreto
¿Lista de tipos distintos?NoSí: [any Contenedor]
CosteNingunoPequeño: el valor va «en una caja»

Regla práctica: empieza con some (o con un genérico) y usa any solo cuando necesites mezclar tipos distintos en una misma variable o lista. some en un parámetro es una forma corta de escribir un genérico: func f(_ x: some Contenedor) equivale a func f<C: Contenedor>(_ x: C).

Cuándo any no tiene sustituto. Con some, el tipo concreto es uno solo, y el compilador tiene que poder deducirlo. Hay dos cosas que some no admite, y ahí any es la única salida:

  • Una propiedad guardada cuyo valor llega de fuera: struct Torneo { let reglamento: some Reglamento } no compila (property declares an opaque return type, but has no initializer expression…). Por eso Torneo de Protocolos y extensiones guarda un any Reglamento.
  • Una función que devuelve tipos distintos según el caso: -> some Reglamento con b ? ReglamentoLiga() : ReglamentoPersonalizado(…) da result values in '? :' expression have mismatching types.

Y un detalle de escritura: any se escribe siempre delante de un protocolo usado como tipo. Con un protocolo con tipo asociado, como Contenedor, hoy el compilador solo avisa si falta, pero en un modo futuro del lenguaje será un error.

Kotlin ↔ Swift

Kotlin (el tuyo)Swift
Una interface no lleva <T>; si hace falta un tipo abierto, lo lleva la clase o la funciónprotocol con associatedtype Elemento
List<Reglamento> (lista de una interfaz: de cada elemento solo ves la interfaz)[any Contenedor]
fun f(): Reglamento (oculta la clase, pero puede devolver otra distinta en cada llamada)-> some Contenedor: siempre el mismo tipo concreto, que el compilador conoce

Reto · Ejercicio: colecciones de cromos y monedas

En tu Playground, haz un protocolo para una colección que tenga una lista de piezas de un tipo que decide cada colección. Haz dos colecciones: cromos (los nombres «Ana», «Iván» y «Luis») y monedas (los valores 1, 2, 5 y 10). Escribe tres funciones: la primera pieza de cualquier colección (o nada); cuántas piezas tiene una colección, aceptando «alguna colección» concreta; y el total de piezas de una lista que mezcla colecciones de tipos distintos. Imprime la primera pieza de cada colección, cuántas tiene cada una (en una línea) y el total de las dos. Lo que debe salir:

Ana
1
3 4
7
Ver solución
solución: colecciones de cromos y monedas
protocol Coleccion {
    associatedtype Pieza
    var piezas: [Pieza] { get }
}
 
struct Cromos: Coleccion { let piezas = ["Ana", "Iván", "Luis"] }
struct Monedas: Coleccion { let piezas = [1, 2, 5, 10] }
 
func primera<C: Coleccion>(_ coleccion: C) -> C.Pieza? { coleccion.piezas.first }
 
func cuantas(_ coleccion: some Coleccion) -> Int { coleccion.piezas.count }
 
func total(_ colecciones: [any Coleccion]) -> Int {
    var suma = 0
    for coleccion in colecciones { suma += coleccion.piezas.count }
    return suma
}
 
print(primera(Cromos()) ?? "ninguna")
print(primera(Monedas()) ?? 0)
print(cuantas(Cromos()), cuantas(Monedas()))
print(total([Cromos(), Monedas()]))

Salida: Ana, 1, 3 4 y 7. primera y cuantas aceptan un tipo concreto cada vez (genérico y some); total necesita any porque la lista mezcla dos tipos.

Paso · funciones sobre protocolos y goleadores

Objetivo. Añadir a Ranking.swift el protocolo Clasificacion y las funciones que lo usan (con some y any), pegar los goleadores y probarlo en main.swift. Al ejecutar torneo-cli verás la lista de goleadores con su puesto.

Torneo
Sources
Torneo
Ranking.swiftse modifica
torneo-cli
main.swiftse modifica
Tests
TorneoTests
RankingTests.swiftse crea y se rellena con «Copiar fichero»
Lo que tocas, en el Project navigator de Xcode con el paquete Torneo abierto. Un fichero nuevo, dos que cambian.

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.

Dónde va cada cosa. Todo lo de Ranking.swift va debajo de lo que ya tienes, una parte debajo de otra, en el orden de esta lección.

Lo que exigen los tests que copiarás. Las funciones lider, nombres (con la etiqueta de) y cuantosPuestosHay. Y goleadores, sobre una lista de partidos y sobre Torneo, que devuelve un ranking de jugadores con desempate por nombre.

Parte 3a · El protocolo Clasificacion y lider(_:)

Piezas nuevas:

  • associatedtype Elemento: el hueco del protocolo, como en Contenedor.
  • typealias Elemento = T: un nombre nuevo para un tipo. Aquí rellena el hueco: «en un Ranking<T>, el Elemento es T». Y extension Ranking: Clasificacion es la forma de que un tipo ya escrito adopte un protocolo desde fuera.
  • C.Elemento: el tipo que haya elegido el C de cada llamada.

Con el Contenedor de arriba se ve claro: Nombres rellenaba el hueco sin decirlo (Swift lo deducía de [String]). Ranking<T> es genérico, así que aquí se escribe a mano: typealias Elemento = T.

Intenta escribirlo tú: el protocolo público Clasificacion, con un hueco para el tipo de sus elementos, Elemento, y una lista ordenados de solo lectura; que Ranking lo adopte, con sus elementos como Elemento; y la función lider, que recibe cualquier clasificación sin etiqueta y devuelve su primer elemento, o nada si está vacía.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo/Sources/Torneo/Ranking.swift
// Un protocolo con un hueco: `Elemento` lo rellena cada tipo que lo adopta.
public protocol Clasificacion {
    associatedtype Elemento
    var ordenados: [Elemento] { get }
}
 
// Ranking adopta el protocolo: aquí el hueco `Elemento` se rellena con `T`.
extension Ranking: Clasificacion {
    public typealias Elemento = T
}
 
// Función genérica sobre el protocolo: `C.Elemento` es el tipo que ese `C` haya elegido.
public func lider<C: Clasificacion>(_ clasificacion: C) -> C.Elemento? {
    clasificacion.ordenados.first
}

Parte 3b · nombres(de:) y cuantosPuestosHay(_:)

Piezas nuevas:

  • some Sequence<Jugador>: Sequence es el protocolo que pegaste en la lección anterior. Algunos protocolos de Swift dejan decir entre <> de qué tipo son sus elementos: Sequence<Jugador> quiere decir «una secuencia cuyos elementos son Jugador». Con some delante, la función acepta cualquier secuencia de jugadores (un array, un Ranking…) y es una forma corta de escribir un genérico.
  • any Clasificacion: aquí sí hace falta any, porque la lista mezcla clasificaciones de tipos distintos (el caso del apartado anterior).
  • reduce(0) { $0 + $1.ordenados.count }: va acumulando un valor a partir del inicial (0), elemento a elemento; aquí suma cuántos elementos tiene cada clasificación.

Las piezas, con el Contenedor de arriba:

ejemplo mínimo: some Sequence<String>, una lista de any y reduce
// some Sequence<String>: cualquier secuencia de textos (un array, un Set…).
func enMayusculas(_ palabras: some Sequence<String>) -> [String] {
    palabras.map { $0.uppercased() }
}
 
// [any Contenedor]: una lista que mezcla Nombres y Dorsales.
func totalDeElementos(_ contenedores: [any Contenedor]) -> Int {
    // reduce(0): empieza en 0; $0 es lo acumulado y $1 el contenedor de esta vuelta.
    contenedores.reduce(0) { $0 + $1.todos.count }
}
 
print(enMayusculas(["sol", "mar"]))                      // ["SOL", "MAR"]
print(totalDeElementos([Nombres(), Dorsales()]))         // 5

Intenta escribirlo tú, debajo de lider: nombres, que recibe con la etiqueta de cualquier secuencia de jugadores y devuelve sus nombres; y cuantosPuestosHay, que recibe sin etiqueta una lista de clasificaciones de tipos distintos y devuelve cuántos elementos suman entre todas.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo/Sources/Torneo/Ranking.swift
// `some Sequence<Jugador>`: «algún tipo concreto que es una secuencia de Jugador».
public func nombres(de jugadores: some Sequence<Jugador>) -> [String] {
    jugadores.map(\.nombre)   // `\.nombre` es una ruta de propiedad: atajo de `{ $0.nombre }`
}
 
// `any Clasificacion`: cualquier clasificación; permite una lista de tipos distintos,
// a cambio de perder el tipo del `Elemento` y de un pequeño coste al usarla.
public func cuantosPuestosHay(_ clasificaciones: [any Clasificacion]) -> Int {
    clasificaciones.reduce(0) { $0 + $1.ordenados.count }
}

Otra pieza que se pega: los goleadores

Cuenta los goles de cada jugador (un diccionario «jugador → goles») y devuelve un Ranking<Jugador> con desempate por nombre. Usa piezas de las colecciones que se ven con calma en Colecciones y key paths, así que va hecho. Una frase de cada una:

  • flatMap recorre una lista de listas y las junta en una sola: [["Ana", "Luis"], ["Ana"]].flatMap { $0 } da ["Ana", "Luis", "Ana"]. Aquí junta los goleadores de todos los partidos.
  • reduce(into:) recorre los elementos y va rellenando un valor que tú inicias: .reduce(into: [String: Int]()) empieza con un diccionario vacío y, en cada vuelta, lo actualiza. Aquí cuenta cuántas veces sale cada jugador, es decir, sus goles.
  • where Element == Partido: esta extensión solo existe cuando los elementos del array son exactamente Partido.
  • [Jugador: Int]() es un diccionario vacío de jugador a número; acumulado[jugador, default: 0] += 1 suma 1 a lo que hay para ese jugador, empezando en 0 si aún no está; goles.keys son sus claves, y [Jugador](goles.keys) las convierte en un array.

Pega el bloque al final de Ranking.swift.

El nombre goleadores aparece tres veces con sentidos distintos: el método de Partido (de Closures con listas) da la lista de jugadores que marcaron, y estos dos, el de [Partido] y el de Torneo, dan un Ranking<Jugador>.

Ranking.swift: goleadores (para pegar)
// Solo para arrays de partidos: `[Partido]` también es `Array<Partido>`.
extension Array where Element == Partido {
    public func goleadores() -> Ranking<Jugador> {
        // `flatMap` junta en una sola lista los goleadores de todos los partidos.
        // `reduce(into:)` va rellenando un diccionario «jugador → goles».
        let goles = flatMap { $0.goleadores() }.reduce(into: [Jugador: Int]()) { acumulado, jugador in
            // Si el jugador aún no está, parte de 0; cada gol suma 1.
            acumulado[jugador, default: 0] += 1
        }
        return Ranking(
            elementos: [Jugador](goles.keys),      // los jugadores que marcaron (un array de Jugador)
            puntos: { goles[$0] ?? 0 },            // sus goles
            desempate: { $0.nombre < $1.nombre }   // a igualdad de goles, por nombre
        )
    }
}
 
extension Torneo {
    // El mismo ranking, sobre los partidos del torneo.
    public func goleadores() -> Ranking<Jugador> { partidos.goleadores() }
}

Probarlo en main.swift

El bloque va entre la línea de comentario // Genéricos: … y let porLongitud, en main.swift (Sources → torneo-cli). Recuerda que Equipo(…) es un init? (devuelve un opcional) y que el ! del final lo abre. Intenta escribirlo tú: crea el equipo Lobos, con Pedro y el dorsal 4, y un Toros contra Lobos con un gol de Iván para Toros en el minuto 20; saca los goleadores de ese partido y de alFinal, e imprime cada uno con su puesto, su etiqueta y sus goles; después, el líder y los nombres de todos. Lo que debes ver, delante de la línea del ranking de textos:

1. Ana (#9): 2
1. Iván (#7): 2
Líder: Ana; nombres: ["Ana", "Iván"]
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo/Sources/torneo-cli/main.swift
let lobos = Equipo(nombre: "Lobos", plantilla: [Jugador(nombre: "Pedro", dorsal: 4)])!
let partidoDeToros = Partido(local: toros, visitante: lobos).registrando(.gol(minuto: 20, jugador: ivan, equipo: toros))
// Los goleadores de dos partidos, ya ordenados: Ana e Iván con 2 goles cada uno.
let tablaDeGoleadores = [alFinal, partidoDeToros].goleadores()
// Un Ranking se recorre con `for` porque es una Sequence.
for jugador in tablaDeGoleadores {
    print("\(tablaDeGoleadores.puesto(de: jugador) ?? 0). \(jugador.etiqueta()): \(tablaDeGoleadores.puntosDe(jugador))")
}
print("Líder: \(lider(tablaDeGoleadores)?.nombre ?? "nadie"); nombres: \(nombres(de: tablaDeGoleadores))")

Compila y ejecuta antes de copiar nada. Desde la Terminal, en ~/Marcador-Swift/Torneo:

xcrun swift build
xcrun swift run torneo-cli

Los tests

RankingTests.swift son siete comprobaciones de Ranking con tipos distintos (uno con String, otro con Int, otro con jugadores), 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.

  1. En el Project navigator, clic derecho sobre Tests → TorneoTests → New File from Template… → Swift File → Next → RankingTests → Create. (Plan B: touch Tests/TorneoTests/RankingTests.swift desde ~/Marcador-Swift/Torneo.)
  2. Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
  3. Haz clic dentro de RankingTests.swift, selecciona todo con Cmd+A y pega con Cmd+V.
RankingTests.swift

Ahora pruébalos: xcrun swift test desde ~/Marcador-Swift/Torneo.

Un límite que verás. Ana e Iván tienen 2 goles cada uno, y los dos salen con el puesto 1. puesto(de:) mira los puntos y no el desempate: empatados en puntos, comparten puesto aunque uno vaya antes en la lista. Kotlin tiene el mismo límite.

Punto de control

  • xcrun swift build (desde ~/Marcador-Swift/Torneo) compila sin errores.
  • xcrun swift run torneo-cli imprime 1. Ana (#9): 2 y 1. Iván (#7): 2, después Líder: Ana; nombres: ["Ana", "Iván"] y Ranking de textos: ["Rayo FC", "Toros", "Lobos"]: el mismo Ranking ordena jugadores por goles y textos por longitud.
  • xcrun swift test termina con Test run with 58 tests in 8 suites passed: los 51 de la lección anterior más los 7 nuevos de RankingTests.
  • Sabes decir cuándo usarías some y cuándo any.

Comprueba lo que entendiste

4 preguntas · necesitas 3 para completar la lección

  1. 1.¿Qué es un associatedtype en un protocolo?

  2. 2.Necesitas una lista con dos tipos distintos que cumplen Contenedor. ¿Qué escribes?

  3. 3.func nombresOrdenados() -> some Contenedor devuelve siempre Nombres(). ¿Qué ve quien llama?

  4. 4.Torneo guarda let reglamento: any Reglamento y no some Reglamento. ¿Por qué?

Faltan 4 de 4