paint-brush
Swift init(), de una vez por todaspor@kfamyn
Nueva Historia

Swift init(), de una vez por todas

por Kiryl Famin19m2025/03/21
Read on Terminal Reader

Demasiado Largo; Para Leer

Este artículo cubre todo lo esencial para los inicializadores de Swift: miembro por miembro, designado, conveniencia, anulación de conveniencia; casos de uso requeridos; inicializador UIView() sin parámetros; asistencia del compilador; init fallible, init de enumeración y más.
featured image - Swift init(), de una vez por todas
Kiryl Famin HackerNoon profile picture
0-item
1-item

Introducción

¡Hola! Me llamo Kiryl Famin y soy desarrollador de iOS.


Hoy quiero analizar a fondo un tema tan sencillo como los inicializadores en Swift. A pesar de su aparente simplicidad, a veces la falta de una comprensión completa de este tema provoca errores frustrantes que uno desea corregir rápidamente sin profundizar en los detalles.


En este artículo, cubriremos todo lo relacionado con los inicializadores, incluidos:


  • Cómo conservar el inicializador miembro por miembro de una estructura al definir uno personalizado

  • Por qué no siempre es necesario escribir un inicializador en las clases

  • Por qué no siempre es necesario llamar super.init en un inicializador designado

  • Por qué se deben completar todos los campos de una subclase antes de llamar super.init

  • Cómo acceder a todos los inicializadores principales con anulaciones mínimas en subclases

  • Cuándo se necesita exactamente el inicializador required

  • Por qué UIView.init() siempre se llama sin parámetros, pero init(frame:) e init(coder:) se anulan


...y más. Pero vayamos paso a paso.

Tabla de contenido

Lo esencial

Estructuras

  • Inicializador de miembros
  • Opcionales
  • var vs let
  • Conservación de un inicializador por miembro

Clases

  • Inicializador designado
  • Inicializador de conveniencia
  • Manteniendo el inicializador de conveniencia de la superclase
  • Minimizar el número de anulaciones
  • Asistencia del compilador
  • inicializador required : genéricos, protocolos, Self() , final
  • UIView() sin parámetros

Menciones honoríficas

  • Inicialización fallida
  • Enumeraciones

Resumen

Enlaces relevantes

Lo esencial

La guía de Apple The Swift Programming Language (6) (que, por cierto, es sorprendentemente detallada para los inicializadores) afirma:


La inicialización es el proceso de preparar una instancia de una clase, estructura o enumeración para su uso. Este proceso implica establecer un valor inicial para cada propiedad almacenada en esa instancia y realizar cualquier otra configuración o inicialización necesaria antes de que la nueva instancia esté lista para su uso.


Este proceso de inicialización se implementa definiendo inicializadores , que son como métodos especiales que se pueden llamar para crear una nueva instancia de un tipo específico. A diferencia de los inicializadores de Objective-C, los inicializadores de Swift no devuelven un valor. Su función principal es garantizar que las nuevas instancias de un tipo se inicialicen correctamente antes de su primer uso.


Bueno, supongo que no tengo que añadir nada aquí.

Estructuras

Comencemos analizando los inicializadores de estructura. Es bastante sencillo, ya que no hay herencia, pero hay algunas reglas que debes conocer.

Inicializador de miembros

Escribamos una estructura simple:

 struct BankAccount { let amount: Double let isBlocked: Bool } let bankAccount = BankAccount(amount: 735, isBlocked: Bool)


Observe que pudimos inicializar la estructura sin declarar explícitamente un inicializador. Esto se debe a que las estructuras reciben un inicializador miembro por miembro generado por el compilador. Esto solo funciona con estructuras .


Al seleccionar Refactorizar → Generar inicializador miembro por miembro , puedes ver cómo se ve:


Generación de inicializadores por miembro en Xcode


 init(amount: Double, isBlocked: Bool) { self.amount = amount self.isBlocked = isBlocked }


A partir de la firma, es fácil ver que si no se proporcionan valores para todos los parámetros se producirá un error de compilación:

 let bankAccount = BankAccount(amount: 735) // ❌ Missing argument for parameter 'isBlocked' in call


Sin embargo, si desea reducir la cantidad de argumentos necesarios, puede definir un inicializador personalizado:

 init(amount: Double, isBlocked: Bool = false) { self.amount = amount isBlocked = isBlocked } let bankAccount = BankAccount(amount: 735) // ✅


Tenga en cuenta que si no se completa isBlocked , se producirá un error de compilación porque todas las propiedades de la estructura deben completarse en un inicializador .

Opcionales, var vs let

El único caso en el que un campo no necesita rellenarse explícitamente es cuando es una variable opcional ( ? ) ( var ). En tales casos, el valor predeterminado del campo es nil :

 struct BankAccount { let amount: Double var isBlocked: Bool? init(amount: Double) { self.amount = amount } } let bankAccount = BankAccount(amount: 735) // ✅


Sin embargo, si intentamos utilizar el inicializador miembro por miembro en este caso, obtendremos un error de compilación:

 let bankAccount = BankAccount( amount: 735, isBlocked: false ) // ❌ Extra argument 'isBlocked' in call

Conservación de un inicializador por miembro

Esto se debe a que al declarar un inicializador personalizado se elimina el inicializador miembro por miembro. Aún es posible definirlo explícitamente, pero no estará disponible automáticamente.


Sin embargo, hay un pequeño truco para conservar el inicializador miembro por miembro: declarar el inicializador personalizado en una extension .

 struct BankAccount { let amount: Double var isBlocked: Bool? } extension BankAccount { init(amount: Double) { self.amount = amount } } let barclaysBankAccount = BankAccount(amount: 735) // ✅ let revolutBankAccount = BankAccount(amount: 812, isBlocked: false) // ✅ print(barclaysBankAccount.isBlocked) // nil print(barclaysBankAccount.isBlocked) // false


Resumen de estructuras

  • Todos los campos deben completarse en un inicializador
  • Los campos var opcionales tienen como valor predeterminado nil
  • Las estructuras reciben un inicializador miembro por miembro gratuito
  • El inicializador miembro por miembro desaparece si se declara un inicializador personalizado
  • Para conservar el inicializador de miembros de la estructura, defina uno personalizado en una extension

Clases

Inicializador designado

El inicializador principal de una clase es el inicializador designado . Cumple dos funciones:

  1. Asegura que todos los campos estén rellenados
  2. Si la clase se hereda, llama al inicializador de la superclase.
 class Animal { var name: String init(name: String) { self.name = name } } class Dog: Animal { var breed: String var name: String init(breed: String, name: String) { self.breed = breed super.init(name: name) } }


Todos los campos deben completarse antes de llamar super.init . Esto se debe a que el inicializador de la superclase puede llamar a métodos sobrescritos por la subclase, lo que podría acceder a propiedades de la subclase no completadas.

 class Animal { var age: Int init(age: Int) { self.age = age getInfo() } func getInfo() { print("Age: ", age) } } class Dog: Animal { var breed: String init(breed: String, age: Int) { self.breed = breed // imagine we haven't done this super.init(age: age) } override func getInfo() { print("Age: ", age, ", breed: ", breed) } }


Por lo tanto, si no hubiéramos definido self.breed = breed , habríamos encontrado un error de ejecución porque el inicializador Animal habría llamado al método getInfo() sobrescrito de la clase Dog . Este método intenta acceder a la propiedad breed , que aún no se habría rellenado.


A diferencia de las estructuras, las clases no reciben un inicializador implícito por miembro. Si hay propiedades sin inicializar, se produce un error de compilación:

 class Animal { // ❌ Class 'Animal' has no initializers var age: Int }
 class Animal { // ✅ var age: Int = 0 }
 class Animal { // ✅ var age: Int? }
 class Animal { } // ✅

Inicializador de conveniencia

Las clases también pueden tener un inicializador de conveniencia . A diferencia de los inicializadores designados, no crean un objeto desde cero, sino que simplifican el proceso de inicialización al reutilizar la lógica de otros inicializadores.

 class Rectangle { var width: Double var height: Double init(width: Double, height: Double) { self.width = width self.height = height } convenience init(side: Double) { self.init(width: side, height: side) // uses a designated initializer of self } }


Los inicializadores de conveniencia pueden llamar a inicializadores designados o a otros inicializadores de conveniencia. En última instancia, siempre se llamará a un inicializador designado.

Conveniencia, inicializadores designados y de superclase


Los inicializadores de conveniencia siempre van horizontales (self.init) y los inicializadores designados van verticales (super.init).

Conservación del inicializador de conveniencia de la superclase

Tan pronto como una subclase declara nuevas propiedades, pierde el acceso a todos los inicializadores de conveniencia de la superclase.

 class Animal { var age: Int var name: String init(age: Int, name: String) { self.age = age self.name = name } convenience init(age: Int) { self.init(age: age, name: "Default") } convenience init(name: String) { self.init(age: 0, name: name) } } class Dog: Animal { var breed: String init(age: Int, name: String, breed: String) { self.breed = breed super.init(age: age, name: name) } } let dog = Dog(age: 3) // ❌ Missing arguments for parameters 'breed', 'name' in call 


Jerarquía de inicializadores actual, solo se declara explícitamente un init


Esto se puede solucionar anulando todos los inicializadores designados de la superclase .

 class Dog: Animal { // ... override init(age: Int, name: String) { self.breed = "Mixed" super.init(age: age, name: name) } } let dog = Dog(age: 3) // ✅ 


Ahora se recuperan los inicializadores de conveniencia, pero se declaran dos inicializadores explícitamente


Es fácil ver que, de esta manera, para utilizar un inicializador de conveniencia en la siguiente subclase, es necesario anular dos inicializadores.

 class GuideDog: Dog { var isTrained: Bool override init(age: Int, name: String) { self.isTrained = false super.init(age: age, name: name) } override init(age: Int, name: String, breed: String) { self.isTrained = false super.init(age: age, name: name, breed: breed) } init(age: Int, name: String, breed: String, isTrained: Bool) { self.isTrained = isTrained super.init(age: age, name: name, breed: breed) } } let dog = GuideDog(age: 3) // ✅ 


Se recuperan los inicializadores de conveniencia, pero GuideDog especifica tres inicializadores explícitamente


Minimizar el número de anulaciones

Sin embargo, esto se puede evitar mediante el uso de un inicializador de anulación de conveniencia .

 class Dog: Animal { var breed: String convenience override init(age: Int, name: String) { self.init(age: age, name: name, breed: "Mixed") // self, not super } init(age: Int, name: String, breed: String) { self.breed = breed super.init(age: age, name: name) } } class GuideDog: Dog { var isTrained: Bool // override init(age: Int, name: String) { // self.isTrained = false // // super.init(age: age, name: name, breed: "Mixed") // } convenience override init(age: Int, name: String, breed: String) { self.init(age: age, name: name, breed: breed, isTrained: false) // self, not super } init(age: Int, name: String, breed: String, isTrained: Bool) { self.isTrained = isTrained super.init(age: age, name: name, breed: breed) } } let dog = GuideDog(age: 3) // ✅ 


Se recuperan los inicializadores de conveniencia de dos animales, se recupera el inicializador de conveniencia de Perro init(edad:, nombre:) y solo se especifican explícitamente dos inicializadores de GuideDog.


Ahora solo tenemos 2 inicializadores especificados explícitamente en cada subclase.

Observe cómo los inicializadores de anulación de conveniencia llaman al init self en lugar de super.init .

Este truco se explica detalladamente en el Capítulo 5 de Swift in Depth de Tjeerd in 't Veen, un libro que recomiendo muchísimo.

Resumen intermedio

  • Un inicializador designado garantiza que se completen todas las propiedades y llama super.init() .
  • Un inicializador de conveniencia simplifica la inicialización al llamar a un inicializador designado.
  • Un inicializador de conveniencia no está disponible para las subclases si declaran nuevas propiedades.
  • Para restaurar el inicializador de conveniencia de una superclase, todos sus inicializadores designados deben ser anulados.
  • Para minimizar la cantidad de anulaciones, se puede utilizar un inicializador de anulación de conveniencia .

Asistencia del compilador

Ya hemos discutido que si una subclase no introduce nuevos parámetros, hereda automáticamente todos los inicializadores de la superclase.

 class Base { let value: Int init() { value = 0 } init(value: Int) { self.value = value } } class Subclass: Base { } let subclass = Subclass() // ✅ let subclass = Subclass(value: 3) // ✅


Sin embargo, hay otro punto importante: si la superclase solo tiene un inicializador designado y este no tiene parámetros ( init() sin argumentos), los inicializadores declarados explícitamente en la subclase no necesitan llamar super.init() . En este caso, el compilador de Swift inserta automáticamente la llamada a super.init() disponible sin argumentos.

 class Base { init() { } } class Subclass: Base { let secondValue: Int init(secondValue: Int) { self.secondValue = secondValue // ✅ without explicit super.init() } }


El código se compila porque se llama implícitamente super.init() . Esto es crucial para algunos de los siguientes ejemplos.

Requerido

Se utiliza un inicializador required en todos los casos en que una subclase debe tener el mismo inicializador que la clase base. También debe llamar a super.init() . A continuación, se muestran ejemplos donde se requiere un inicializador required .

Genéricos

Solo es posible llamar init en un tipo genérico declarándolo como un required init .

 class Base { } class Subclass: Base { } struct Factory<T: Base> { func initInstance() -> T { // ❌ Constructing an object of class T() // type 'T' with a metatype value } // must use a 'required' initializer }


Este código no compila porque Factory desconoce las subclases de Base . Aunque en este caso, Subclass tiene un init() sin parámetros, imagine si introdujera un nuevo campo:

 class Subclass: Base { let value: Int init(value: Int) { self.value = value } }


Aquí ya no tiene un init vacío, por lo que debe declararse como required .

 class Base { required init() { } } class Subclass: Base { } struct Factory<T: Base> { static func initInstance() -> T { // ✅ T() } } let subclass = Factory<Subclass>.initInstance()


Tenga en cuenta que, aunque no declaramos explícitamente required init en Subclass , el compilador lo generó automáticamente. Esto se explicó en la sección "Asistencia del compilador" . El required init se heredó automáticamente y se llamó a super.init() .

 class Subclass: Base { required init() { super.init() } }

Protocolos

Todos los inicializadores declarados en los protocolos deben ser required :

 protocol Initable { init() } class InitableObject: Initable { init() { // ❌ Initializer requirement 'init()' can only // be satisfied by a 'required' initializer } // in non-final class 'InitableObject' }


Nuevamente, esto es necesario para que el compilador garantice que la subclase implemente el inicializador del protocolo. Como ya sabemos, esto no siempre ocurre: si no se required init , la subclase no está obligada a sobrescribirlo y puede definir su propio inicializador.

 class IntValue: InitableObject { let value: Int init(value: Int) { self.value = value } } let InitableType: Initable.Type = IntValue.self let initable: Initable = InitableType.init()


Por supuesto, el siguiente código no se compilará porque Base.init() no es required .

 class InitableObject: Initable { required init() { } // ✅ } class IntValue: InitableObject { let value: Int required init() { self.value = 0 } init(value: Int) { self.value = value } }

Ser()

Una situación similar ocurre cuando se llama al inicializador Self() en métodos estáticos.

 class Base { let value: Int init(value: Int) { self.value = value } static func instantiate() -> Self { Self(value: 3) // ❌ Constructing an object of class type 'Self' } // with a metatype value must use a 'required' initializer }


Como siempre, la cuestión radica en la herencia:

 class Subclass: BaseClass { let anotherValue: Int init(anotherValue: Int) { self.anotherValue = anotherValue } } let subclass = Subclass.instantiate() // ❌
 class BaseClass { let value: Int required init(value: Int) { // ✅ self.value = value } static func instantiate() -> Self { Self(value: 3) } }

Deshacerse de required : final

Dado que el propósito de required es imponer la implementación de un inicializador en subclases, naturalmente, prohibir la herencia usando la palabra clave final elimina la necesidad de marcar un inicializador como required .

 protocol Initable { init() } final class InitableObject: Initable { } // ✅
 protocol ValueInitable { init(value: Int) } final class ValueInitableObject: ValueInitable { init(value: Int) { } // ✅ }

Resumen intermedio

  • Si una subclase no introduce nuevos parámetros, hereda automáticamente todos los inicializadores de su superclase.
  • Si la superclase solo tiene init() sin parámetros, se llama automáticamente en los inicializadores de subclase.
  • Se necesita un inicializador required para garantizar su presencia en subclases para su uso en genéricos, protocolos y Self() .

Vista de interfaz de usuario()

Una breve mención del inicializador UIView() sin parámetros, que no se puede encontrar en la documentación pero que misteriosamente se usa en todas partes.


La razón es que UIView hereda de NSObject , que tiene un init() sin parámetros. Por lo tanto , este inicializador no se declara explícitamente en la interfaz UIView , pero sigue estando disponible:

 @available(iOS 2.0, *) @MainActor open class UIView : UIResponder, NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate { open class var layerClass: AnyClass { get } public init(frame: CGRect) public init?(coder: NSCoder) open var isUserInteractionEnabled: Bool // no init()


Sin embargo, en el fondo, este inicializador llama init(frame:) cuando se inicializa en código o init(coder:) cuando se inicializa mediante Interface Builder. Esto se debe a que UIView proporciona su propia implementación de NSObject.init() , lo cual se confirma por el hecho de que method_getImplementation devuelve direcciones diferentes para NSObject.init() y UIView.init() .

Menciones honoríficas

Inicialización fallida

Un init fallible es simplemente uno que devuelve un valor opcional.

 final class Failable { let positiveValue: Int init?(value: Int) { guard value > 0 else { return nil } positiveValue = value } }

Enumeración

¿Las enumeraciones con un valor bruto obtienen una init?(rawValue:)

 enum Direction: String { case north case west case south case east } let north = Direction(rawValue: "north")

También puedes crear un inicializador personalizado para enumeraciones. Todos los inicializadores de enumeraciones deben asignar self .

 enum DeviceType { case phone case tablet init(screenWidth: Int) { self = screenWidth > 800 ? .tablet : .phone } }

Resumen final

Hemos cubierto todos los aspectos esenciales de los inicializadores en Swift:


  • En un inicializador, todos los campos deben estar completos.

  • Las propiedades var opcionales tienen como valor predeterminado nil .

  • Las estructuras reciben un inicializador miembro por miembro gratuito.

  • El inicializador miembro por miembro desaparece cuando se define un inicializador personalizado.

  • Un inicializador designado garantiza que todos los campos estén completos y llama super.init() .

  • Un inicializador de conveniencia simplifica la inicialización al llamar a un inicializador designado.

  • Los inicializadores de conveniencia siempre van horizontales ( self.init ), y los inicializadores designados van verticales ( super.init ).

  • Un inicializador de conveniencia no está disponible para las subclases si declaran nuevas propiedades.

  • Para restaurar el inicializador de conveniencia de una superclase, todos sus inicializadores designados deben ser anulados.

  • Para minimizar la cantidad de anulaciones, se puede utilizar un inicializador de anulación de conveniencia .

  • Si una subclase no introduce nuevos parámetros, hereda automáticamente todos los inicializadores de su superclase.

  • Si la superclase solo tiene init() sin parámetros, se llama automáticamente en los inicializadores de subclase.

  • Un inicializador obligatorio asegura su presencia en subclases para su uso en genéricos, protocolos y Self() .

  • UIView.init() llama a UIView.init(frame:) o UIView.init(coder:) .

  • Un inicializador que puede fallar devuelve un opcional.

  • Las enumeraciones con un valor bruto obtienen un init?(rawValue:) gratuito.

  • Todos los inicializadores de enumeración deben asignarse self .


Espero que este artículo te haya resultado útil. Si algo no te queda claro o encuentras alguna inexactitud, no dudes en contactarme para obtener una explicación gratuita en Telegram: @kfamyn .

Enlaces relevantes

  1. El lenguaje de programación Swift (6) / Inicialización
  2. Rápido en profundidad de Tjeerd en 't Veen
  3. Telegrama - @kfamyn