El proceso de inicialización de una clase, estructura o enumeración, consiste en preparar los valores iniciales de las propiedades almacenadas de una instancia antes de que esté lista para ser usada.
Valor inicial de las propiedades almacenadas
Todas las variables almacenadas en una estructura o clase deben tener un valor inicial. Lo pueden quedar en un estado indeterminado.
Se puede definir su valor inicial en un inicializador, o asignándoselo como parte de la definición de la propiedad.
Al asignar el valor a una propiedad desde el inicializador o en la definición de la misma, no se invoca ningún "property observer".
Inicializador
Un inicializador crea una instancia de un tipo específico. En su forma más simple, tiene la sintaxis init():
init() {
// Inicialización de atributos
}
Por ejemplo:
struct Fahrenheit {
let temperature: Double
init() {
temperature = 32.0
}
}
var f = Fahrenheit()
Propiedades con valores por defecto
Cuando se declara una propiedad dentro de una estructura o clase, se le puede definir un valor por defecto.
Para asignar un único valor a una propiedad, es preferible asignarlo por defecto en la definición, que usar el inicializador, debido a que usar un valor en la definición de una propiedad es más cercano a su declaración.
struct Fahrenheit {
let temperature: Double = 32.0
}
var f = Fahrenheit()
Personalizar la inicialización
Parámetros de inicialización
Se pueden definir parámetros en el inicializador igual que en un método de instancia.
struct Celsius {
let temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
}
Siempre que las etiquetas de los argumentos estén definidas, se las debe usar, como en cualquier otro método. Si se quiere omitir una etiqueta, se debe usar un guion bajo (i.e. _).
Atributos de tipos opcionales
Las propiedades de tipo opcional son inicializadas automáticamente con el valor nil, indicando que todavía no tienen ningún valor durante la inicialización.
class SurveyQuestion {
var text: String
var response: String? // Se le asigna `nil` por defecto
init(text: String) {
self.text = text
}
func ask() {
print(text)
}
}
let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")
cheeseQuestion.ask()
cheeseQuestion.response = "Yes, I do like cheese."
Inicialización de constantes
Se le puede asignar un valor a una constante durante la inicialización. Una vez asignado, ya no se puede modificar.
class SurveyQuestion {
let text: String
var response: String?
init(text: String) {
self.text = text
}
func ask() {
print(text)
}
}
let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
beetsQuestion.response = "I also like beets. (But not with cheese.)"
Inicializador por defecto
Swift provee un inicializador por defecto para toda estructura o clase que provea valores por defecto para todos sus atributos y no ofrezca ni un inicializador.
El inicializador por defecto asigna los valores por defecto a las propiedades.
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()
Inicializador por miembros para estructuras
Las estructuras tienen un inicializador por miembros incluido si no definen ningún otro inicializador.
struct Size {
var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)
Se pueden omitir los argumentos asociados a las propiedades que tengan un valor asignado por defecto. Por ejemplo:
struct Size {
var width = 0.0, height = 0.0
}
let zeroByTwo = Size(height: 2.0)
print(zeroByTwo.width, zeroByTwo.height)
// Prints "0.0 2.0".
Inicializador delegado para tipos por valor (i.e. struct y enum)
Un inicializador puede llamar a otro para hacer parte de la inicialización, lo que evita duplicar código entre inicializadores.
Como los tipos por valor (struct y enum) no admiten herencia, deben suministrar el inicializador delegado ellos mismos.
En los tipos por valor se puede llamar self.init para invocar otros inicializadores delegados.
Por otro lado, si se define un inicializador personalizado para un tipo por valor, ya no se tendrá acceso al inicializador por defecto para obligar al cliente a usar la lógica de inicialización más compleja.
Para que el inicializador por defecto y por miembros estén disponibles, adicional a un inicializador personalizado, se deben definir estos últimos en una extensión.
struct Rect {
var origin = Point()
var size = Size()
init() {}
init(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}
Herencia e inicialización
Todas las propiedades almacenadas de una clase deben tener un valor inicial durante la inicialización.
Inicializadores designados ("designated") y por conveniencia ("convenience")
El inicializador designado es el mecanismo principal para inicializar una clase. Este inicializa todas las propiedades almacenadas de una clase y puede llamar el inicializador de la superclase que haga falta para continuar con la cadena de inicialización de la herencia.
Toda clase debe tener al menos un inicializador designado.
El inicializador por conveniencia es un mecanismo de apoyo, que invoca a algún inicializador designado para crear una instancia de una clase para un caso de uso específico. No es obligatorio tener inicializadores por conveniencia. Se usa este tipo de inicializador para ahorrar tiempo o hacer más clara la intención.
Sintaxis para inicializadores designados ("designated") y por conveniencia ("convenience")
// Designated
init(<#parameters#>) {
<#statements#>
}
// Convenience
convenience init(<#parameters#>) {
<#statements#>
}
Inicializador delegado para clases
Reglas de inicialización:
- Un inicializador designado debe llamar a otro inicializador designado de su superclase inmediata. No aplica si no hay superclase.
- Un inicializador por conveniencia debe llamar a otro inicializador de la misma clase.
- Un inicializador por conveniencia debe terminar llamando a un inicializador designado.
Proceso de inicialización por dos fases
El proceso de inicialización en una clase tiene dos fases:
- Asegurar estado válido: Cada clase es responsable de inicializar las propiedades que ella misma declara. Aquí no se puede acceder a métodos o variables de instancia. Termina cuando se invoca el constructor de la súperclase. Esto garantiza que sea internamente consistente aunque todavía no esté configurado.
-
Personalización: Cuando las propiedades de todas las clases en la jerarquía ya tienen valor, ya se puede acceder a
serlf, llamar métodos, modificar propiedades o sobrescribir valores iniciales. El objetivo es ajustar la instancia a su configuración final.
class Vehiculo {
let ruedas: Int
init() {
ruedas = 4 // obligación: inicializar la propiedad
}
}
class Auto: Vehiculo {
var marca: String
init(marca: String) {
self.marca = marca // fase 1: inicializa su propiedad
super.init() // fase 1 del padre
// --- aquí empieza la fase 2 ---
print("Auto listo")
}
}
Esta división en dos fases se hace para evitar errores comunes como:
- Acceder a propiedades sin inicializar
- Que una subclase cambie valores del padre antes de tiempo
- Estados inconsistentes del objeto.
El compilador hace algunas guardas de seguridad durante el proceso de inicialización de dos fases:
- Un inicializador designado debe inicializar todas las propiedades introducidas por tu clase antes de llamar al inicializador del super.
- Un inicializador designado debe llamar al
superantes de asignar un valor a una propiedad heredada. - Un inicializador por conveniencia debe delegador a un inicializador designado antes de asignar un valor a cualquier propiedad.
- Un inicializador no puede llamar a ningún método de instancia, leer valores de alguna propiedad o referir a
selfhasta que la primera fase de inicialización haya terminado.
Herencia y sobrescritura de inicializadores
Una subclase de Swift no hereda los inicializadores por defecto. Swift previene el caso de heredar un inicializador simple de una superclase que deje a una subclase incorrectamente inicializada.
Si el inicializador de una subclase coincide con la firma de un inicializador designado de su superclase, se debe usar el modificador override antes de la definición del inicializador.
Si la firma de un inicializador coincide con la de un inicializador por conveniencia de su superclase, no se usa el modificador override porque una instancia de una clase no puede invocar directamente a un inicializador por conveniencia.
class Vehicle {
var numberOfWheels = 0
var description: String {
return "\(numberOfWheels) wheel(s)"
}
}
class Bicycle: Vehicle {
override init() {
super.init()
numberOfWheels = 2
}
}
Si el inicializador de una subclase no hace ninguna personalización en la etapa dos del proceso de inicialización, y si la superclase tiene un inicializador designado síncrono sin argumentos, se puede omitir el llamado a super.init() después de asignar los valores a todas las propiedades almacenadas de la subclase.
class Vehicle {
var numberOfWheels: Int = 0
var description: String {
return "\(numberOfWheels) wheel(s)"
}
}
class Bicycle: Vehicle {
let color: String
init(color: String) {
self.color = color
// Se llama implícitamente super.init()
}
}
Una subclase puede modificar las variables heredadas en la inicialización, pero no puede modificar las constantes heredadas.
Herencia automática de inicializador
Como norma general, las subclases no heredan los inicializadores de su superclase. Sin embargo, si todas las propiedades de la subclase tienen valores por defecto:
- Si la subclase no define inicializadores designados, heredará todos los inicializadores designados.
- Si la clase provee una implementación de todos los inicializadores designados de la superclase, heredará todos los inicializadores por conveniencia de la superclase.
Inicializador que puede fallar
En ocasiones, la inicialización puede fallar debido a parámetros de inicialización inválidos, la ausencia de un recurso externo requerido o alguna otra condición. La sintaxis es: init?.
No se pueden definir un inicializador que no falla y otro que puede fallar con los mismos nombres y tipos de parámetros.
Este tipo de inicializador crea un valor opcional. Si la inicialización falla, retornar nil (aunque conceptualmente hablando, los inicializadores no retornan un valor, sino que garantizan que self se haya construido correctamente).
Este tipo de inicializadores son usados en la conversión de tipos numéricos (e.g. Int(exactly: 3.14150).
struct Animal {
let species: String
init?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}
Inicializador que puede fallar para enumeraciones
Se puede usar un inicializador que falla para seleccionar un caso de enumeración con base en uno o más parámetros.
enum TemperatureUnit {
case kelvin, celsius, fahrenheit
init?(symbol: Character) {
switch symbol {
case "K":
self = .kelvin
case "C":
self = .celsius
case "F":
self = .fahrenheit
default:
return nil
}
}
}
Inicializador que puede fallar para enumeraciones con valores crudos
Las enumeraciones con valores crudos automáticamente reciben un inicializador que puede fallar: init?(rawValue:).
Propagación de inicializadores que pueden fallar
Si se delega a otro inicializador que case que falle la inicialización, todo el proceso de inicialización falla inmediatamente y el resto de código de inicialización queda sin ejecutarse.
Sobrescribir un inicializador que puede fallar
Una clase puede tener un inicializador que puede fallar (e.g. init?(param:)) y una de sus subclases puede sobrescribir dicho inicializador haciendo que no falle (i.e. init(param:) puesto que puede manejar el caso de error de una manera diferente que su superclase, sin necesidad de fallar.
class Document {
var name: String?
// this initializer creates a document with a nil name value
init() {}
// this initializer creates a document with a nonempty name value
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
class AutomaticallyNamedDocument: Document {
override init() {
super.init()
self.name = "[Untitled]"
}
override init(name: String) {
super.init()
if name.isEmpty {
self.name = "[Untitled]"
} else {
self.name = name
}
}
}
Además, si estoy 100% seguro de que no va a fallar el llamado al super, puedo hacer force-unwrap.
class UntitledDocument: Document {
override init() {
super.init(name: "[Untitled]")!
}
}
Inicializador requerido
El modificador required indica que todas las subclases de una clase deben implementar ese inicializador.
class SomeClass {
required init() {
// initializer implementation goes here
}
}
También se requiere escribir el modificador required antes de la implementación del inicializador para indicar que el requerimiento del inicializador aplica también a las subclases.
No se necesita escribir override cuando se sobrescribe un inicializador designado requerido.
class SomeSubclass: SomeClass {
required init() {
// subclass implementation of the required initializer goes here
}
}
Definir valores por defecto a una propiedad con un closure o función
Se puede usar un closure o función global para asignar el valor por defecto a una propiedad.
class SomeClass {
let someProperty: SomeType = {
// create a default value for someProperty inside this closure
// someValue must be of the same type as SomeType
return someValue
}()
}
Top comments (0)