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 de este tema provoca errores frustrantes que uno desea corregir rápidamente sin profundizar en los detalles. completa 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 en un inicializador designado super.init 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é siempre se llama sin parámetros, pero e se anulan UIView.init() init(frame:) init(coder:) ...y más. Pero vayamos paso a paso. Tabla de contenido Lo esencial Estructuras Inicializador de miembros Opcionales vs var 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 : genéricos, protocolos, , required Self() final sin parámetros UIView() Menciones honoríficas Inicialización fallida Enumeraciones Resumen Enlaces relevantes Lo esencial La guía de Apple (que, por cierto, es sorprendentemente detallada para los inicializadores) afirma: The Swift Programming Language (6) 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. La inicialización Este proceso de inicialización se implementa definiendo , 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. inicializadores 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 generado por el compilador. Esto . inicializador miembro por miembro solo funciona con estructuras Al seleccionar , puedes ver cómo se ve: Refactorizar → Generar inicializador miembro por miembro 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 , se producirá un error de compilación porque . isBlocked todas las propiedades de la estructura deben completarse en un inicializador vs Opcionales, var let El único caso en el que un campo no necesita rellenarse es cuando es una ( ) ( ). En tales casos, el valor predeterminado del campo es : explícitamente variable opcional ? var 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 tienen como valor predeterminado Los campos var opcionales 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 . Cumple dos funciones: inicializador designado Asegura que todos los campos estén rellenados 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 llamar . 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. deben completarse antes de super.init 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 , habríamos encontrado un error de ejecución porque el inicializador habría llamado al método sobrescrito de la clase . Este método intenta acceder a la propiedad , que aún no se habría rellenado. self.breed = breed Animal getInfo() Dog breed 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 . 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. inicializador de conveniencia 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. 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 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) // ✅ Es fácil ver que, de esta manera, para utilizar un inicializador de conveniencia en la siguiente subclase, es necesario anular inicializadores. dos 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) // ✅ 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) // ✅ Ahora solo tenemos 2 inicializadores especificados explícitamente en cada subclase. al init en lugar de . Observe cómo los inicializadores de anulación de conveniencia llaman self super.init Este truco se explica detalladamente en el Capítulo 5 de de Tjeerd in 't Veen, un libro que recomiendo muchísimo. Swift in Depth Resumen intermedio Un garantiza que se completen todas las propiedades y llama . inicializador designado super.init() Un simplifica la inicialización al llamar a un inicializador designado. inicializador de conveniencia Un no está disponible para las subclases si declaran nuevas propiedades. inicializador de conveniencia Para restaurar de una superclase, todos sus inicializadores designados deben ser anulados. el inicializador de conveniencia 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 ( sin argumentos), los inicializadores declarados explícitamente en la subclase llamar . En este caso, el compilador de Swift la llamada a disponible sin argumentos. init() no necesitan super.init() inserta automáticamente super.init() 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 . Esto es crucial para algunos de los siguientes ejemplos. super.init() Requerido Se utiliza un inicializador en todos los casos en que una subclase debe tener el mismo inicializador que la clase base. También debe llamar a . A continuación, se muestran ejemplos donde se requiere un inicializador . required super.init() required Genéricos Solo es posible llamar en un tipo genérico declarándolo como un . init 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 desconoce las subclases de . Aunque en este caso, tiene un sin parámetros, imagine si introdujera un nuevo campo: Factory Base Subclass init() class Subclass: Base { let value: Int init(value: Int) { self.value = value } } Aquí ya no tiene un vacío, por lo que debe declararse como . init 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 en , el compilador lo generó automáticamente. Esto se explicó en la . El se heredó automáticamente y se llamó a . required init Subclass sección "Asistencia del compilador" required init 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 , la subclase no está obligada a sobrescribirlo y puede definir su propio inicializador. required init 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 no es . Base.init() 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 en métodos estáticos. Self() 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 es imponer la implementación de un inicializador en subclases, naturalmente, prohibir la herencia usando la palabra clave elimina la necesidad de marcar un inicializador como . required final 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 sin parámetros, se llama automáticamente en los inicializadores de subclase. init() Se necesita un inicializador para garantizar su presencia en subclases para su uso en genéricos, protocolos y . required Self() Vista de interfaz de usuario() Una breve mención del inicializador sin parámetros, que no se puede encontrar en la documentación pero que misteriosamente se usa en todas partes. UIView() La razón es que hereda de , que tiene un sin parámetros. , este inicializador no se declara explícitamente en la interfaz , pero sigue estando disponible: UIView NSObject init() Por lo tanto UIView @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 cuando se inicializa en código o cuando se inicializa mediante Interface Builder. Esto se debe a que proporciona su propia implementación de , lo cual se confirma por el hecho de que devuelve direcciones diferentes para y . init(frame:) init(coder:) UIView NSObject.init() method_getImplementation NSObject.init() 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 opcionales tienen como valor predeterminado . var nil Las estructuras reciben un gratuito. inicializador miembro por miembro El cuando se define un inicializador personalizado. inicializador miembro por miembro desaparece Un garantiza que todos los campos estén completos y llama . inicializador designado super.init() Un simplifica la inicialización al llamar a un inicializador designado. inicializador de conveniencia Los inicializadores de conveniencia siempre van ( ), y los inicializadores designados van ( ). horizontales self.init verticales super.init Un no está disponible para las subclases si declaran nuevas propiedades. inicializador de conveniencia Para restaurar de una superclase, todos sus inicializadores designados deben ser anulados. el inicializador de conveniencia 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 sin parámetros, se llama automáticamente en los inicializadores de subclase. init() Un asegura su presencia en subclases para su uso en genéricos, protocolos y . inicializador obligatorio Self() llama a o . UIView.init() UIView.init(frame:) UIView.init(coder:) Un devuelve un opcional. inicializador que puede fallar Las enumeraciones con un valor bruto obtienen un gratuito. init?(rawValue:) 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 El lenguaje de programación Swift (6) / Inicialización Rápido en profundidad de Tjeerd en 't Veen Telegrama - @kfamyn