Perché gli inizializzatori Swift non possono chiamare inizializzatori di convenienza sulla loro superclasse?


88

Considera le due classi:

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

Non vedo perché questo non è permesso. In ultima analisi, inizializzatore designato di ogni classe viene chiamato con tutti i valori di cui hanno bisogno, quindi perché devo ripetermi in B's initspecificando un valore predefinito per xancora una volta, quando la convenienza inita Afarà bene?


2
Ho cercato una risposta ma non trovo nessuna che mi soddisfi. Probabilmente è un motivo per l'implementazione. Forse cercare inizializzatori designati in un'altra classe è molto più semplice che cercare inizializzatori di convenienza ... o qualcosa del genere.
Sulthan

@Robert, grazie per i tuoi commenti qui sotto. Penso che potresti aggiungerli alla tua domanda, o anche pubblicare una risposta con ciò che hai ricevuto: "Questo è di progettazione e tutti i bug rilevanti sono stati risolti in quest'area.". Quindi sembra che non possano o non vogliano spiegare il motivo.
Ferran Maylinch

Risposte:


24

Questa è la regola 1 delle regole "Initializer Chaining" come specificato nella Swift Programming Guide, che recita:

Regola 1: gli inizializzatori designati devono chiamare un inizializzatore designato dalla loro superclasse immediata.

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

Enfasi mia. Gli inizializzatori designati non possono chiamare inizializzatori di convenienza.

C'è un diagramma che accompagna le regole per dimostrare quali "direzioni" dell'inizializzatore sono consentite:

Concatenamento di inizializzatori


80
Ma perché è fatto in questo modo? La documentazione dice solo che semplifica il design, ma non vedo come sia il caso se devo continuare a ripetermi specificando continuamente i valori predefiniti sugli inizializzatori designati per lo più non mi interessa quando sono quelli predefiniti nella convenienza gli inizializzatori faranno?
Robert

5
Ho segnalato un bug 17266917 un paio di giorni dopo questa domanda, chiedendo di poter chiamare inizializzatori di convenienza. Nessuna risposta ancora, ma non ho ricevuto nessuna delle mie altre segnalazioni di bug di Swift!
Robert il

8
Si spera che consentano di chiamare inizializzatori di convenienza. Per alcune classi nell'SDK non c'è altro modo per ottenere un determinato comportamento se non chiamando l'inizializzatore di convenienza. Vedi SCNGeometry: puoi aggiungere SCNGeometryElements solo usando l'inizializzatore di convenienza, quindi non si può ereditare da esso.
Parla il

4
Questa è una decisione di progettazione del linguaggio molto scarsa. Fin dall'inizio della mia nuova app ho deciso di sviluppare con swift ho bisogno di chiamare NSWindowController.init (windowNibName) dalla mia sottoclasse e non posso farlo :(
Uniqus

4
@Kaiserludi: Niente di utile, è stato chiuso con la seguente risposta: "Questo è di progettazione ed eventuali bug rilevanti sono stati risolti in quest'area."
Robert

21

Ritenere

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

Poiché tutti gli inizializzatori designati di classe A vengono sovrascritti, la classe B erediterà tutti gli inizializzatori di convenienza di A. Quindi l'esecuzione di questo produrrà

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

Ora, se l'inizializzatore designato B.init (a: b :) fosse autorizzato a chiamare l'inizializzatore di convenienza della classe base A.init (a :), ciò risulterebbe in una chiamata ricorsiva a B.init (a:, b: ).


È facile da aggirare. Non appena si chiama l'inizializzatore di convenienza di una superclasse dall'interno di un inizializzatore designato, gli inizializzatori di convenienza della superclasse non verranno più ereditati.
fluidsonic

@fluidsonic ma sarebbe folle perché la struttura e i metodi di classe cambierebbero a seconda di come sono stati utilizzati. Immagina il divertimento del debug!
kdazzle

2
@kdazzle Né la struttura della classe né i suoi metodi di classe cambierebbero. Perché dovrebbero? - L'unico problema a cui riesco a pensare è che gli inizializzatori di convenienza devono delegare dinamicamente all'inizializzatore designato di una sottoclasse quando ereditato e staticamente all'inizializzatore designato della propria classe quando non ereditato ma delegato a da una sottoclasse.
fluidsonic

Penso che puoi anche ottenere un problema di ricorsione simile con metodi normali. Dipende dalla tua decisione quando chiami i metodi. Ad esempio, sarebbe sciocco se una lingua non consentisse le chiamate ricorsive perché potresti entrare in un ciclo infinito. I programmatori dovrebbero capire cosa stanno facendo. :)
Ferran Maylinch

13

È perché puoi finire con una ricorsione infinita. Ritenere:

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 guarda:

let a = SubClass()

chi chiamerà SubClass.init()chi chiamerà SuperClass.init(value:)chi chiamerà SubClass.init().

Le regole di inizializzazione designate / convenienza sono progettate in modo che l'inizializzazione di una classe sarà sempre corretta.


1
Sebbene una sottoclasse non possa chiamare esplicitamente inizializzatori di convenienza dalla sua superclasse, può ereditarli , dato che la sottoclasse fornisce l'implementazione di tutti gli inizializzatori designati dalla sua superclasse (vedi ad esempio questo esempio ). Quindi il tuo esempio sopra è effettivamente vero, ma è un caso speciale non esplicitamente correlato all'inizializzatore di convenienza di una superclasse, ma piuttosto al fatto che un inizializzatore designato potrebbe non chiamare uno di convenienza , poiché questo produrrà scenari ricorsivi come quello sopra .
dfrib

1

Ho trovato una soluzione per questo. Non è super carino, ma risolve il problema di non conoscere i valori di una superclasse o di voler impostare valori predefiniti.

Tutto quello che devi fare è creare un'istanza della superclasse, usando la comodità init, proprio nella initsottoclasse. Quindi chiami il designato initdel super usando l'istanza che hai appena creato.

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

Prendi in considerazione l'estrazione del codice di inizializzazione dalla tua comoda init()a una nuova funzione di supporto foo(), chiama foo(...)per eseguire l'inizializzazione nella tua sottoclasse.


Buon suggerimento, ma in realtà non risponde alla domanda.
Swifts Namesake

0

Guarda il video WWDC "403 intermedio Swift" alle 18:30 per una spiegazione approfondita degli inizializzatori e della loro eredità. A quanto ho capito, considera quanto segue:

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

Ma ora considera una sottoclasse Wyrm: un Wyrm è un drago senza gambe e senza ali. Quindi l'inizializzatore per una viverna (2 gambe, 2 ali) è sbagliato! Questo errore può essere evitato se non è possibile chiamare la convenienza Wyvern-Initializer ma solo l'inizializzatore designato completo:

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

12
Non è proprio un motivo. E se creo una sottoclasse quando initWyvernha senso essere chiamato?
Sulthan

4
Sì, non sono convinto. Non c'è nulla che impedisca Wyrmdi sovrascrivere il numero di segmenti dopo aver chiamato un inizializzatore di convenienza.
Robert

È il motivo indicato nel video WWDC (solo con auto -> auto da corsa e una proprietà hasTurbo booleana). Se la sottoclasse implementa tutti gli inizializzatori designati, eredita anche gli inizializzatori di convenienza. Ne vedo un po 'il senso, inoltre onestamente non ho avuto molti problemi con il modo in cui Objective-C ha funzionato. Vedi anche la nuova convenzione per chiamare super.init per ultimo nell'init, non per primo come in Objective-C.
Ralf

IMO la convenzione per chiamare super.init "last" non è concettualmente nuova, è la stessa di quella in object-c, solo che lì, a tutto veniva assegnato automaticamente un valore iniziale (nil, 0, qualunque). È solo che in Swift, dobbiamo fare noi stessi questa fase di inizializzazione. Un vantaggio di questo approccio è che abbiamo anche la possibilità di assegnare un valore iniziale diverso.
Roshan

-1

Perché non hai solo due inizializzatori, uno con un valore predefinito?

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.