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.
En el artículo anterior diseccionamos propiedades, métodos y subscripts — el tejido conectivo de todo tipo. Hoy entramos al ciclo de vida de una class de nacimiento a muerte: cómo hereda de otra class (herencia), cómo se le da vida (inicialización), y cómo se destruye (deinicialización).
Esta es una historia exclusiva de classes. En el #8 aprendimos que solo las classes tienen herencia, identidad y deinit. Todo lo que veremos hoy nace de ese hecho: una instancia de class vive en el heap, puede construirse encima de otra class, y Swift tiene que garantizar que cada stored property — incluidas las que hereda — tenga un valor válido antes de que la toques.
Inicializar no es “correr algo de código de setup”. Es un contrato que el compilador hace cumplir: cuando init retorna, cada stored property tiene un valor real — sin excepciones, sin nil por accidente.
Herencia: construir encima de otra class
Una class que no hereda de nada es una base class. A diferencia de otros lenguajes, Swift no tiene una class raíz universal — tus base classes empiezan limpias:
class Vehicle { var currentSpeed = 0.0 var description: String { "traveling at \(currentSpeed) miles per hour" } func makeNoise() { // do nothing — an arbitrary vehicle doesn't necessarily make a noise }}
let someVehicle = Vehicle()print(someVehicle.description)// traveling at 0.0 miles per hourPara hacer subclassing, escribe el nombre de la subclass, dos puntos, y el nombre de la superclass. La subclass gana automáticamente todo lo que tiene la superclass:
class Bicycle: Vehicle { var hasBasket = false}
let bicycle = Bicycle()bicycle.hasBasket = truebicycle.currentSpeed = 15.0 // heredado de Vehicleprint(bicycle.description) // también heredado// traveling at 15.0 miles per hourLas subclasses también pueden tener subclasses — la cadena es tan profunda como quieras:
class Tandem: Bicycle { var currentNumberOfPassengers = 0}Tandem hereda de Bicycle, que hereda de Vehicle. Una instancia de Tandem carga cada propiedad y método declarado en cualquier punto de esa cadena.

Overriding: reemplazar comportamiento heredado
class Train: Vehicle { override func makeNoise() { print("Choo Choo") }}
let train = Train()train.makeNoise() // "Choo Choo"Esa keyword override hace trabajo real. Recuerda del #8: los métodos de class usan dynamic dispatch por defecto — en runtime, Swift consulta la vtable para encontrar qué implementación ejecutar de verdad. El overriding es justo lo que le da sentido a esa indirección: el makeNoise() de Train reemplaza la entrada de Vehicle en la vtable para las instancias de Train.
Llamar hacia arriba con super
A veces quieres extender el comportamiento de la superclass en vez de reemplazarlo por completo. El prefijo super alcanza la versión de la superclass de un método, propiedad o subscript:
class Car: Vehicle { var gear = 1 override var description: String { super.description + " in gear \(gear)" }}
let car = Car()car.currentSpeed = 25.0car.gear = 3print(car.description)// traveling at 25.0 miles per hour in gear 3Car hace override de la computed property read-only description, llama a super.description para obtener el texto base, y luego le agrega lo suyo. Puedes hacer override de cualquier propiedad heredada con un getter personalizado (y setter, si aplica) — una subclass no puede saber si la propiedad heredada era stored o computed en la superclass; solo conoce su nombre y su tipo.
Override de observers en propiedades heredadas
También puedes agregar property observers a una propiedad heredada — aunque tú nunca la hayas declarado:
class AutomaticCar: Car { override var currentSpeed: Double { didSet { gear = Int(currentSpeed / 10.0) + 1 } }}
let automatic = AutomaticCar()automatic.currentSpeed = 35.0print(automatic.description)// traveling at 35.0 miles per hour in gear 4final: cerrar la puerta
Marca un método, propiedad o subscript como final para prohibir hacerle override. Marca toda la class como final para prohibir hacerle subclassing por completo:
final class APIClient { func fetch() { /* ... */ }}// class CachedClient: APIClient {} // ERROR — APIClient es finalComo vimos en el #8, final no es solo una herramienta de diseño — es una herramienta de rendimiento. Sin override posible, el compilador puede eliminar el lookup en la vtable y usar static dispatch, saltando directo al código.
Inicialización: el contrato
La regla de hierro: classes y structures deben asignar a todas sus stored properties un valor válido para cuando termine la inicialización. No existe un estado “sin inicializar” que puedas leer por accidente.
struct Fahrenheit { var temperature: Double init() { temperature = 32.0 }}
var f = Fahrenheit()print(f.temperature) // 32.0Si una propiedad tiene un valor por defecto en su declaración, ni siquiera necesitas asignarla en init — y los structs a los que no les escribes un initializer reciben uno memberwise gratis, exactamente como vimos en el #8. Las classes nunca reciben un memberwise init, y ahora vamos a ver por qué: la herencia convierte la inicialización de classes en un problema genuinamente más difícil.
Designated vs convenience initializers
Las classes tienen dos tipos de initializers, y esa distinción es el corazón de todo este capítulo.
class Food { var name: String init(name: String) { // designated self.name = name } convenience init() { // convenience self.init(name: "[Unnamed]") }}
let bacon = Food(name: "Bacon") // name es "Bacon"let mystery = Food() // name es "[Unnamed]"El init() convenience no toca name directamente — delega de lado al designated init(name:). Esa delegación es obligatoria, y se rige por tres reglas.
Las tres reglas de delegación
- Regla 1 — Un designated initializer debe llamar a un designated initializer de su superclass inmediata.
- Regla 2 — Un convenience initializer debe llamar a otro initializer de la misma class.
- Regla 3 — Un convenience initializer debe terminar llamando a un designated initializer.
El truco mnemotécnico que da el libro de Swift vale la pena memorizarlo:
Los designated initializers siempre delegan hacia arriba. Los convenience initializers siempre delegan de lado.

Two-phase initialization: bajo el capó
Aquí está la parte que explica por qué existen todas esas reglas. La inicialización de classes en Swift corre en dos fases, y el compilador lo hace cumplir.
¿Para qué tanto lío? Por seguridad de memoria. Una instancia de class ocupa una sola alocación en el heap que contiene el almacenamiento de cada class de su cadena. Hasta que cada uno de esos espacios tenga un valor real, el objeto no está formado del todo — y leer self significaría leer memoria sin inicializar.
El compilador hace cumplir cuatro safety checks para que esto sea hermético:
- Check 1 — Un designated init debe inicializar todas las propiedades que su propia class introduce antes de delegar hacia arriba a
super. - Check 2 — Un designated init debe delegar hacia arriba a
superantes de asignar un valor a una propiedad heredada (si no,superla sobrescribiría). - Check 3 — Un convenience init debe delegar a otro initializer antes de asignar a cualquier propiedad.
- Check 4 — Ningún initializer puede leer propiedades de instancia, llamar métodos de instancia, o usar
selfcomo valor hasta que la fase 1 termine.
Veamos cómo se despliegan las fases:
class Bicycle: Vehicle { var hasBasket: Bool init(hasBasket: Bool) { // FASE 1: primero pongo mi propia propiedad (safety check 1) self.hasBasket = hasBasket // ...luego delego hacia arriba (safety check 2) super.init() // FASE 2: ahora self es válido — puedo leerlo/personalizarlo (safety check 4) currentSpeed = 10.0 // propiedad heredada, ya es seguro tocarla }}
Este orden es exactamente lo que garantizan las Reglas 1–3. La fase 1 sube hasta lo alto de la cadena estableciendo el almacenamiento crudo; la fase 2 baja, y solo entonces self es un objeto real y usable.
Initializer inheritance y overriding
Aquí va una sorpresa si vienes de Objective-C o Java: las subclasses de Swift no heredan los initializers de su superclass por defecto. Esto evita que una subclass especializada se cree a través de un initializer genérico de la superclass que la deje a medio hacer.
Cuando escribes un initializer de subclass que coincide con un designated initializer de la superclass, le estás haciendo override — así que escribes override:
class Vehicle { var numberOfWheels = 0 var description: String { "\(numberOfWheels) wheel(s)" } // recibe un default initializer init() automáticamente}
class Bicycle: Vehicle { override init() { super.init() // delega hacia arriba primero numberOfWheels = 2 // luego personaliza la propiedad heredada }}
print(Bicycle().description) // "2 wheel(s)"Automatic initializer inheritance
Las subclasses sí heredan los initializers de la superclass automáticamente — pero solo cuando es seguro. Asumiendo que les das valores por defecto a todas las propiedades nuevas que tu subclass introduce:
- Regla 1 — Si tu subclass no define designated initializers, hereda todos los designated initializers de la superclass.
- Regla 2 — Si tu subclass implementa todos los designated initializers de la superclass (heredándolos vía Regla 1 o escribiéndolos), también hereda todos los convenience initializers de la superclass.
Por esto rara vez tienes que escribir initializers de boilerplate en la práctica. Aquí está la clásica jerarquía de tres niveles del libro de Swift que lo une todo:
class Food { var name: String init(name: String) { self.name = name } // designated convenience init() { self.init(name: "[Unnamed]") }}
class RecipeIngredient: Food { var quantity: Int init(name: String, quantity: Int) { // designated self.quantity = quantity super.init(name: name) } override convenience init(name: String) { // override del designated init de Food self.init(name: name, quantity: 1) }}
class ShoppingListItem: RecipeIngredient { var purchased = false // valor por defecto, no necesita init var description: String { "\(quantity) x \(name)" + (purchased ? " ✔" : " ✘") }}RecipeIngredient hace override del designated init(name:) de Food — esta vez como convenience initializer — por eso lleva override. Como ahora implementa todos los designated initializers de Food, hereda automáticamente el convenience init() de Food. Y ShoppingListItem solo introduce una propiedad con valor por defecto y ningún initializer, así que hereda todo:
let a = ShoppingListItem() // convenience init() heredadolet b = ShoppingListItem(name: "Bacon") // heredado, quantity por defecto 1let c = ShoppingListItem(name: "Eggs", quantity: 6) // designated init heredadoFailable initializers
A veces la inicialización debe poder fallar — parámetros inválidos, un recurso ausente, un estado inválido. Escribe init? y return nil en el punto de falla. El resultado es una instancia opcional.
struct Animal { let species: String init?(species: String) { if species.isEmpty { return nil } self.species = species }}
let giraffe = Animal(species: "Giraffe") // Animal? -> no nillet nothing = Animal(species: "") // Animal? -> nilYa has usado failable initializers sin saberlo. Los enums con raw values reciben init?(rawValue:) gratis, y las conversiones numéricas como Int(exactly:) son failable:
let pi = 3.14159let n = Int(exactly: pi) // nil — el valor no se puede preservar exactoLa falla se propaga: si un failable init delega a otro failable init que falla, todo el proceso aborta de inmediato:
class Product { let name: String init?(name: String) { if name.isEmpty { return nil } self.name = name }}
class CartItem: Product { let quantity: Int init?(name: String, quantity: Int) { if quantity < 1 { return nil } // falla antes de delegar hacia arriba self.quantity = quantity super.init(name: name) // también puede fallar -> aborta todo el init }}
CartItem(name: "sock", quantity: 2) // CartItem? -> no nilCartItem(name: "shirt", quantity: 0) // CartItem? -> nil (quantity inválido)CartItem(name: "", quantity: 1) // CartItem? -> nil (la superclass falla por name)Required initializers
Marca un initializer como required para forzar a que toda subclass lo implemente. Las subclasses también escriben required (no override) para propagar el requerimiento por la cadena:
class SomeClass { required init() { // ... }}
class SomeSubclass: SomeClass { required init() { // la subclass debe proveer esto }}Puedes saltarte escribirlo explícitamente si un initializer heredado ya satisface el requerimiento. required importa más para la conformidad a protocolos (los protocolos vienen más adelante en la serie) y para APIs estilo factory (código que crea instancias por ti a partir de un tipo base) — donde el sistema de tipos necesita la garantía de que el initializer existe en cada subclass concreta.
Deinitialization: el último acto
Este es el cierre de todo lo que cubrimos. Recuerda del #8: las classes viven en el heap y las gestiona ARC (Automatic Reference Counting). Cuando la última referencia al objeto desaparece — su reference count llega a 0 — ARC lo desaloca. Justo antes de que eso pase, tu deinit recibe una última oportunidad para limpiar:
class FileHandle { let path: String init(path: String) { self.path = path print("Opened \(path)") } deinit { print("Closing \(path)") // libera el recurso antes de la desalocación }}
var handle: FileHandle? = FileHandle(path: "/tmp/data")// "Opened /tmp/data"handle = nil// "Closing /tmp/data" — refcount llegó a 0, corrió deinit, luego se liberó la memoriaEl ejemplo Bank/Player del libro de Swift muestra por qué esto importa — deinit devuelve las monedas de un jugador al banco en el instante en que abandona el juego:
class Bank { static var coinsInBank = 10_000 static func distribute(coins requested: Int) -> Int { let toVend = min(requested, coinsInBank) coinsInBank -= toVend return toVend } static func receive(coins: Int) { coinsInBank += coins }}
class Player { var coinsInPurse: Int init(coins: Int) { coinsInPurse = Bank.distribute(coins: coins) } deinit { Bank.receive(coins: coinsInPurse) } // devuelve monedas al salir}
var playerOne: Player? = Player(coins: 100)print(Bank.coinsInBank) // 9900playerOne = nil // dispara deinit -> las monedas regresanprint(Bank.coinsInBank) // 10000init es el apretón de manos que garantiza un objeto válido. deinit es la despedida que devuelve lo que pidió prestado. ARC decide cuándo ocurre cada uno — tú decides qué hacen.
La memoria: el ciclo de vida en una imagen

Recapitulación
- Herencia — solo las classes la tienen; una subclass gana todo de su superclass y puede agregar más
- Overriding — reemplaza comportamiento heredado con
override; el compilador verifica que exista una coincidencia real - super — alcanza la versión de la superclass para extender en vez de reemplazar
- final — prohíbe override/subclassing; permite al compilador usar static dispatch
- Designated init — inicializa por completo su class, delega hacia arriba
- Convenience init — un atajo, delega de lado a un designated init
- Two-phase init — la fase 1 pone el almacenamiento subiendo la cadena, la fase 2 personaliza bajándola; cuatro safety checks lo hacen cumplir
- Sin herencia automática de inits — salvo bajo las dos reglas de herencia automática
- Failable init —
init?retorna un opcional; la falla se propaga por la delegación - Required init — toda subclass debe implementarlo
- deinit — corre justo antes de que ARC desaloque; el
deinitde la superclass siempre corre también
Lo que viene
En el próximo artículo exploramos Optionals y Optional Chaining — el antídoto de Swift al error de mil millones de dólares. Veremos cómo Optional es solo un enum bajo el capó, en qué se compilan realmente ?, !, if let, guard let y ??, y por qué el sistema de tipos te obliga a enfrentar la ausencia de un valor de frente.
Nos vemos la próxima semana.
Una class no es solo datos y métodos — es un ciclo de vida. Domina init y deinit y dejas de pelear con ARC; empiezas a diseñar con él.
Referencias
Relacionados
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #14: Genéricos
Un solo swapTwoValues, un solo Stack<Element>, un solo findIndex — escritos una vez, funcionando para todo tipo. Los genéricos dejan que el llamador elija el tipo concreto, que el compilador especialice el código, y que la caja del #13 desaparezca. Array, Dictionary, Optional y Result fueron genéricos todo el tiempo.
-
- swift
- swift-cero-experto
- swift-fundamentals
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.
-
- 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.