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 variable | El dato mismo | Una flecha (referencia) hacia un objeto |
Al asignar b = a | b recibe su propia copia | b recibe la misma flecha: a y b señalan al mismo objeto |
Si cambias b | a no se entera | a lo ve |
| Comparar identidad | No tiene sentido | === («¿es el mismo objeto?») |
| Ejemplo en Marcador | Partido, Jugador | Equipo, 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.
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.
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 inaccessibleinit?: 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.
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' operandsUn 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 Equipo | En Swift hay que escribir final |
require(nombre.isNotBlank()) en init | guard !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 Equatable | Swift no adivina |
require(jugador in plantilla) en nombrarCapitan | guard plantilla.contains(jugador) else { return false } | Sin excepción: devuelve si pudo |
var capitan … private set | public 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
anaVer solución
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.
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.swiftComprueba 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).publicla hace visible desde el programa de consola.[Jugador]: el tipo «lista deJugador»; los corchetes rodean el tipo de los elementos.public init?(…)conguard … 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.
// 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 noIntenta 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
// 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 eldorsalde cada jugador y descarta losnil(los jugadores sin dorsal no cuentan como repetidos).\.dorsales 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:
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 repetidosIntenta 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
// `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 opcionalvarempieza ennilsolo.@discardableResult: permite llamar anombrarCapitansin usar elBoolque 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
// 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 { … }: unaextensionañade cosas a un tipo desde fuera de su declaración (la lección de Protocolos y extensiones la explica). Hace falta porque elPartidode la lección siguiente seráEquatable, y eso solo compila siEquipotambién lo es.static func ==: define qué significa==para dos equipos. Recibe el de la izquierda y el de la derecha y devuelve unBool;staticquiere 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: CustomStringConvertiblecondescription: es el texto que sale al hacerprint(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
// 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:
// 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(…)!: elinit?devuelve unEquipo?; 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: nilIntenta primero. Cuando termines, compara con la solución.Mostrar el código
// 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 buildtermina enBuild complete!. - Run.
xcrun swift run torneo-clitermina 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 testtermina conTest run with 16 tests in 3 suites passed(ignora la líneaExecuted 0 testsde XCTest): los de las lecciones anteriores siguen pasando. - Sabes decir por qué un
Equipoes unaclass(tiene identidad: el Rayo FC es ese equipo) y por quéEquipo(...)devuelve un opcional (es uninit?y los datos pueden no valer).
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Tienes
var b = ay después cambiasb. ¿En qué casoatambién cambia?2.¿Qué devuelve un inicializador
init?cuando los datos no valen?3.En Marcador, ¿por qué
Equipoes unaclassyJugadorunstruct?4.Escribes
let caja = Caja()dondeCajaes unaclassconvar x = 0, y despuéscaja.x = 5. ¿Qué pasa?5.Comparas dos
Equipocon==y el compilador dicebinary operator '==' cannot be applied to two 'Equipo' operands. ¿Qué falta?