Pourquoi les initialiseurs Swift ne peuvent-ils pas appeler les initialiseurs de commodité sur leur superclasse?

88

Considérez les deux 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'
    }
}

Je ne vois pas pourquoi ce n'est pas autorisé. En fin de compte, l'initialiseur désigné de chaque classe est appelée avec toutes les valeurs dont ils ont besoin, alors pourquoi dois - je me répéter dans B« s initen spécifiant une valeur par défaut de xnouveau, lorsque la commodité initen Afera très bien?

Robert
la source
2
J'ai cherché une réponse mais je n'en trouve pas qui me satisfasse. C'est probablement une raison de mise en œuvre. Peut-être que la recherche d'initialiseurs désignés dans une autre classe est beaucoup plus facile que de rechercher des initialiseurs pratiques ... ou quelque chose comme ça.
Sulthan
@Robert, merci pour vos commentaires ci-dessous. Je pense que vous pourriez les ajouter à votre question, ou même poster une réponse avec ce que vous avez reçu: "C'est par conception et tous les bugs pertinents ont été résolus dans ce domaine.". Il semble donc qu'ils ne peuvent ou ne veulent pas expliquer la raison.
Ferran Maylinch

Réponses:

24

Il s'agit de la règle 1 des règles "Initializer Chaining" comme spécifié dans le Guide de programmation Swift, qui se lit comme suit:

Règle 1: les initialiseurs désignés doivent appeler un initialiseur désigné à partir de leur superclasse immédiate.

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

Soulignez le mien. Les initialiseurs désignés ne peuvent pas appeler les initialiseurs pratiques.

Il existe un diagramme qui accompagne les règles pour montrer quelles "directions" d'initialisation sont autorisées:

Chaîne d'initialisation

Craig Otis
la source
80
Mais pourquoi est-ce fait de cette façon? La documentation dit simplement que cela simplifie la conception, mais je ne vois pas comment c'est le cas si je dois continuer à me répéter en spécifiant continuellement des valeurs par défaut sur les initialiseurs désignés. les initialiseurs feront-ils?
Robert
5
J'ai déposé un bogue 17266917 quelques jours après cette question, demandant de pouvoir appeler des initialiseurs de convenance. Pas encore de réponse, mais je n'en ai eu aucune pour aucun de mes autres rapports de bogues Swift!
Robert
8
Espérons qu'ils permettront d'appeler des initialiseurs de commodité. Pour certaines classes du SDK, il n'y a pas d'autre moyen d'obtenir un certain comportement que d'appeler l'initialiseur de commodité. Voir SCNGeometry: vous pouvez ajouter des SCNGeometryElements uniquement en utilisant l'initialiseur de commodité, donc on ne peut pas en hériter.
Spak
4
C'est une très mauvaise décision de conception de langage. Dès le début de ma nouvelle application, j'ai décidé de développer avec swift, je dois appeler NSWindowController.init (windowNibName) à partir de ma sous-classe, et je ne peux tout simplement pas le faire :(
Uniqus
4
@Kaiserludi: Rien d'utile, il a été fermé avec la réponse suivante: "C'est par conception et tous les bugs pertinents ont été résolus dans ce domaine."
Robert
21

Considérer

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

Étant donné que tous les initialiseurs désignés de la classe A sont remplacés, la classe B héritera de tous les initialiseurs de commodité de A. Donc, l'exécution de ceci produira

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

Maintenant, si l'initialiseur désigné B.init (a: b :) était autorisé à appeler l'initialiseur de commodité de classe de base A.init (a :), cela entraînerait un appel récursif à B.init (a:, b: ).

beba
la source
C'est facile à contourner. Dès que vous appelez un initialiseur de commodité de superclasse à partir d'un initialiseur désigné, les initialiseurs de commodité de la superclasse ne seront plus hérités.
fluidsonic
@fluidsonic mais ce serait insensé car votre structure et vos méthodes de classe changeraient en fonction de la façon dont elles étaient utilisées. Imaginez le plaisir de débogage!
kdazzle
2
@kdazzle Ni la structure de la classe ni ses méthodes de classe ne changeraient. Pourquoi devraient-ils? - Le seul problème auquel je peux penser est que les initialiseurs de commodité doivent déléguer dynamiquement à l'initialiseur désigné d'une sous-classe lorsqu'ils sont hérités et statiquement à l'initialiseur désigné de sa propre classe lorsqu'ils ne sont pas hérités mais délégués à une sous-classe.
fluidsonic
Je pense que vous pouvez également avoir un problème de récursivité similaire avec les méthodes normales. Cela dépend de votre décision lors de l'appel des méthodes. Par exemple, ce serait idiot si un langage n'autorisait pas les appels récursifs car vous pourriez entrer dans une boucle infinie. Les programmeurs doivent comprendre ce qu'ils font. :)
Ferran Maylinch
13

C'est parce que vous pouvez vous retrouver avec une récursion infinie. Considérer:

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

et regardez:

let a = SubClass()

qui appellera SubClass.init()qui appellera SuperClass.init(value:)qui appellera SubClass.init().

Les règles d'initialisation désignées / pratiques sont conçues pour qu'une initialisation de classe soit toujours correcte.

Julien
la source
1
Bien qu'une sous-classe ne puisse pas appeler explicitement des initialiseurs de convenance depuis sa superclasse, elle peut en hériter , étant donné que la sous-classe fournit l'implémentation de tous ses initialiseurs désignés par la superclasse (voir par exemple cet exemple ). Par conséquent, votre exemple ci-dessus est en effet vrai, mais il s'agit d'un cas particulier qui n'est pas explicitement lié à l'initialiseur de commodité d'une superclasse, mais plutôt au fait qu'un initialiseur désigné ne peut pas appeler un initialiseur de commodité , car cela conduira à des scénarios récursifs tels que celui ci-dessus .
dfri le
1

J'ai trouvé une solution pour cela. Ce n'est pas super joli, mais cela résout le problème de ne pas connaître les valeurs d'une superclasse ou de vouloir définir des valeurs par défaut.

Tout ce que vous avez à faire est de créer une instance de la superclasse, en utilisant la commodité init, directement dans initla sous-classe. Ensuite, vous appelez le désigné initdu super en utilisant l'instance que vous venez de créer.

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
la source
1

Pensez à extraire le code d'initialisation de votre pratique init()vers une nouvelle fonction d'assistance foo(), appelez foo(...)pour effectuer l'initialisation dans votre sous-classe.

Evanchin
la source
Bonne suggestion, mais elle ne répond pas vraiment à la question.
SwiftsNamesake
0

Regardez la vidéo WWDC "403 intermediaire Swift" à 18h30 pour une explication détaillée des initialiseurs et de leur héritage. D'après ce que j'ai compris, considérez ce qui suit:

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

Mais maintenant, considérez une sous-classe Wyrm: Un Wyrm est un Dragon sans pattes et sans ailes. Donc l'initialiseur pour un Wyvern (2 pattes, 2 ailes) est faux! Cette erreur peut être évitée si la commodité Wyvern-Initializer ne peut tout simplement pas être appelée, mais uniquement l'initialiseur désigné complet:

class Wyrm: Dragon {
    init() {
        super.init(legs: 0, isFlying: false)
    }
}
Ralf
la source
12
Ce n'est pas vraiment une raison. Et si je initWyverncrée une sous-classe quand il est logique d'être appelé?
Sulthan
4
Ouais, je ne suis pas convaincu. Rien n'empêche Wyrmde remplacer le nombre de segments après avoir appelé un initialiseur de commodité.
Robert
C'est la raison donnée dans la vidéo de la WWDC (uniquement avec les voitures -> voitures de course et une propriété booléenne hasTurbo). Si la sous-classe implémente tous les initialiseurs désignés, elle hérite également des initialiseurs de commodité. J'en vois un peu le sens, et honnêtement, je n'ai eu aucun problème avec le fonctionnement d'Objective-C. Voir aussi la nouvelle convention pour appeler super.init en dernier dans l'init, pas en premier comme dans Objective-C.
Ralf
OMI, la convention d'appeler super.init "last" n'est pas conceptuellement nouvelle, c'est la même chose que celle d'Objectif-c, juste que là, tout se voyait attribuer automatiquement une valeur initiale (nil, 0, peu importe). C'est juste que dans Swift, nous devons faire cette phase d'initialisation nous-mêmes. Un avantage de cette approche est que nous avons également la possibilité d'attribuer une valeur initiale différente.
Roshan
-1

Pourquoi n'avez-vous pas simplement deux initialiseurs - un avec une valeur par défaut?

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
la source