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

Genéricos en Swift

Al terminar: Escribir genéricos, entender los associated types y distinguir some de any.

Marcador necesita clasificaciones de cosas distintas: goleadores (jugadores puntuados por sus goles) y, más adelante, otras con otros elementos. Al terminar esta lección tendrás un solo Ranking<T> que sirve para todas, y lo probarás con una lista de textos ordenada por longitud: ["Rayo FC", "Toros", "Lobos"].

Temas: genéricos, restricciones y extensiones condicionales. En la lección siguiente, some y any, los protocolos ganan un hueco de tipo y sacarás la lista de goleadores con su puesto.

Genéricos: un tipo que se decide al usarlo

Una función o un tipo genérico lleva entre < y > un nombre de marcador (T, Elemento) en lugar de un tipo concreto. El tipo real se decide en cada uso y el compilador lo comprueba.

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

ejemplo mínimo: una función y un tipo genéricos (con comentarios)
// T: Comparable es una restricción: solo tipos que se pueden comparar con >.
// `a > b ? a : b` es el operador ternario: si `a > b` vale `a` y, si no, `b` (en Kotlin, `if (a > b) a else b`).
func mayor<T: Comparable>(_ a: T, _ b: T) -> T { a > b ? a : b }
 
struct Pila<Elemento> {
    private var items: [Elemento] = []
    mutating func apilar(_ x: Elemento) { items.append(x) }
    mutating func sacar() -> Elemento? { items.popLast() }
}
 
print(mayor(3, 7), mayor("rayo", "toros"))
var pila = Pila<Int>()
pila.apilar(1)
pila.apilar(2)
print(pila.sacar() as Any, pila.sacar() as Any, pila.sacar() as Any)

Imprime:

7 toros
Optional(2) Optional(1) nil

Comparable es un protocolo de Swift: lo cumplen los tipos que se pueden ordenar con < y >, como Int y String (los textos, por orden alfabético: por eso gana "toros").

mutating marca un método de un struct que cambia el propio struct. popLast() quita y devuelve el último elemento, o nil si no hay ninguno; por eso sacar() devuelve un opcional. (as Any solo evita un aviso del compilador por imprimir un opcional directamente.) Dos piezas más que usa el ejercicio: private(set) var cosas se puede leer desde fuera pero solo cambia dentro del tipo, y max() da el mayor elemento de una lista de cosas comparables, o nil si la lista está vacía ([3, 12, 7].max() es 12).

Una extensión condicional añade métodos solo cuando el tipo cumple una condición. Es la respuesta de Swift a algo que Kotlin no necesita: en Kotlin todo objeto tiene equals, así que elemento !in ordenados compila con cualquier T. En Swift, para saber si un elemento «está» hay que poder compararlo, y eso se pide con where Elemento: Equatable:

ejemplo mínimo: extensión condicional
extension Pila where Elemento: Equatable {
    func contiene(_ x: Elemento) -> Bool { items.contains(x) }
}
struct Marca { let valor: Int }          // no es Equatable
 
var enteros = Pila<Int>()
enteros.apilar(5)
print(enteros.contiene(5))               // true: Int es Equatable

Con una Pila<Marca>, la llamada a contiene no compila.

Kotlin ↔ Swift

Kotlin (el tuyo)Swift
class Ranking<T>struct Ranking<T>
fun <T> primero(lista: List<T>): Tfunc primero<T>(_ lista: [T]) -> T; la restricción <T: Comparable> es nueva
: Iterable<T> con iterator(): Sequence con makeIterator()
compareByDescending { … }.then(desempate)sorted { a, b in … }: si los puntos son distintos gana el mayor y, si no, decide el desempate
elemento !in ordenados (todo objeto tiene equals)extension … where T: Equatable: hay que pedir la comparación

Reto · Ejercicio: cajas de mudanza

En tu Playground, haz una caja genérica que sirva para cualquier cosa: guarda sus cosas (se leen desde fuera, solo cambian dentro), tiene una función para meter una y otra que dice cuántas hay. Solo cuando las cosas se pueden ordenar, la caja sabe darte la mayor (o nada, si está vacía). Crea una caja de textos con «Momo» y «Dune», una de platos (un plato tiene un color; mete uno blanco) y una de números con 3, 12 y 7. Imprime cuántas cosas tiene cada caja (en una línea), la mayor de los textos y la mayor de los números. Después intenta pedirle la mayor a la caja de platos y anota qué dice el compilador. Lo que debe salir antes de ese último intento:

2 1 3
Momo
12
Ver solución
solución: cajas de mudanza
struct Caja<Cosa> {
    private(set) var cosas: [Cosa] = []
 
    mutating func meter(_ cosa: Cosa) {
        cosas.append(cosa)
    }
 
    func cuantas() -> Int { cosas.count }
}
 
extension Caja where Cosa: Comparable {
    func laMayor() -> Cosa? { cosas.max() }
}
 
struct Plato { let color: String }
 
var libros = Caja<String>()
libros.meter("Momo")
libros.meter("Dune")
var platos = Caja<Plato>()
platos.meter(Plato(color: "blanco"))
var pesos = Caja<Int>()
pesos.meter(3)
pesos.meter(12)
pesos.meter(7)
 
print(libros.cuantas(), platos.cuantas(), pesos.cuantas())
print(libros.laMayor() ?? "vacía")
print(pesos.laMayor() ?? 0)

Con platos.laMayor() el compilador dice que Plato tiene que cumplir Comparable: es el mismo error que el de Pila<Marca>, con otro protocolo.

Paso · Ranking<T>

Objetivo. Escribir Ranking.swift en dos partes (el struct genérico y puesto(de:)), pegar una pieza que no es de genéricos (Sequence) y probarlo en main.swift con una lista de textos. Al ejecutar torneo-cli verás el ranking de textos.

Torneo
Sources
Torneo
Ranking.swiftse crea
torneo-cli
main.swiftse modifica
Lo que tocas, en el Project navigator de Xcode con el paquete Torneo abierto. Un fichero nuevo y uno que cambia.

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.

Crear Ranking.swift. 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 Ranking 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/Ranking.swift. Las partes siguientes van una debajo de otra en ese mismo fichero.

Lo que exigen los tests que copiarás en la lección siguiente. Ranking se crea con las etiquetas elementos, puntos y desempate (esta, opcional); tiene ordenados, puntosDe y top y, solo cuando sus elementos se pueden comparar, puesto, con la etiqueta de (los empatados en puntos comparten puesto; nada si el elemento no está). Ranking se puede recorrer con for.

Parte 1 · El struct genérico

Piezas nuevas:

  • desempate: (T, T) -> Bool = { _, _ in false }: un parámetro que es una closure, con valor por defecto («nadie va antes»). Los dos _ ignoran los dos parámetros de la closure: no los necesita.
  • @escaping: puntos se guarda en el struct para usarlo después de que el init termine, y por eso lo lleva; desempate solo se usa dentro del init y no lo necesita.
  • sorted { a, b in … }: ordena con una closure que responde «¿va a antes que b?».
  • a != b ? x : y: el ternario de la lección, para elegir entre dos valores según una condición.
  • ordenados.prefix(cantidad) da los primeros cantidad elementos (o menos, si no hay tantos) y Array(…) los convierte en un array.

Las piezas en otro tipo genérico: un catálogo que ordena productos de más barato a más caro. Pruébalo en tu Playground.

ejemplo mínimo: un struct genérico que guarda una closure y ordena
// <T>: el tipo de lo que hay en el catálogo; se decide al crearlo.
struct Catalogo<T> {
    private let precio: (T) -> Int              // una closure guardada: cómo se calcula el precio
 
    let deBaratoACaro: [T]
 
    init(
        productos: [T],
        precio: @escaping (T) -> Int,           // @escaping: se guarda y se usa después del init
        empate: (T, T) -> Bool = { _, _ in false }   // valor por defecto: nadie va antes
    ) {
        self.precio = precio
        // sorted pregunta «¿va a antes que b?» para cada pareja que compara.
        self.deBaratoACaro = productos.sorted { a, b in
            precio(a) != precio(b) ? precio(a) < precio(b) : empate(a, b)
        }
    }
 
    func precioDe(_ producto: T) -> Int { precio(producto) }   // usa la closure guardada
 
    // prefix(n): los n primeros (o menos); Array(…) lo convierte en array.
    func losMasBaratos(_ n: Int) -> [T] { Array(deBaratoACaro.prefix(n)) }
}
 
let frutas = Catalogo(productos: ["sandía", "kiwi", "pera"], precio: { $0.count })
print(frutas.deBaratoACaro)          // ["kiwi", "pera", "sandía"]: por número de letras: kiwi y pera empatan
print(frutas.losMasBaratos(2))       // ["kiwi", "pera"]

Intenta escribirlo tú: Ranking, un struct público y genérico (sirve para jugadores, textos o lo que sea). Se crea con los elementos, una forma de puntuar cada uno, que guarda, y un desempate que dice si un elemento va antes que otro (si no se da, nadie va antes). Tiene ordenados, los elementos de más a menos puntos y, a igualdad, según el desempate; puntosDe, que recibe un elemento sin etiqueta y da sus puntos; y top, que recibe una cantidad sin etiqueta y da los primeros (o menos, si no hay tantos). Después compara con el bloque, que está comentado línea a línea.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo/Sources/Torneo/Ranking.swift
// `<T>` es el tipo de lo que se clasifica (Jugador, String…): se decide al usarlo.
public struct Ranking<T> {
    // Cómo se puntúa un elemento. Se GUARDA en el struct, por eso `@escaping` en el `init`.
    private let puntos: (T) -> Int
 
    // Los elementos, de más a menos puntos.
    public let ordenados: [T]
 
    public init(
        elementos: [T],
        // `@escaping`: se guarda en `puntos` y se usa después de que el `init` termine.
        puntos: @escaping (T) -> Int,
        // «¿va `a` antes que `b`?» para los empates. Por defecto, nadie va antes.
        desempate: (T, T) -> Bool = { _, _ in false }
    ) {
        self.puntos = puntos
        // `sorted` ordena con una closure que responde «¿va `a` antes que `b`?».
        self.ordenados = elementos.sorted { a, b in
            let puntosDeA = puntos(a)
            let puntosDeB = puntos(b)
            // Si los puntos son distintos, va antes el que más tiene; si no, decide el desempate.
            return puntosDeA != puntosDeB ? puntosDeA > puntosDeB : desempate(a, b)
        }
    }
 
    // Los puntos de un elemento: llama a la closure guardada.
    public func puntosDe(_ elemento: T) -> Int { puntos(elemento) }
 
    // Los primeros `cantidad`: `prefix` da menos si no hay tantos, y `Array(…)` lo vuelve array.
    public func top(_ cantidad: Int) -> [T] { Array(ordenados.prefix(cantidad)) }
}

Una pieza que se pega: recorrer el Ranking con for

Una Sequence es cualquier cosa que se recorre con for … in; es un protocolo con un único requisito, makeIterator(), que entrega los elementos de uno en uno. Aquí basta con pedírselo al array ordenados; el tipo IndexingIterator<[T]> no hace falta aprenderlo, solo escribirlo. A cambio, Ranking se puede recorrer y recibe gratis map, filter, contains… No es el tema de la lección, así que va hecho: pega este bloque debajo de la parte 1, en Ranking.swift.

Ranking.swift: Sequence (para pegar)
// Con `Sequence`, un Ranking se recorre con `for … in` y gana `map`, `filter`…
extension Ranking: Sequence {
    // Devuelve el iterador de `ordenados`: el que entrega los elementos uno a uno.
    public func makeIterator() -> IndexingIterator<[T]> { ordenados.makeIterator() }
}

Parte 2 · Una extensión condicional: puesto(de:)

Piezas nuevas:

  • extension Ranking where T: Equatable: este método solo existe cuando T se puede comparar con ==.
  • firstIndex(where: { … }): la posición del primer elemento que cumple la condición, o nil si ninguno.
  • guard … else { return nil }: sale de la función si la condición falla.

Las tres piezas, con números y con una bandeja genérica (guard, que ya conoces de guard let y textos que no son números, sale de la función si algo falta):

ejemplo mínimo: firstIndex(where:) y guard en una extensión condicional
let numeros = [4, 9, 2, 9]
// firstIndex(where:): la posición (desde 0) del primero que cumple la condición, o nil.
print(numeros.firstIndex(where: { $0 > 5 }) as Any)    // Optional(1)
print(numeros.firstIndex(where: { $0 > 50 }) as Any)   // nil
 
struct Bandeja<T> { let cosas: [T] }
// Solo existe si T se puede comparar con ==.
extension Bandeja where T: Equatable {
    func posicion(de cosa: T) -> Int? {
        // Si no está, sale con nil; si está, su posición humana (empieza en 1).
        guard let indice = cosas.firstIndex(where: { $0 == cosa }) else { return nil }
        return indice + 1
    }
}
print(Bandeja(cosas: ["a", "b"]).posicion(de: "b") as Any)    // Optional(2)

Intenta escribirlo tú: puesto, que existe solo cuando los elementos se pueden comparar, recibe un elemento con la etiqueta de y devuelve su puesto con empates (dos con los mismos puntos comparten puesto: 1, 1, 3…), o nada si el elemento no está.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Ranking.swift: puesto(de:)
// Extensión CONDICIONAL: este método solo existe cuando `T` es `Equatable`.
// Para saber si un elemento «está» hay que poder compararlo; con un `T`
// cualquiera no se puede (en Kotlin, `!in` usa `equals` de cualquier objeto).
extension Ranking where T: Equatable {
    public func puesto(de elemento: T) -> Int? {
        // Si el elemento no está en el ranking, no tiene puesto.
        guard ordenados.contains(elemento) else { return nil }
        let suyos = puntos(elemento)
        // La posición del primero que tiene los mismos puntos…
        guard let primero = ordenados.firstIndex(where: { puntos($0) == suyos }) else { return nil }
        // …más 1, porque las posiciones empiezan en 0 y los puestos en 1.
        return primero + 1
    }
}

Probarlo en main.swift

El bloque va justo antes de let torneo = Torneo(…), en main.swift (Sources → torneo-cli), detrás de las líneas de la lección anterior. Intenta escribirlo tú: un ranking de los textos «Rayo FC», «Toros» y «Lobos», puntuados por su longitud, e imprime sus elementos ordenados (\.count es una ruta de propiedad, un atajo de { $0.count }, como en Equatable y Hashable). Lo que debes ver:

Ranking de textos: ["Rayo FC", "Toros", "Lobos"]
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo/Sources/torneo-cli/main.swift
// Genéricos: el mismo Ranking sirve para jugadores y para cualquier otra cosa.
let porLongitud = Ranking(elementos: ["Rayo FC", "Toros", "Lobos"], puntos: \.count)
print("Ranking de textos: \(porLongitud.ordenados)")

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

xcrun swift build
xcrun swift run torneo-cli
xcrun swift test

Punto de control

  • xcrun swift build (desde ~/Marcador-Swift/Torneo) compila sin errores.
  • xcrun swift run torneo-cli imprime, después de las líneas de las lecciones anteriores, Ranking de textos: ["Rayo FC", "Toros", "Lobos"]: el Ranking ordena textos por su longitud.
  • xcrun swift test termina con Test run with 51 tests in 7 suites passed: los mismos de la lección anterior. Los tests de Ranking llegan en la lección siguiente, cuando ya existan las funciones que usan.
  • Provócalo: en main.swift, cambia puntos: \.count por puntos: \.first y ejecuta xcrun swift build; el compilador dice algo como key path value type 'String.Element?' (aka 'Optional<Character>') cannot be converted to contextual type 'Int'. \.first da la primera letra, que puede faltar, y puntos tiene que devolver un número. Devuelve \.count.
  • Sabes decir para qué sirve una restricción como T: Comparable y por qué puesto(de:) solo existe para tipos Equatable.

Sigue con some y any: el hueco de tipo de un protocolo, some frente a any y la lista de goleadores.

Comprueba lo que entendiste

4 preguntas · necesitas 3 para completar la lección

  1. 1.En func mayor<T: Comparable>(_ a: T, _ b: T) -> T, ¿qué significa T: Comparable?

  2. 2.¿Por qué puesto(de:) vive en extension Ranking where T: Equatable?

  3. 3.¿Qué imprime print(mayor("rayo", "toros")) con func mayor<T: Comparable>(_ a: T, _ b: T) -> T?

  4. 4.Una Pila<Marca> donde Marca no es Equatable. ¿Qué pasa al llamar a contiene de extension Pila where Elemento: Equatable?

Faltan 4 de 4