¿Por qué los inicializadores de Swift no pueden llamar inicializadores de conveniencia en su superclase?

88

Considere las dos clases:

class A {
    var x: Int

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

    convenience init() {
        self.init(x: 0)
    }
}

class B: A {
    init() {
        super.init() // Error: Must call a designated initializer of the superclass 'A'
    }
}

No veo por qué esto no está permitido. En última instancia, el inicializador designado de cada clase se llama con los valores que necesiten, entonces, ¿por qué necesito repetirme en B's initespecificando un valor predeterminado para xnuevamente, cuando la conveniencia initen funcionará Abien?

Robert
fuente
2
Busqué una respuesta pero no puedo encontrar ninguna que me satisfaga. Probablemente sea alguna razón de implementación. Tal vez buscar inicializadores designados en otra clase sea mucho más fácil que buscar inicializadores de conveniencia ... o algo así.
Sulthan
@Robert, gracias por tus comentarios a continuación. Creo que podría agregarlos a su pregunta, o incluso publicar una respuesta con lo que recibió: "Esto es por diseño y se han solucionado los errores relevantes en esta área". Entonces parece que no pueden o no quieren explicar la razón.
Ferran Maylinch

Respuestas:

24

Esta es la Regla 1 de las reglas de "Encadenamiento de inicializadores" como se especifica en la Guía de programación de Swift, que dice:

Regla 1: Los inicializadores designados deben llamar a un inicializador designado de su superclase inmediata.

https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html

Énfasis mío. Los inicializadores designados no pueden llamar a los inicializadores de conveniencia.

Hay un diagrama que acompaña las reglas para demostrar qué "direcciones" de inicializador están permitidas:

Encadenamiento de inicializador

Craig Otis
fuente
80
Pero, ¿por qué se hace así? La documentación solo dice que simplifica el diseño, pero no veo cómo es este el caso si tengo que seguir repitiéndome especificando continuamente los valores predeterminados en los inicializadores designados. los inicializadores servirán?
Robert
5
Presenté un error 17266917 un par de días después de esta pregunta, pidiendo poder llamar a los inicializadores de conveniencia. Aún no hay respuesta, ¡pero tampoco he recibido ninguna para mis otros informes de errores de Swift!
Robert
8
Con suerte, permitirán llamar a los inicializadores de conveniencia. Para algunas clases en el SDK, no hay otra forma de lograr un determinado comportamiento que llamar al inicializador de conveniencia. Ver SCNGeometry: puede agregar SCNGeometryElements solo usando el inicializador de conveniencia, por lo tanto, no se puede heredar de él.
Spak
4
Esta es una decisión de diseño de lenguaje muy pobre. Desde el comienzo de mi nueva aplicación, decidí desarrollar con rapidez.Necesito llamar a NSWindowController.init (windowNibName) desde mi subclase, y simplemente no puedo hacer eso :(
Uniqus
4
@Kaiserludi: Nada útil, se cerró con la siguiente respuesta: "Esto es por diseño y se han solucionado los errores relevantes en esta área".
Robert
21

Considerar

class A
{
    var a: Int
    var b: Int

    init (a: Int, b: Int) {
        print("Entering A.init(a,b)")
        self.a = a; self.b = b
    }

    convenience init(a: Int) {
        print("Entering A.init(a)")
        self.init(a: a, b: 0)
    }

    convenience init() {
        print("Entering A.init()")
        self.init(a:0)
    }
}


class B : A
{
    var c: Int

    override init(a: Int, b: Int)
    {
        print("Entering B.init(a,b)")
        self.c = 0; super.init(a: a, b: b)
    }
}

var b = B()

Debido a que todos los inicializadores designados de la clase A se anulan, la clase B heredará todos los inicializadores de conveniencia de A. Por lo tanto, ejecutar esto dará como resultado

Entering A.init()
Entering A.init(a:)
Entering B.init(a:,b:)
Entering A.init(a:,b:)

Ahora, si al inicializador designado B.init (a: b :) se le permitiera llamar al inicializador de conveniencia de la clase base A.init (a :), esto resultaría en una llamada recursiva a B.init (a:, b: ).

beba
fuente
Eso es fácil de solucionar. Tan pronto como llame al inicializador de conveniencia de una superclase desde un inicializador designado, los inicializadores de conveniencia de la superclase ya no se heredarán.
fluidsonic
@fluidsonic, pero eso sería una locura porque su estructura y métodos de clase cambiarían dependiendo de cómo se usen. ¡Imagínese la diversión de depurar!
kdazzle
2
@kdazzle Ni la estructura de la clase ni sus métodos de clase cambiarían. ¿Por qué deberían hacerlo? - El único problema en el que puedo pensar es que los inicializadores de conveniencia deben delegar dinámicamente al inicializador designado de una subclase cuando se heredan y estáticamente al inicializador designado de su propia clase cuando no se heredan sino que se delegan de una subclase.
fluidsonic
Creo que también puede tener un problema de recursión similar con los métodos normales. Eso depende de su decisión al llamar a los métodos. Por ejemplo, sería una tontería si un lenguaje no permitiera llamadas recursivas porque podría entrar en un bucle infinito. Los programadores deben comprender lo que están haciendo. :)
Ferran Maylinch
13

Es porque puede terminar con una recursividad infinita. Considerar:

class SuperClass {
    init() {
    }

    convenience init(value: Int) {
        // calls init() of the current class
        // so init() for SubClass if the instance
        // is a SubClass
        self.init()
    }
}

class SubClass : SuperClass {
    override init() {
        super.init(value: 10)
    }
}

y mira:

let a = SubClass()

cuál llamará SubClass.init()cuál llamará SuperClass.init(value:)cuál llamará SubClass.init().

Las reglas de inicialización designadas / convenientes están diseñadas para que la inicialización de una clase siempre sea correcta.

Julien
fuente
1
Mientras que una subclase no puede llamar explícitamente a los inicializadores de conveniencia de su superclase, puede heredarlos , dado que la subclase proporciona la implementación de todos sus inicializadores designados por superclase (ver, por ejemplo, este ejemplo ). Por lo tanto, su ejemplo anterior es cierto, pero es un caso especial que no está explícitamente relacionado con el inicializador de conveniencia de una superclase, sino más bien con el hecho de que un inicializador designado no puede llamar a uno de conveniencia , ya que esto dará lugar a escenarios recursivos como el anterior. .
dfrib
1

Encontré una solución para esto. No es muy bonito, pero resuelve el problema de no conocer los valores de una superclase o querer establecer valores predeterminados.

Todo lo que tiene que hacer es crear una instancia de la superclase, utilizando la conveniencia init, directamente en initla subclase. Luego llamas al designado initdel super usando la instancia que acabas de crear.

class A {
    var x: Int

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

    convenience init() {
        self.init(x: 0)
    }
}

class B: A {
    init() {
        // calls A's convenience init, gets instance of A with default x value
        let intermediate = A() 

        super.init(x: intermediate.x) 
    }
}
bigelerow
fuente
1

Considere extraer el código de inicialización de su conveniente init()a una nueva función auxiliar foo(), llame foo(...)para hacer la inicialización en su subclase.

evanchin
fuente
Buena sugerencia, pero en realidad no responde a la pregunta.
SwiftsNamesake
0

Mire el video de WWDC "403 Intermedio Swift" a las 18:30 para una explicación detallada de los inicializadores y su herencia. Como lo entendí, considere lo siguiente:

class Dragon {
    var legs: Int
    var isFlying: Bool

    init(legs: Int, isFlying: Bool) {
        self.legs = legs
        self.isFlying = isFlying
    }

    convenience initWyvern() { 
        self.init(legs: 2, isFlying: true)
    }
}

Pero ahora considere una subclase Wyrm: Un Wyrm es un Dragón sin piernas ni alas. ¡Así que el inicializador para un Wyvern (2 patas, 2 alas) es incorrecto! Ese error se puede evitar si simplemente no se puede llamar al inicializador Wyvern de conveniencia, sino solo al inicializador designado completo:

class Wyrm: Dragon {
    init() {
        super.init(legs: 0, isFlying: false)
    }
}
Ralf
fuente
12
Esa no es realmente una razón. ¿Qué pasa si hago una subclase cuando initWyverntiene sentido que me llamen?
Sulthan
4
Sí, no estoy convencido. No hay nada que impida Wyrmanular el número de tramos después de llamar a un inicializador de conveniencia.
Robert
Es la razón dada en el video de la WWDC (solo con autos -> autos de carrera y una propiedad hasTurbo booleana). Si la subclase implementa todos los inicializadores designados, también hereda los inicializadores de conveniencia. Veo un poco el sentido en eso, también honestamente, no tuve muchos problemas con la forma en que funcionaba Objective-C. Consulte también la nueva convención para llamar a super.init en último lugar en init, no primero como en Objective-C.
Ralf
En mi opinión, la convención para llamar a super.init "último" no es conceptualmente nueva, es la misma que en el objetivo-c, solo que allí, a todo se le estaba asignando un valor inicial (nulo, 0, lo que sea) automáticamente. Es solo que en Swift, tenemos que hacer esta fase de inicialización nosotros mismos. Un beneficio de este enfoque es que también tenemos la opción de asignar un valor inicial diferente.
Roshan
-1

¿Por qué no tiene dos inicializadores, uno con un valor predeterminado?

class A {
  var x: Int

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

  init() {
    self.x = 0
  }
}

class B: A {
  override init() {
    super.init()

    // Do something else
  }
}

let s = B()
s.x // 0
Jason
fuente