Por que os inicializadores do Swift não podem chamar inicializadores de conveniência em sua superclasse?


88

Considere as duas classes:

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'
    }
}

Não vejo por que isso não é permitido. No final das contas, o inicializador designado de cada classe é chamado com quaisquer valores de que precisam, então por que preciso me repetir em B's initespecificando um valor padrão para xnovamente, quando a conveniência initem Abastará?


2
Procurei uma resposta, mas não consigo encontrar nenhuma que me satisfaça. Provavelmente é algum motivo de implementação. Talvez pesquisar inicializadores designados em outra classe seja muito mais fácil do que pesquisar inicializadores de conveniência ... ou algo parecido.
Sulthan

@Robert, obrigado por seus comentários abaixo. Acho que você poderia adicioná-los à sua pergunta, ou até mesmo postar uma resposta com o que recebeu: "Isso ocorre por design e quaisquer bugs relevantes foram resolvidos nesta área.". Portanto, parece que eles não podem ou não querem explicar o motivo.
Ferran Maylinch

Respostas:


24

Esta é a Regra 1 das regras de "Encadeamento de inicializador" conforme especificado no Guia de programação Swift, que diz:

Regra 1: os inicializadores designados devem chamar um inicializador designado de sua superclasse imediata.

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

Ênfase minha. Os inicializadores designados não podem chamar inicializadores de conveniência.

Há um diagrama que acompanha as regras para demonstrar quais "direções" do inicializador são permitidas:

Encadeamento de inicializador


80
Mas por que isso é feito dessa maneira? A documentação apenas diz que simplifica o design, mas não vejo como é o caso se eu tiver que continuar me repetindo especificando continuamente os valores padrão em inicializadores designados. inicializadores vão fazer?
Robert

5
Registrei um bug 17266917 alguns dias após esta pergunta, pedindo para ser capaz de chamar inicializadores de conveniência. Nenhuma resposta ainda, mas também não recebi nenhuma para nenhum dos meus outros relatórios de bug do Swift!
Robert

8
Esperançosamente, eles permitirão chamar inicializadores de conveniência. Para algumas classes no SDK, não há outra maneira de obter um determinado comportamento senão chamando o inicializador de conveniência. Veja SCNGeometry: você só pode adicionar SCNGeometryElements usando o inicializador de conveniência, portanto, não se pode herdar dele.
Spak

4
Esta é uma decisão de design de linguagem muito pobre. Desde o início do meu novo aplicativo, decidi desenvolver com o swift. Preciso chamar NSWindowController.init (windowNibName) da minha subclasse, e simplesmente não posso fazer isso :(
Uniqus

4
@Kaiserludi: Nada útil, ele foi fechado com a seguinte resposta: "Isso ocorre por design e todos os bugs relevantes foram resolvidos nesta á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()

Como todos os inicializadores designados da classe A são substituídos, a classe B herdará todos os inicializadores de conveniência de A. Portanto, a execução disso resultará

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

Agora, se o inicializador designado B.init (a: b :) tivesse permissão para chamar o inicializador de conveniência da classe base A.init (a :), isso resultaria em uma chamada recursiva para B.init (a:, b: )


Isso é fácil de contornar. Assim que você chamar um inicializador de conveniência da superclasse de dentro de um inicializador designado, os inicializadores de conveniência da superclasse não serão mais herdados.
fluidsonic

@fluidsonic, mas isso seria uma loucura porque sua estrutura e métodos de classe mudariam dependendo de como eles fossem usados. Imagine a diversão de depuração!
kdazzle

2
@kdazzle Nem a estrutura da classe nem seus métodos de classe mudariam. Por que eles deveriam? - O único problema em que posso pensar é que os inicializadores de conveniência devem delegar dinamicamente ao inicializador designado de uma subclasse quando herdado e estaticamente ao inicializador designado de sua própria classe quando não herdado, mas delegado de uma subclasse.
fluidsonic de

Eu acho que você também pode obter um problema de recursão semelhante com métodos normais. Isso depende da sua decisão ao chamar métodos. Por exemplo, seria bobo se uma linguagem não permitisse chamadas recursivas porque você poderia entrar em um loop infinito. Os programadores devem entender o que estão fazendo. :)
Ferran Maylinch

13

É porque você pode acabar com uma recursão 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)
    }
}

e olhe para:

let a = SubClass()

que chamará SubClass.init()que ligará SuperClass.init(value:)que ligará SubClass.init().

As regras de inicialização designadas / de conveniência são projetadas de forma que a inicialização de uma classe esteja sempre correta.


1
Embora uma subclasse não possa chamar explicitamente os inicializadores de conveniência de sua superclasse, ela pode herdá- los, visto que a subclasse fornece a implementação de todos os seus inicializadores designados pela superclasse (veja, por exemplo, este exemplo ). Portanto, seu exemplo acima é de fato verdadeiro, mas é um caso especial não explicitamente relacionado ao inicializador de conveniência de uma superclasse, mas sim ao fato de que um inicializador designado pode não chamar um de conveniência , já que isso resultará em cenários recursivos como o anterior .
dfri

1

Eu encontrei uma solução alternativa para isso. Não é muito bonito, mas resolve o problema de não saber os valores de uma superclasse ou de querer definir os valores padrão.

Tudo o que você precisa fazer é criar uma instância da superclasse, usando a conveniência init, diretamente initda subclasse. Em seguida, você chama o designado initdo super usando a instância que acabou de criar.

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) 
    }
}

1

Considere extrair o código de inicialização de sua função conveniente init()para uma nova função auxiliar foo(), chamada foo(...)para fazer a inicialização em sua subclasse.


Boa sugestão, mas na verdade não responde à pergunta.
SwiftsNamesake

0

Veja o vídeo WWDC "403 intermediário Swift" às 18:30 para uma explicação detalhada dos inicializadores e sua herança. Pelo que entendi, considere o seguinte:

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)
    }
}

Mas agora considere uma subclasse da Wyrm: Uma Wyrm é um Dragão sem pernas e sem asas. Portanto, o inicializador para um Wyvern (2 pernas, 2 asas) é errado para isso! Esse erro pode ser evitado se o Wyvern-Initializer de conveniência simplesmente não puder ser chamado, mas apenas o Initializer completo designado:

class Wyrm: Dragon {
    init() {
        super.init(legs: 0, isFlying: false)
    }
}

12
Essa não é realmente uma razão. E se eu criar uma subclasse quando initWyvernfizer sentido ser chamado?
Sulthan

4
Sim, não estou convencido. Não há nada que impeça a Wyrmsubstituição do número de trechos após chamar um inicializador de conveniência.
Robert

É a razão dada no vídeo WWDC (apenas com carros -> carros de corrida e uma propriedade hasTurbo booleana). Se a subclasse implementar todos os inicializadores designados, ela também herdará os inicializadores de conveniência. Eu meio que vejo o sentido nisso, também honestamente não tive muitos problemas com a forma como o Objective-C funcionava. Veja também a nova convenção para chamar super.init por último no init, não primeiro como em Objective-C.
Ralf

IMO a convenção para chamar super.init de "último" não é conceitualmente nova, é a mesma que em objetivo-c, apenas que lá, tudo estava sendo atribuído um valor inicial (nil, 0, qualquer) automaticamente. Só que em Swift, temos que fazer essa fase de inicialização nós mesmos. Um benefício dessa abordagem é que também temos a opção de atribuir um valor inicial diferente.
Roshan

-1

Por que você não tem apenas dois inicializadores - um com um valor padrão?

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.