Saltar al contenido
K
Swift: el lenguajePaso a paso45 min Intermedio

struct frente a class

Al terminar: Distinguir tipos de valor y de referencia, copy-on-write y mutating.

Al terminar esta lección Equipo existirá en Swift como class, con un inicializador que puede fallar, un capitán y su igualdad por identidad, y sabrás decir qué diferencia a una class de un struct. En Kotlin esa decisión casi no la tomabas: una data class y una class se pasan de mano en mano de la misma manera. En Swift la misma decisión cambia lo que ocurre cuando copias una variable.

Es la pregunta de entrevista de Swift más repetida, y también la que más errores evita cuando la respondes bien. Temas: tipos de valor frente a tipos de referencia, private(set) e inicializadores que pueden fallar, init?. La lección siguiente sigue con mutating, el copy-on-write y Partido y Torneo.

Dos formas de guardar una cosa

Una variable guarda algo. Lo que guarda puede ser de dos maneras:

Tipo de valor (struct, Int, String, las listas)Tipo de referencia (class)
Qué guarda la variableEl dato mismoUna flecha (referencia) hacia un objeto
Al asignar b = ab recibe su propia copiab recibe la misma flecha: a y b señalan al mismo objeto
Si cambias ba no se enteraa lo ve
Comparar identidadNo tiene sentido=== («¿es el mismo objeto?»)
Ejemplo en MarcadorPartido, JugadorEquipo, Torneo

Este ejemplo lo escribes en tu Playground de Xcode, el de Tu primer paquete de Swift: reutilízalo y borra lo anterior antes de pegar otro ejemplo, porque dos tipos con el mismo nombre chocan. Una palabra nueva: final class es una clase de la que nadie puede heredar (heredar es construir una clase nueva sobre otra). Es la costumbre en Swift, y la usarás en todo el curso; en Kotlin las clases ya eran finales por defecto.

ejemplo mínimo: copia frente a referencia
struct Punto { var x = 0 }
var a = Punto()
var b = a          // copia
b.x = 5
print(a.x, b.x)
 
final class Caja { var x = 0 }
let c = Caja()
let d = c          // la misma caja
d.x = 5
print(c.x, d.x)

print con dos valores separados por coma los escribe en la misma línea. Imprime 0 5 (el struct se copió, a no cambió) y 5 5 (la class es una sola caja con dos nombres). Aunque d es un let, pudo cambiar x: un let sobre una clase congela la flecha, no el objeto.

private(set): leer desde fuera, cambiar solo dentro

Otra palabra que verás en seguida. private(set) var valor deja que cualquiera lea valor, pero solo el código de dentro del propio tipo puede cambiarlo. Es lo que en Kotlin escribiste como var valor … private set.

ejemplo mínimo: private(set)
final class Contador {
    private(set) var valor = 0       // se lee desde fuera; solo se cambia aquí dentro
    func sumar() { valor += 1 }
}
 
let contador = Contador()
contador.sumar()
print(contador.valor)                // 1
// contador.valor = 5                // error: cannot assign to property: 'valor' setter is inaccessible

init?: un constructor que puede decir «no»

Tu Equipo de Kotlin lanzaba una excepción desde init con require(...) si el nombre estaba vacío o había dorsales repetidos. Swift ofrece otra vía: el inicializador que puede fallar, init?. Devuelve un opcional: el objeto si los datos valen, nil si no. (El opcional y ?. están en Opcionales.)

Para rechazar datos usa guard condición else { return nil }: si la condición es falsa se ejecuta el else, que tiene que salir de la función. Es el guard let de Opcionales sin el let: comprueba una condición cualquiera en lugar de abrir un opcional.

ejemplo mínimo: init? que puede fallar
struct Dorsal {
    let numero: Int
 
    init?(_ numero: Int) {
        guard (1...99).contains(numero) else { return nil }
        self.numero = numero
    }
}
 
print(Dorsal(7)?.numero ?? -1)
print(Dorsal(0)?.numero ?? -1)

Imprime 7 y -1. Quien crea un Dorsal recibe un Dorsal? y el compilador le obliga a decidir qué hace con el nil. Dos detalles del ejemplo: (1...99).contains(numero) pregunta si numero está entre 1 y 99, y la ! delante de una condición la invierte (!texto.isEmpty es «el texto no está vacío»; la usarás en el ejercicio).

Error típico: comparar dos Equipo con ==

Si comparas dos Equipo con == sin haber escrito Equatable (el protocolo que da sentido a ==, que se ve en Protocolos y extensiones), el compilador dice:

error: binary operator '==' cannot be applied to two 'Equipo' operands

Un struct puede pedir Equatable y el compilador escribe la comparación; una class tiene que escribirla ella, y en Equipo «el mismo equipo» significa «el mismo objeto» (===), que es lo que hacía == en tu clase de Kotlin sin equals. Lo escribes en el tramo 4 del paso.

Tu Kotlin y este Swift, línea a línea

Kotlin (tu módulo :torneo)Swift (tu paquete Torneo)Diferencia
class Equipo (final por defecto)final class EquipoEn Swift hay que escribir final
require(nombre.isNotBlank()) en initguard !nombre.isEmpty else { return nil } en init?Sin excepción. isEmpty solo rechaza el texto vacío: un nombre de solo espacios pasa (isNotBlank lo rechazaba)
== de una clase compara identidad== no existe hasta que escribes EquatableSwift no adivina
require(jugador in plantilla) en nombrarCapitanguard plantilla.contains(jugador) else { return false }Sin excepción: devuelve si pudo
var capitan … private setpublic private(set) var capitan: Jugador?Se lee desde fuera; solo se cambia dentro

Reto · Ejercicio: copiar o compartir, y una sesión que dice no

En tu Playground: un struct, NotaPropia, y una clase final, NotaCompartida, los dos con un valor que empieza en 0. Guarda cada uno en una constante, asígnalo a una segunda variable, súmale 1 al valor a través de la segunda e imprime en una línea el valor de las dos. Después, una clase final, Sesion, con un usuario de texto que no cambia y un inicializador que puede fallar: si el usuario está vacío, no hay sesión. Intenta crear una con el usuario vacío y otra con «ana», e imprime el usuario de cada una o «sin sesión». Lo que debe salir, y piensa por qué las dos primeras líneas son distintas:

0 1
1 1
sin sesión
ana
Ver solución
solución del ejercicio
struct NotaPropia { var valor = 0 }
final class NotaCompartida { var valor = 0 }
 
let propia = NotaPropia()
var otraPropia = propia          // copia: un struct
otraPropia.valor += 1
print(propia.valor, otraPropia.valor)
 
let compartida = NotaCompartida()
let otraCompartida = compartida  // la misma nota: una class
otraCompartida.valor += 1
print(compartida.valor, otraCompartida.valor)
 
final class Sesion {
    let usuario: String
    init?(usuario: String) {
        guard !usuario.isEmpty else { return nil }
        self.usuario = usuario
    }
}
print(Sesion(usuario: "")?.usuario ?? "sin sesión")
print(Sesion(usuario: "ana")?.usuario ?? "sin sesión")

Imprime 0 1 (el struct se copió: el original sigue en 0), 1 1 (la clase es un solo objeto), sin sesión y ana. Nota: aunque compartida es un let, la clase deja cambiar su valor: el let congela la flecha, no el objeto.

Paso · Equipo

Objetivo. Crear Equipo, una clase con init?, capitán e igualdad por identidad, y comprobarlo en la consola. Al terminar, xcrun swift run torneo-cli imprime dos líneas nuevas al final: Rayo FC (3 jugadores), capitán: Ana y Toros con dorsales repetidos: nil.

Torneo/Sources/Torneo/
Equipo.swiftse crea
Torneo/Sources/torneo-cli/
main.swiftse modifica
Un fichero nuevo y uno modificado. Rutas dentro de ~/Marcador-Swift.

Crea el fichero. 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 (Equipo) y pulsa Create. Si el menú de tu versión de Xcode se llama distinto, hazlo desde la Terminal (SwiftPM detecta solos los ficheros nuevos de Sources/Torneo):

touch Sources/Torneo/Equipo.swift

Comprueba que queda dentro de Sources/Torneo: el objetivo al que pertenece un fichero lo decide la carpeta donde está. Lo vas a rellenar por tramos pequeños.

Tramo 1 · Equipo: nombre y plantilla

Piezas nuevas:

  • public final class Equipo { … }: una clase (se comparte, no se copia). public la hace visible desde el programa de consola.
  • [Jugador]: el tipo «lista de Jugador»; los corchetes rodean el tipo de los elementos.
  • public init?(…) con guard … else { return nil }: el inicializador que puede fallar, de arriba.

Los ejemplos de este paso usan otro mundo, el de una biblioteca: un Libro (como tu Jugador, con un código que puede faltar) y una Estanteria que guarda una lista de libros. Puedes ir juntándolos en tu Playground y ejecutarlos.

ejemplo mínimo: una clase con una lista y un init?
// Un struct con un opcional, como Jugador.
public struct Libro: Equatable, Hashable {
    public let titulo: String
    public let codigo: Int?                 // un libro puede no tener código
}
 
public final class Estanteria {             // final class: se comparte y nadie hereda de ella
    public let etiqueta: String
    public let libros: [Libro]              // [Libro]: una lista cuyos elementos son Libro
 
    // init?: puede fallar y devolver nil en vez de una Estanteria.
    public init?(etiqueta: String, libros: [Libro]) {
        guard !etiqueta.isEmpty else { return nil }   // sin etiqueta, no se crea
        self.etiqueta = etiqueta            // si llegamos aquí, los datos valen
        self.libros = libros
    }
}
 
print(Estanteria(etiqueta: "", libros: []) == nil)   // true: el init? dijo que no

Intenta escribirlo tú: Equipo, una clase pública de la que nadie hereda, con un nombre y una plantilla de jugadores que no cambian, y un inicializador con esas dos etiquetas que puede fallar: si el nombre está vacío, no se crea ningún equipo.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Equipo.swift: tramo 1, el fichero entero
// Una clase: los `Equipo` se comparten, no se copian. `final`: nadie hereda de ella.
public final class Equipo {
    public let nombre: String
    public let plantilla: [Jugador]    // `[Jugador]`: una lista de Jugador
 
    // `init?` puede fallar: devuelve nil si los datos no valen.
    public init?(nombre: String, plantilla: [Jugador]) {
        guard !nombre.isEmpty else { return nil }   // sin nombre, no hay equipo
        self.nombre = nombre
        self.plantilla = plantilla
    }
}

Tramo 2 · Rechazar dorsales repetidos

Piezas nuevas:

  • plantilla.compactMap(\.dorsal): recorre la plantilla, se queda con el dorsal de cada jugador y descarta los nil (los jugadores sin dorsal no cuentan como repetidos). \.dorsal es una ruta a la propiedad (un key path), atajo de una closure (un trozo de código que se pasa como dato) que verás en Closures.
  • Set(dorsales): un conjunto, que no guarda repetidos; si tiene menos elementos que la lista, había dorsales repetidos.

Las dos piezas sueltas, con libros:

ejemplo mínimo: compactMap con un key path y un Set
let libros = [
    Libro(titulo: "Atlas", codigo: 3),
    Libro(titulo: "Apuntes", codigo: nil),   // sin código
    Libro(titulo: "Mapas", codigo: 3),       // repite el 3
]
// Coge el código de cada libro y tira los nil: [3, 3]
let codigos = libros.compactMap(\.codigo)
// Un Set quita los repetidos: {3}, con 1 elemento
print(codigos.count, Set(codigos).count)    // 2 1: distintos, así que había repetidos

Intenta escribirlo tú: en el inicializador, justo después de comprobar el nombre, que tampoco se cree el equipo si dos jugadores de la plantilla tienen el mismo dorsal (los que no tienen dorsal no cuentan).

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Equipo.swift: tramo 2, dentro del init?, tras el primer guard
        // `compactMap` recorre la plantilla y se queda con el dorsal de cada jugador,
        // descartando los nil. `\.dorsal` es una ruta a la propiedad, atajo de una closure.
        let dorsales = plantilla.compactMap(\.dorsal)
        // Un `Set` no guarda repetidos: si tiene menos elementos que la lista, había repetidos.
        guard dorsales.count == Set(dorsales).count else { return nil }

Tramo 3 · El capitán

Piezas nuevas:

  • public private(set) var capitan: Jugador?: se lee desde fuera y solo se cambia dentro de la clase (private(set), de arriba). Un opcional var empieza en nil solo.
  • @discardableResult: permite llamar a nombrarCapitan sin usar el Bool que devuelve, sin aviso del compilador.
  • var cantidadDeJugadores: Int { plantilla.count }: una propiedad calculada; se recalcula cada vez que la lees.

Las tres piezas son pequeñas: private(set) es el del ejemplo Contador de arriba, guard … else { return false } es el guard de init? devolviendo un Bool, y la propiedad calculada es una var con el cuerpo entre llaves en lugar de un valor.

Intenta escribirlo tú, dentro de la clase, debajo del inicializador: capitan, que empieza sin nadie, se lee desde fuera y solo cambia dentro; nombrarCapitan, que recibe un jugador sin etiqueta, lo nombra capitán solo si está en la plantilla y dice si lo hizo (quien llama puede ignorar la respuesta); y cantidadDeJugadores, que cuenta la plantilla cada vez que se lee.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Equipo.swift: tramo 3, dentro de la clase, debajo del init?
    // Se lee desde fuera; solo se cambia aquí dentro. Empieza en nil.
    public private(set) var capitan: Jugador?
 
    // Devuelve false si el jugador no está en la plantilla.
    // `@discardableResult`: se puede llamar sin usar el Bool que devuelve.
    @discardableResult
    public func nombrarCapitan(_ jugador: Jugador) -> Bool {
        guard plantilla.contains(jugador) else { return false }
        capitan = jugador
        return true
    }
 
    // Propiedad calculada: se recalcula cada vez que se lee.
    public var cantidadDeJugadores: Int { plantilla.count }

Tramo 4 · Igualdad y texto de Equipo

Piezas nuevas:

  • extension Equipo: Equatable { … }: una extension añade cosas a un tipo desde fuera de su declaración (la lección de Protocolos y extensiones la explica). Hace falta porque el Partido de la lección siguiente será Equatable, y eso solo compila si Equipo también lo es.
  • static func ==: define qué significa == para dos equipos. Recibe el de la izquierda y el de la derecha y devuelve un Bool; static quiere decir que es de «los equipos en general», no de uno concreto. Dentro se usa ===, porque «el mismo equipo» significa «el mismo objeto».
  • extension Equipo: CustomStringConvertible con description: es el texto que sale al hacer print(equipo); aquí, Rayo FC (3 jugadores).

Intenta escribirlo tú, fuera de la clase, al final de Equipo.swift, dos extensiones: una que hace que se puedan comparar con == (dos son iguales solo si son el mismo objeto) y otra que hace que un equipo se imprima como «Rayo FC (3 jugadores)».

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Equipo.swift: tramo 4, al final del fichero, fuera de la clase
// Dos Equipo son iguales solo si son el mismo objeto (`===` compara identidad).
// `static`: es de los equipos en general, no de uno concreto.
extension Equipo: Equatable {
    public static func == (izquierda: Equipo, derecha: Equipo) -> Bool {
        izquierda === derecha
    }
}
 
// El texto que sale al hacer `print(equipo)`.
extension Equipo: CustomStringConvertible {
    public var description: String { "\(nombre) (\(cantidadDeJugadores) jugadores)" }
}

Falta un último trozo de Equipo que no escribes: Hashable, el protocolo que permite usar un Equipo como clave de un diccionario (el Map de Kotlin) o meterlo en un Set. Todavía no lo usas, pero las lecciones siguientes sí, y Protocolos y extensiones explica cómo funciona. Pégalo tal cual al final de Equipo.swift:

Equipo.swift: Hashable, para pegar al final del fichero
// Hashable: permite usar un Equipo como clave de un diccionario o en un Set.
// Como la igualdad es por identidad, aquí también se usa el identificador del objeto.
extension Equipo: Hashable {
    public func hash(into hasher: inout Hasher) {
        hasher.combine(ObjectIdentifier(self))
    }
}

Compila con xcrun swift build desde Torneo: debe terminar con Build complete!.

Tramo 5 · Verlo en la consola

Piezas nuevas:

  • Equipo(…)!: el init? devuelve un Equipo?; el ! lo abre a la fuerza. Aquí los datos son válidos, así que no será nil.
  • jugadores[0]: el elemento de la posición 0 de la lista; las posiciones empiezan en 0, como en las listas de Kotlin.
  • === compara identidad (¿es el mismo objeto?) y solo existe entre clases; lo usarás en la lección siguiente.

Intenta escribirlo tú, al final de main.swift, debajo de lo que ya tienes (usa la lista jugadores que ya existe). Primero: Ana es el primer jugador de la lista, e Iván, uno nuevo con el 7 (lo usarás en la lección siguiente); crea el Rayo FC con los jugadores (sabiendo que se puede crear), nombra capitana a Ana e imprime el equipo con su capitán; después intenta crear un Toros con dos jugadores con el dorsal 7 e imprime si salió o no. Lo que debes ver:

Rayo FC (3 jugadores), capitán: Ana
Toros con dorsales repetidos: nil
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
main.swift: fragmento, el equipo y el que no se puede crear
// Struct frente a class.
let ana = jugadores[0]                          // el primer jugador de la lista: Ana
let ivan = Jugador(nombre: "Iván", dorsal: 7)
 
// `Equipo(...)` devuelve un opcional (es un `init?`); el `!` lo abre a la fuerza.
let rayo = Equipo(nombre: "Rayo FC", plantilla: jugadores)!
rayo.nombrarCapitan(ana)                        // sin usar el Bool que devuelve
let nombreDelCapitan = rayo.capitan?.nombre ?? "sin capitán"
print("\(rayo), capitán: \(nombreDelCapitan)")
 
// Dos jugadores con el mismo dorsal: el `init?` devuelve nil.
let conRepetidos = Equipo(nombre: "Toros", plantilla: [Jugador(nombre: "A", dorsal: 7), Jugador(nombre: "B", dorsal: 7)])
if conRepetidos == nil {
    print("Toros con dorsales repetidos: nil")
} else {
    print("Toros con dorsales repetidos: creado")
}

Ejecuta xcrun swift run torneo-cli: al final deben salir las dos líneas que ves en el punto de control de abajo.

Punto de control

  • Build. Desde ~/Marcador-Swift/Torneo, xcrun swift build termina en Build complete!.
  • Run. xcrun swift run torneo-cli termina con estas dos líneas (Rayo FC (3 jugadores) cuenta los tres jugadores del ejemplo):
Rayo FC (3 jugadores), capitán: Ana
Toros con dorsales repetidos: nil
  • Test. xcrun swift test termina con Test run with 16 tests in 3 suites passed (ignora la línea Executed 0 tests de XCTest): los de las lecciones anteriores siguen pasando.
  • Sabes decir por qué un Equipo es una class (tiene identidad: el Rayo FC es ese equipo) y por qué Equipo(...) devuelve un opcional (es un init? y los datos pueden no valer).

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.Tienes var b = a y después cambias b. ¿En qué caso a también cambia?

  2. 2.¿Qué devuelve un inicializador init? cuando los datos no valen?

  3. 3.En Marcador, ¿por qué Equipo es una class y Jugador un struct?

  4. 4.Escribes let caja = Caja() donde Caja es una class con var x = 0, y después caja.x = 5. ¿Qué pasa?

  5. 5.Comparas dos Equipo con == y el compilador dice binary operator '==' cannot be applied to two 'Equipo' operands. ¿Qué falta?

Faltan 5 de 5