¿Por qué la palabra clave de conveniencia es necesaria en Swift?

132

Dado que Swift admite la sobrecarga de métodos e inicializadores, puede poner varios initjuntos y usar lo que considere conveniente:

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    init() {
        self.name = "John"
    }
}

Entonces, ¿por qué existiría la conveniencepalabra clave? ¿Qué hace que lo siguiente sea sustancialmente mejor?

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    convenience init() {
        self.init(name: "John")
    }
}
Desmond Hume
fuente
13
Estaba leyendo esto en la documentación y también me confundí al respecto. : /
boidkan

Respuestas:

235

Las respuestas existentes solo cuentan la mitad de la conveniencehistoria. La otra mitad de la historia, la mitad que no cubre ninguna de las respuestas existentes, responde a la pregunta que Desmond ha publicado en los comentarios:

¿Por qué Swift me obligaría a poner conveniencedelante de mi inicializador solo porque necesito llamar self.initdesde allí?

Lo toqué un poco en esta respuesta , en la que cubro varias de las reglas de inicialización de Swift en detalles, pero el enfoque principal estaba en la requiredpalabra. Pero esa respuesta seguía abordando algo que es relevante para esta pregunta y esta respuesta. Tenemos que entender cómo funciona la herencia del inicializador Swift.

Debido a que Swift no permite variables no inicializadas, no se garantiza que herede todos (o ninguno) inicializadores de la clase de la que hereda. Si subclasificamos y agregamos cualquier variable de instancia no inicializada a nuestra subclase, habremos dejado de heredar inicializadores. Y hasta que agreguemos nuestros propios inicializadores, el compilador nos gritará.

Para ser claros, una variable de instancia no inicializada es cualquier variable de instancia a la que no se le da un valor predeterminado (teniendo en cuenta que las opciones y las opciones implícitamente desenvueltas asumen automáticamente un valor predeterminado de nil).

Entonces en este caso:

class Foo {
    var a: Int
}

aes una variable de instancia no inicializada. Esto no se compilará a menos que le demos aun valor predeterminado:

class Foo {
    var a: Int = 0
}

o inicializar aen un método de inicializador:

class Foo {
    var a: Int

    init(a: Int) {
        self.a = a
    }
}

Ahora, veamos qué sucede si subclasificamos Foo, ¿de acuerdo?

class Bar: Foo {
    var b: Int

    init(a: Int, b: Int) {
        self.b = b
        super.init(a: a)
    }
}

¿Correcto? Agregamos una variable y agregamos un inicializador para establecer un valor para bque se compile. Dependiendo de qué idioma está viniendo, se podría esperar que Barha heredado Foo's inicializador, init(a: Int). Pero no lo hace. ¿Y cómo podría? ¿Cómo Foo's init(a: Int)saber cómo asignar un valor a la bvariable que Baragrega? No lo hace. Por lo tanto, no podemos inicializar una Barinstancia con un inicializador que no pueda inicializar todos nuestros valores.

¿Qué tiene que ver todo esto convenience?

Bueno, veamos las reglas sobre la herencia del inicializador :

Regla 1

Si su subclase no define ningún inicializador designado, hereda automáticamente todos sus inicializadores designados de superclase.

Regla 2

Si su subclase proporciona una implementación de todos sus inicializadores designados de superclase, ya sea al heredarlos según la regla 1 o al proporcionar una implementación personalizada como parte de su definición, entonces hereda automáticamente todos los inicializadores de conveniencia de superclase.

Observe la Regla 2, que menciona los inicializadores de conveniencia.

Entonces, ¿qué la conveniencepalabra clave hace indicar a nosotros, que inicializadores pueden ser heredados por las subclases que las variables de instancia sin añadir valores por defecto.

Tomemos esta Baseclase de ejemplo :

class Base {
    let a: Int
    let b: Int

    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }

    convenience init() {
        self.init(a: 0, b: 0)
    }

    convenience init(a: Int) {
        self.init(a: a, b: 0)
    }

    convenience init(b: Int) {
        self.init(a: 0, b: b)
    }
}

Tenga en cuenta que tenemos tres convenienceinicializadores aquí. Eso significa que tenemos tres inicializadores que se pueden heredar. Y tenemos un inicializador designado (un inicializador designado es simplemente cualquier inicializador que no sea un inicializador de conveniencia).

Podemos crear instancias de la clase base de cuatro maneras diferentes:

ingrese la descripción de la imagen aquí

Entonces, creemos una subclase.

class NonInheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }
}

Estamos heredando de Base. Agregamos nuestra propia variable de instancia y no le dimos un valor predeterminado, por lo que debemos agregar nuestros propios inicializadores. Hemos añadido una, init(a: Int, b: Int, c: Int)pero no coincide con la firma de la Baseclase ha designado inicializador: init(a: Int, b: Int). Eso significa que no estamos heredando ningún inicializador de Base:

ingrese la descripción de la imagen aquí

Entonces, ¿qué pasaría si heredamos de Base, pero seguimos adelante e implementamos un inicializador que coincide con el inicializador designado Base?

class Inheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }

    convenience override init(a: Int, b: Int) {
        self.init(a: a, b: b, c: 0)
    }
}

Ahora, además de los dos inicializadores que implementamos directamente en esta clase, debido a que implementamos un inicializador Basedesignado de la clase que coincide con el inicializador, podemos heredar todos Baselos convenienceinicializadores de la clase :

ingrese la descripción de la imagen aquí

El hecho de que el inicializador con la firma correspondiente esté marcado como convenienceno hace ninguna diferencia aquí. Solo significa que Inheritortiene un solo inicializador designado. Entonces, si heredamos de Inheritor, solo tendríamos que implementar ese inicializador designado, y luego heredaríamos Inheritorel inicializador de conveniencia, lo que a su vez significa que hemos implementado todos Baselos inicializadores designados y podemos heredar sus convenienceinicializadores.

nhgrif
fuente
16
La única respuesta que realmente responde la pregunta y sigue los documentos. Lo aceptaría si fuera el OP.
FreeNickname
12
Deberías escribir un libro;)
coolbeet
1
@SLN Esta respuesta cubre mucho sobre cómo funciona la herencia del inicializador Swift.
nhgrif
1
@SLN Porque crear una barra con init(a: Int)dejaría bsin inicializar.
Ian Warburton
2
@IanWarburton No sé la respuesta a este particular "por qué". Su lógica en la segunda parte de su comentario me parece sólida, pero la documentación indica claramente que así es como funciona, y arrojar un ejemplo de lo que está preguntando en un Playground confirma que el comportamiento coincide con lo documentado.
nhgrif
9

Principalmente claridad. De tu segundo ejemplo,

init(name: String) {
    self.name = name
}

se requiere o designado . Tiene que inicializar todas tus constantes y variables. Los inicializadores de conveniencia son opcionales y, por lo general, se pueden usar para facilitar la inicialización. Por ejemplo, supongamos que su clase Persona tiene una variable de género opcional:

var gender: Gender?

donde el género es una enumeración

enum Gender {
  case Male, Female
}

podrías tener inicializadores convenientes como este

convenience init(maleWithName: String) {
   self.init(name: name)
   gender = .Male
}

convenience init(femaleWithName: String) {
   self.init(name: name)
   gender = .Female
}

Los inicializadores de conveniencia deben llamar a los inicializadores designados o requeridos en ellos. Si su clase es una subclase, debe llamar super.init() dentro de su inicialización.

Nate Mann
fuente
2
Por lo tanto, sería perfectamente obvio para el compilador lo que estoy tratando de hacer con múltiples inicializadores, incluso sin una conveniencepalabra clave, pero Swift todavía estaría molesto al respecto. Ese no es el tipo de simplicidad que esperaba de Apple =)
Desmond Hume
2
Esta respuesta no responde nada. Dijiste "claridad", pero no explicaste cómo aclara algo.
Robo Robok
7

Bueno, lo primero que me viene a la mente es que se usa en herencia de clase para la organización y legibilidad del código. Continuando con tu Personclase, piensa en un escenario como este

class Person{
    var name: String
    init(name: String){
        self.name = name
    }

    convenience init(){
        self.init(name: "Unknown")
    }
}


class Employee: Person{
    var salary: Double
    init(name:String, salary:Double){
        self.salary = salary
        super.init(name: name)
    }

    override convenience init(name: String) {
        self.init(name:name, salary: 0)
    }
}

let employee1 = Employee() // {{name "Unknown"} salary 0}
let john = Employee(name: "John") // {{name "John"} salary 0}
let jane = Employee(name: "Jane", salary: 700) // {{name "Jane"} salary 700}

Con el inicializador de conveniencia, puedo crear un Employee()objeto sin valor, de ahí la palabraconvenience

u54r
fuente
2
Con las conveniencepalabras clave eliminadas, ¿Swift no obtendría suficiente información para comportarse de la misma manera?
Desmond Hume
No, si elimina la conveniencepalabra clave, no puede inicializar el Employeeobjeto sin ningún argumento.
u54r
Específicamente, la llamada Employee()llama al convenienceinicializador (heredado, debido a ) init(), que llama self.init(name: "Unknown"). init(name: String), también un inicializador de conveniencia para Employee, llama al inicializador designado.
BallpointBen
1

Aparte de los puntos que otros usuarios han explicado aquí, es mi comprensión.

Siento firmemente la conexión entre el inicializador de conveniencia y las extensiones. En cuanto a mí, los inicializadores de conveniencia son más útiles cuando quiero modificar (en la mayoría de los casos hacer que sea breve o fácil) la inicialización de una clase existente.

Por ejemplo, alguna clase de terceros que usa tiene initcuatro parámetros, pero en su aplicación los dos últimos tienen el mismo valor. Para evitar escribir más y limpiar su código, puede definir un convenience initcon solo dos parámetros y dentro de él invocar self.initcon los últimos parámetros con valores predeterminados.

Abdullah
fuente
1
¿Por qué Swift me obligaría a poner conveniencedelante de mi inicializador solo porque necesito llamar self.initdesde él? Esto parece redundante y un poco inconveniente.
Desmond Hume
1

De acuerdo con la documentación de Swift 2.1 , los convenienceinicializadores deben cumplir con algunas reglas específicas:

  1. Un convenienceinicializador solo puede llamar a inicializadores en la misma clase, no en superclases (solo a través, no arriba)

  2. Un convenienceinicializador debe llamar a un inicializador designado en algún lugar de la cadena

  3. Un convenienceinicializador no puede cambiar CUALQUIER propiedad antes de llamar a otro inicializador, mientras que un inicializador designado debe inicializar las propiedades que introduce la clase actual antes de llamar a otro inicializador.

Al usar la conveniencepalabra clave, el compilador Swift sabe que debe verificar estas condiciones; de lo contrario, no podría.

El ojo
fuente
Podría decirse que el compilador probablemente podría resolver esto sin la conveniencepalabra clave.
nhgrif
Además, su tercer punto es engañoso. Un inicializador de conveniencia solo puede cambiar las propiedades (y no puede cambiar las letpropiedades). No puede inicializar propiedades. Un inicializador designado tiene la responsabilidad de inicializar todas las propiedades introducidas antes de llamar a un superinicializador designado.
nhgrif
1
Al menos la palabra clave de conveniencia deja en claro al desarrollador, también es la legibilidad lo que cuenta (además de verificar el inicializador con las expectativas del desarrollador). Su segundo punto es bueno, cambié mi respuesta en consecuencia.
TheEye
1

Una clase puede tener más de un inicializador designado. Un inicializador de conveniencia es un inicializador secundario que debe llamar a un inicializador designado de la misma clase.

Abdul Yasin
fuente