Swift de Cero a Experto #13: Protocolos
Un protocolo es un contrato, no una clase. Dice qué debe hacer un tipo sin decir qué es — y esa sola idea impulsa la delegación, el Equatable sintetizado, los existenciales y las extensiones de protocolo que hacen idiomático a Swift.
En el artículo anterior hicimos lanzables nuestros enums conformándolos a un protocolo diminuto: Error. No tenía métodos ni propiedades — conformarlo era solo una marca que decía “un valor de este tipo puede lanzarse”. Ese fue tu primer protocolo, y escondió la idea entera a plena vista. Un protocolo es un contrato: declara qué debe ser capaz de hacer un tipo, mientras calla por completo sobre qué es ese tipo.
Esa separación es el punto. La herencia del #10 deja que los tipos compartan comportamiento solo compartiendo una superclase — un árbol genealógico rígido, de un solo padre, al que solo pueden unirse las clases. Un protocolo deja que tipos no relacionados — un struct, un enum, una clase, incluso tipos que no escribiste tú — prometan la misma capacidad sin compartir un ancestro. Un Circle y un Country no tienen nada en común, y aun así ambos pueden prometer “tengo un area”. Esa promesa es un protocolo.
Una clase dice “soy una clase de Animal”. Un protocolo dice “puedo hacer lo que hace un Drawable”. Uno trata de identidad y ascendencia; el otro, de capacidad. Swift está construido sobre la segunda idea.
Sintaxis de protocolo: definir el contrato
Defines un protocolo muy parecido a un tipo, con la palabra clave protocol:
protocol SomeProtocol { // protocol definition goes here}Un tipo declara que adopta un protocolo escribiendo el nombre del protocolo tras el nombre del tipo, separados por dos puntos — exactamente la sintaxis que usaste para la herencia de clases. Varios protocolos se separan con comas, y si una clase tiene una superclase, la superclase va primero:
struct SomeStructure: FirstProtocol, AnotherProtocol { // structure definition goes here}
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol { // class definition goes here}Requisitos de propiedad
Un protocolo puede exigir que un tipo conforme provea una propiedad con un nombre y un tipo particulares. Lo crucial: no dice si esa propiedad es almacenada o computada — solo su nombre, su tipo, y si debe ser gettable, o gettable y settable.
Los requisitos de propiedad siempre se escriben con var. Escribes { get set } para un requisito de lectura-escritura y { get } para uno de solo lectura:
protocol SomeProtocol { var mustBeSettable: Int { get set } var doesNotNeedToBeSettable: Int { get }}La regla es sobre mínimos. Un requisito { get set } no puede satisfacerse con un let ni con una propiedad computada de solo lectura — el conformador debe ofrecer ambas. Un requisito { get } se satisface con cualquier propiedad, y está bien que esa propiedad sea además settable si te resulta útil.
Aquí un protocolo con un requisito de propiedad de instancia:
protocol FullyNamed { var fullName: String { get }}Un struct puede satisfacer fullName con una simple propiedad almacenada:
struct Person: FullyNamed { var fullName: String}let john = Person(fullName: "John Appleseed")// john.fullName is "John Appleseed"…y una clase puede satisfacer el mismo requisito con una propiedad computada del #9 — el protocolo nunca supo ni le importó la diferencia:
class Starship: FullyNamed { var prefix: String? var name: String init(name: String, prefix: String? = nil) { self.name = name self.prefix = prefix } var fullName: String { return (prefix != nil ? prefix! + " " : "") + name }}var ncc1701 = Starship(name: "Enterprise", prefix: "USS")// ncc1701.fullName is "USS Enterprise"
Requisitos de método
Un protocolo puede exigir métodos de instancia y de tipo. Los escribes exactamente como declaraciones normales de método, pero sin cuerpo y sin llaves — solo la firma:
protocol RandomNumberGenerator { func random() -> Double}RandomNumberGenerator exige un método, random(), que retorna un Double. No dice nada sobre cómo se produce el número — eso es asunto del conformador. Aquí una clase lo implementa como un generador congruencial lineal:
class LinearCongruentialGenerator: RandomNumberGenerator { var lastRandom = 42.0 let m = 139968.0 let a = 3877.0 let c = 29573.0 func random() -> Double { lastRandom = ((lastRandom * a + c) .truncatingRemainder(dividingBy: m)) return lastRandom / m }}Como con las propiedades, los requisitos de método de tipo se prefijan con static en el protocolo. Los valores por defecto de parámetros no se permiten en los requisitos de método de un protocolo — el contrato declara la firma, no comodidades por defecto.
Requisitos de método mutating
Un método sobre un tipo de valor — un struct o enum del #8 — que necesita cambiar self debe marcarse mutating. Cuando un protocolo exige un método así, lleva la palabra clave mutating al contrato, para que los tipos de valor puedan satisfacerlo:
protocol Togglable { mutating func toggle()}Un enum ahora puede conformar e invertir su propio estado:
enum OnOffSwitch: Togglable { case off, on mutating func toggle() { switch self { case .off: self = .on case .on: self = .off } }}var lightSwitch = OnOffSwitch.offlightSwitch.toggle()// lightSwitch is now equal to .onRequisitos de initializer
Un protocolo también puede exigir un initializer del #10 — de nuevo, solo la firma, sin cuerpo:
protocol SomeProtocol { init(someParameter: Int)}Una clase que satisface un requisito de initializer debe marcar su implementación como required. Esa palabra obliga a cada subclase a proveer también el initializer, de modo que toda la jerarquía de clases siga conformando:
class SomeClass: SomeProtocol { required init(someParameter: Int) { // initializer implementation goes here }}Protocolos como tipos: el existencial any
Hasta ahora un protocolo ha sido una lista de verificación. Pero un protocolo también es un tipo — puedes usarlo donde usarías Int o String: como tipo de una variable, parámetro de una función, tipo de elemento de un array. Ya te topaste con esto al final del #12, donde Result { try f() } producía un Result<Int, any Error>, y prometimos que la palabra clave any tendría su explicación aquí. Aquí está.
protocol Shape { func draw() -> String}
struct VerticalShapes { var shapes: [any Shape] func draw() -> String { return shapes.map { $0.draw() }.joined(separator: "\n\n") }}[any Shape] es un array cuyos elementos pueden ser cada uno de un tipo concreto distinto — un triángulo aquí, un cuadrado allá — siempre que cada uno conforme a Shape. Como la caja oculta el tipo concreto, solo puedes tocar lo que Shape mismo promete: draw() está bien; la propiedad size de un triángulo no, a menos que primero hagas un cast de vuelta al tipo concreto.
![Un arreglo [any Shape] mostrado como una fila de tres cajas. Cada caja presenta la misma tapa de interfaz Shape con draw(), pero envuelve dentro un tipo concreto distinto: Triangle, Square y Circle. Una nota indica que la caja es una indirección en runtime con coste de rendimiento, razón por la que se escribe any; los genéricos y el opaco some eliminan la caja.](/_astro/any-shape-existential-boxes.CReJR86O_Z1uRBIu.webp)
Delegación: un protocolo como entrega
Uno de los patrones de protocolo más antiguos y útiles es la delegación — un tipo que entrega parte de su trabajo a otro objeto que promete, vía un protocolo, encargarse de él.
Aquí un juego de dados reporta su progreso a un delegado. El juego guarda un delegado opcional y devuelve la llamada en momentos clave:
class DiceGame { let sides: Int let generator = LinearCongruentialGenerator() weak var delegate: Delegate?
init(sides: Int) { self.sides = sides }
func roll() -> Int { return Int(generator.random() * Double(sides)) + 1 }
func play(rounds: Int) { delegate?.gameDidStart(self) for round in 1...rounds { let player1 = roll() let player2 = roll() if player1 == player2 { delegate?.game(self, didEndRound: round, winner: nil) } else if player1 > player2 { delegate?.game(self, didEndRound: round, winner: 1) } else { delegate?.game(self, didEndRound: round, winner: 2) } } delegate?.gameDidEnd(self) }
protocol Delegate: AnyObject { func gameDidStart(_ game: DiceGame) func game(_ game: DiceGame, didEndRound round: Int, winner: Int?) func gameDidEnd(_ game: DiceGame) }}Dos patrones de capítulos anteriores sostienen el diseño entero. delegate es Delegate?, así que cada llamada pasa por el optional chaining del #11 — delegate?.gameDidStart(self). Si nadie está escuchando, la llamada simplemente no hace nada. Y el delegado es una referencia weak para evitar un ciclo de retención, que es exactamente por qué el protocolo hereda de AnyObject — haciéndolo class-only (más sobre esto abajo).
Cualquier clase puede volverse un rastreador conformando:
class DiceGameTracker: DiceGame.Delegate { var playerScore1 = 0 var playerScore2 = 0 func gameDidStart(_ game: DiceGame) { playerScore1 = 0 playerScore2 = 0 } func game(_ game: DiceGame, didEndRound round: Int, winner: Int?) { switch winner { case 1: playerScore1 += 1 case 2: playerScore2 += 1 default: break } } func gameDidEnd(_ game: DiceGame) { if playerScore1 == playerScore2 { print("The game ended in a draw.") } else if playerScore1 > playerScore2 { print("Player 1 won!") } else { print("Player 2 won!") } }}
let tracker = DiceGameTracker()let game = DiceGame(sides: 6)game.delegate = trackergame.play(rounds: 3)
DiceGame nunca se entera de qué clase de objeto es tracker. Solo conoce el contrato — y ese es el poder entero de la delegación.
Agregar conformidad con una extensión
Puedes hacer que un tipo conforme a un protocolo en una extensión, separada de su declaración original — incluso para un tipo cuyo código fuente no posees. Esto es modelado retroactivo (retroactive modeling): enseñarle a Int, a Array o al tipo de otra persona un truco nuevo.
protocol TextRepresentable { var textualDescription: String { get }}
extension Dice: TextRepresentable { var textualDescription: String { return "A \(sides)-sided dice" }}Tras esta extensión, cada Dice — incluyendo los creados antes de que se escribiera la extensión — es un TextRepresentable. La conformidad aplica a todas las instancias existentes y futuras.
Conformidad condicional con where
Un tipo genérico puede conformar a un protocolo solo cuando sus elementos lo hacen. Lo expresas con una cláusula where en la extensión:
extension Array: TextRepresentable where Element: TextRepresentable { var textualDescription: String { let itemsAsText = self.map { $0.textualDescription } return "[" + itemsAsText.joined(separator: ", ") + "]" }}Aquí Element es el nombre genérico incorporado para cualquier tipo que el array contenga — verás los generics en condiciones en el #14. Ahora [Dice] es TextRepresentable (porque Dice lo es), pero [SomeRandomType] no. La conformidad es condicional al tipo del elemento.
Declarar adopción con una extensión vacía
Si un tipo ya cumple cada requisito pero no lo ha dicho, una extensión vacía oficializa la adopción:
struct Hamster { var name: String var textualDescription: String { return "A hamster named \(name)" }}extension Hamster: TextRepresentable {}Conformidad sintetizada: Equatable, Hashable, Comparable
Algunas conformidades son tan mecánicas que Swift las escribe por ti. Para Equatable, Hashable y Comparable, a menudo solo declaras la conformidad y paras — el compilador sintetiza la implementación.
Declara Equatable sobre un struct cuyas propiedades almacenadas son todas Equatable, y == se genera por ti (comparando cada propiedad), junto con un != gratis:
struct Vector3D: Equatable { var x = 0.0, y = 0.0, z = 0.0}
let a = Vector3D(x: 2.0, y: 3.0, z: 4.0)let b = Vector3D(x: 2.0, y: 3.0, z: 4.0)if a == b { print("These two vectors are equivalent.")}Hashable se sintetiza bajo las mismas condiciones — structs cuyas propiedades almacenadas son todas Hashable, y enums cuyos valores asociados son todos Hashable (o que no tienen ninguno). Eso es lo que deja que tus propios tipos se vuelvan miembros de Set y claves de Dictionary, del #4.
Comparable se sintetiza para enumeraciones sin raw value; si los cases llevan valores asociados, esos también deben ser Comparable. El orden del código fuente define el orden — los cases anteriores son “menores que” los posteriores:
enum SkillLevel: Comparable { case beginner case intermediate case expert(stars: Int)}var levels = [SkillLevel.intermediate, SkillLevel.beginner, SkillLevel.expert(stars: 5), SkillLevel.expert(stars: 3)]for level in levels.sorted() { print(level)}// beginner// intermediate// expert(stars: 3)// expert(stars: 5)Definir < una vez te compra <=, > y >= gratis, porque Comparable provee esos encima del único operador que tú (o el compilador) suministras.
Colecciones de tipos de protocolo
Como un protocolo es un tipo, puedes almacenar tipos concretos mezclados en un array siempre que compartan un protocolo. Este es el array existencial de antes, puesto a trabajar:
let things: [any TextRepresentable] = [game, d12, simonTheHamster]
for thing in things { print(thing.textualDescription)}Dentro del loop, thing es estáticamente any TextRepresentable — no DiceGame, Dice ni Hamster, aunque eso es lo que cada caja realmente contiene. Puedes llamar a textualDescription porque el protocolo lo garantiza; alcanzar cualquier cosa específica del tipo concreto requeriría un cast.
Herencia de protocolos
Un protocolo puede heredar otros protocolos, agregando requisitos encima. La sintaxis refleja la herencia de clases, pero puedes listar varios padres:
protocol PrettyTextRepresentable: TextRepresentable { var prettyTextualDescription: String { get }}Cualquier cosa que adopte PrettyTextRepresentable debe satisfacer tanto su propio requisito prettyTextualDescription como el heredado textualDescription. El hijo puede apoyarse en la garantía del padre — una implementación de PrettyTextRepresentable puede llamar a textualDescription, sabiendo que siempre está ahí.
Protocolos class-only con AnyObject
Por defecto cualquier clase de tipo puede adoptar un protocolo. Para restringir la adopción a solo clases, hereda de AnyObject:
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol { // class-only protocol definition goes here}Un struct o enum que intente adoptarlo es un error de compilación. Esta es la regla que hizo funcionar a DiceGame.Delegate: AnyObject: solo los tipos de referencia pueden mantenerse weak, así que un protocolo de delegado que quiera una referencia débil debe ser class-only.
Composición de protocolos con &
A veces un solo protocolo no es un requisito lo bastante fuerte — necesitas un valor que conforme a varios a la vez. Combínalos con & en una composición de protocolos:
protocol Named { var name: String { get }}protocol Aged { var age: Int { get }}struct Person: Named, Aged { var name: String var age: Int}func wishHappyBirthday(to celebrator: Named & Aged) { print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")}let birthdayPerson = Person(name: "Malcolm", age: 21)wishHappyBirthday(to: birthdayPerson)// Happy birthday, Malcolm, you're 21!Named & Aged significa “cualquier tipo que conforme a ambos”. No define ningún protocolo nuevo con nombre — es una combinación temporal, en el momento. Una composición también puede incluir una clase, para exigir una superclase particular junto a los protocolos:
func beginConcert(in location: Location & Named) { print("Hello, \(location.name)!")}Aquí location debe ser una subclase de Location y conformar a Named — capacidad y ascendencia, exigidas juntas.
Verificar conformidad con is y as?
Los operadores is y as? — que conocimos atrapando errores por tipo — también prueban conformidad de protocolo en tiempo de ejecución, con sintaxis idéntica:
isretornatruesi la instancia conforma al protocolo.as?retorna un opcional del tipo del protocolo —nilsi la instancia no conforma.as!hace un cast forzado y hace trap si la instancia no conforma.
protocol HasArea { var area: Double { get }}
class Circle: HasArea { let pi = 3.1415927 var radius: Double var area: Double { return pi * radius * radius } init(radius: Double) { self.radius = radius }}class Country: HasArea { var area: Double init(area: Double) { self.area = area }}class Animal { var legs: Int init(legs: Int) { self.legs = legs }}
let objects: [AnyObject] = [ Circle(radius: 2.0), Country(area: 243_610), Animal(legs: 4),]
for object in objects { if let objectWithArea = object as? HasArea { print("Area is \(objectWithArea.area)") } else { print("Something that doesn't have an area") }}// Area is 12.5663708// Area is 243610.0// Something that doesn't have an areaCircle, Country y Animal no comparten ninguna clase base — y aun así as? HasArea separa a los conformadores del resto en runtime. Este es el mismo if let optional binding del #11, aquí desempacando el resultado de un cast de protocolo. Los objetos no cambian; solo se ven a través de la ventana angosta de HasArea mientras dura el binding.
Extensiones de protocolo: implementaciones por defecto
Aquí es donde los protocolos dejan de ser meras listas de verificación y se vuelven la característica definitoria de Swift. Puedes extender un protocolo mismo para proveer implementaciones de método y de propiedad — comportamiento que cada tipo conforme obtiene gratis, sin escribir nada.
extension RandomNumberGenerator { func randomBool() -> Bool { return random() > 0.5 }}Cada RandomNumberGenerator tiene ahora randomBool(), construido sobre el random() que ya prometía — y ningún conformador tuvo que mover un dedo:
let generator = LinearCongruentialGenerator()print(generator.random()) // 0.3746499199817101print(generator.randomBool()) // true
Así que un protocolo puede tanto declarar un requisito como darle un default — dejando que los conformadores hagan override solo cuando tengan algo mejor que ofrecer:
extension PrettyTextRepresentable { var prettyTextualDescription: String { return textualDescription }}Este es el corazón de la programación orientada a protocolos: el comportamiento compartido vive en el protocolo, atado a una capacidad en vez de a un lugar en un árbol de clases. Donde la herencia del #10 fuerza al código compartido a bajar por una sola línea de descendencia, una extensión de protocolo lo esparce de lado a cada conformador, relacionado o no.
La herencia comparte código hacia abajo, a través de los hijos de una clase. Una extensión de protocolo lo comparte hacia los lados, a cada tipo que opta por entrar — sin superclase, sin árbol genealógico, sin permiso. Ese alcance lateral es por qué a Swift se le llama orientado a protocolos.
Extensiones restringidas con where
Puedes restringir una extensión de protocolo para que sus miembros solo aparezcan cuando los tipos conformes cumplen restricciones extra — escritas, de nuevo, con una cláusula genérica where:
extension Collection where Element: Equatable { func allEqual() -> Bool { for element in self { if element != self.first { return false } } return true }}allEqual() existe sobre cualquier Collection del #4, pero solo cuando su Element es Equatable — porque el cuerpo necesita != para comparar elementos. Los arrays de enteros califican:
let equalNumbers = [100, 100, 100, 100, 100]let differentNumbers = [100, 100, 200, 100, 200]
print(equalNumbers.allEqual()) // trueprint(differentNumbers.allEqual()) // falseEste patrón de extensión restringida está por todas partes en la librería estándar — es cómo Array obtiene sorted() solo cuando sus elementos son Comparable, y contains(_:) solo cuando son Equatable. Una capacidad que depende de otra capacidad, expresada con precisión.
Recapitulación

- Protocolo — un contrato de requisitos (propiedades, métodos, initializers) que un tipo conforme debe satisfacer; sobre capacidad, no identidad
- Requisitos — propiedades (
{ get }/{ get set }), métodos (solo firma), métodosmutating(para dejar entrar a los tipos de valor) einit(las clases lo marcanrequired) - Protocolos como tipos —
any ProtocolNamees un existencial en caja que contiene cualquier tipo conforme en runtime, al costo de una caja - Delegación — un tipo entrega trabajo a un protocolo de delegado
weak, opcional y class-only, llamándolo a través de optional chaining - Conformidad vía extensiones — agrégala retroactivamente, condicionalmente con
where, o decláradla con una extensión vacía; la conformidad siempre es explícita - Conformidad sintetizada —
Equatable,HashableyComparablese autogeneran para structs y enums simples en el archivo original - Herencia de protocolos — un protocolo puede heredar otros y agregar requisitos;
AnyObjecthace un protocolo class-only - Composición (
&) —A & Bexige conformidad a varios protocolos (más opcionalmente una superclase) a la vez is/as?— verifica y castea conformidad de protocolo en runtime, exactamente como el type casting- Extensiones de protocolo — implementaciones por defecto compartidas de lado a todos los conformadores;
wherelas restringe a los tipos que califican — la columna vertebral del Swift orientado a protocolos
Desafíos
Abre un playground y prueba cada uno antes de abrir la solución.
Desafío 1: Predice el toggle
Un tipo de valor satisface un requisito de protocolo mutating. ¿Qué imprime esto?
protocol Togglable { mutating func toggle()}enum OnOffSwitch: Togglable { case off, on mutating func toggle() { switch self { case .off: self = .on case .on: self = .off } }}var lightSwitch = OnOffSwitch.offlightSwitch.toggle()lightSwitch.toggle()lightSwitch.toggle()print(lightSwitch)Ver solución
onEl enum empieza en .off. Cada toggle() invierte self, así que el estado recorre off → on → off → on. Tres toggles caen en on. Como el protocolo lleva la palabra clave mutating, un tipo de valor como un enum puede reasignar self y aún así satisfacer el contrato.
Desafío 2: Arregla el existencial
Este fragmento no compila. Encuentra el único error y corrígelo para que el bucle se ejecute.
protocol Shape { func draw() -> String}struct Triangle: Shape { var size: Int func draw() -> String { return "Triangle" }}let shapes: [any Shape] = [Triangle(size: 3), Triangle(size: 7)]for shape in shapes { print(shape.size)}Ver solución
for shape in shapes { print(shape.draw())}// Triangle// TriangleDentro del bucle shape es estáticamente any Shape, no Triangle. La caja oculta el tipo concreto, así que solo puedes tocar lo que el protocolo Shape promete — draw(). La propiedad size pertenece a Triangle, y alcanzarla a través del existencial es un error en tiempo de compilación (value of type 'any Shape' has no member 'size'). Para usar size tendrías que castear de vuelta primero con as?.
Desafío 3: Escribe una implementación por defecto
Define un protocolo Greeter con un único requisito name: String { get } y una extensión de protocolo que dé a cada conformador un método greeting() -> String que devuelva "Hello, <name>!". Luego haz un Robot que use el valor por defecto y un Pirate que sobreescriba greeting() para devolver "Arr, I be <name>!".
Ver solución
protocol Greeter { var name: String { get }}extension Greeter { func greeting() -> String { return "Hello, \(name)!" }}struct Robot: Greeter { var name: String}struct Pirate: Greeter { var name: String func greeting() -> String { return "Arr, I be \(name)!" }}print(Robot(name: "R2").greeting()) // Hello, R2!print(Pirate(name: "Redbeard").greeting()) // Arr, I be Redbeard!Robot no escribe ningún greeting(), pero obtiene uno gratis de la implementación por defecto de la extensión de protocolo, construida sobre el name que prometió. Pirate provee su propio greeting(), y ese gana sobre el valor por defecto aquí porque greeting() se llama sobre el tipo concreto Pirate. Nota la sutileza: greeting() vive solo en una extensión de protocolo, no como un requisito de Greeter, así que este override gana por dispatch estático sobre el tipo concreto — llama al mismo método a través de any Greeter y obtendrías el valor por defecto de la extensión en su lugar. Para que el override gane en todos los sitios de llamada, declara greeting() -> String como un requisito real de Greeter.
Lo que viene
Seguimos chocando con un muro: any Shape y [any TextRepresentable] encajan sus valores a un costo de runtime, y un protocolo con un associatedtype (que deliberadamente saltamos aquí) ni siquiera puede usarse como any sin límites. La respuesta son los Generics — el próximo artículo — donde el llamador elige un tipo concreto, el compilador especializa el código, y la caja desaparece. Hemos estado escribiendo where Element: Equatable y Result<Success, Failure> todo este tiempo; el #14 por fin explica la maquinaria. Después de eso, el #15 cubre los tipos opacos (some) — la tercera puerta junto a any y los generics.
Nos vemos la próxima semana.
Un protocolo es la promesa más pequeña posible: “un valor de este tipo puede hacer esto”. De esa sola idea Swift construye la delegación, la equatabilidad, los existenciales y las extensiones de protocolo que dejan fluir el comportamiento de lado a través de tipos no relacionados. La herencia pregunta qué eres; un protocolo pregunta solo qué puedes hacer — y esa pregunta escala más lejos.
Referencias
Relacionados
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #12: Manejo de Errores
throw, try, do/catch, defer, rethrows, Result y los typed throws de Swift 6. Un error no es una excepción mágica — es solo un valor de un tipo que conforma Error, enrutado por tu call stack tan barato como un return.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #11: Opcionales y Optional Chaining
El antídoto de Swift al error de mil millones de dólares. Optional es solo un enum — y nil, if let, guard let, ??, ! y el optional chaining compilan a preguntar en qué case estás.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #10: Herencia e Inicialización
Subclassing, overriding y super. Designated vs convenience initializers, two-phase initialization, failable y required init, y deinit — el ciclo de vida completo de una class, explicado en memoria.