Pourquoi le mot-clé de commodité est-il même nécessaire dans Swift?

132

Puisque Swift prend en charge la surcharge de méthode et d'initialisation, vous pouvez mettre plusieurs à initcôté de l'autre et utiliser ce que vous jugez pratique:

class Person {
    var name:String

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

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

Alors, pourquoi le conveniencemot-clé existerait-il même? Qu'est-ce qui améliore considérablement les éléments suivants?

class Person {
    var name:String

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

    convenience init() {
        self.init(name: "John")
    }
}
Desmond Hume
la source
13
Je lisais juste ceci dans la documentation et je me suis également trompé à ce sujet. : /
boidkan

Réponses:

235

Les réponses existantes ne racontent que la moitié de l' conveniencehistoire. L'autre moitié de l'histoire, la moitié qu'aucune des réponses existantes ne couvre, répond à la question que Desmond a postée dans les commentaires:

Pourquoi Swift me forcerait-il à mettre conveniencedevant mon initialiseur simplement parce que j'ai besoin d'appeler self.initdepuis celui-ci?

Je l'ai légèrement abordé dans cette réponse , dans laquelle je couvre en détail plusieurs règles d'initialisation de Swift, mais l'accent était principalement mis sur le requiredmot. Mais cette réponse concernait toujours quelque chose qui est pertinent pour cette question et cette réponse. Nous devons comprendre comment fonctionne l'héritage de l'initialisation Swift.

Étant donné que Swift n'autorise pas les variables non initialisées, vous n'êtes pas assuré d'hériter de tous les initialiseurs (ou de certains) de la classe dont vous héritez. Si nous sous-classons et ajoutons des variables d'instance non initialisées à notre sous-classe, nous avons arrêté d'hériter des initialiseurs. Et jusqu'à ce que nous ajoutions nos propres initialiseurs, le compilateur nous hurlera dessus.

Pour être clair, une variable d'instance non initialisée est toute variable d'instance qui n'a pas de valeur par défaut (en gardant à l'esprit que les options et les options implicitement déballées prennent automatiquement la valeur par défaut de nil).

Donc dans ce cas:

class Foo {
    var a: Int
}

aest une variable d'instance non initialisée. Cela ne se compilera que si nous donnons aune valeur par défaut:

class Foo {
    var a: Int = 0
}

ou initialisez adans une méthode d'initialisation:

class Foo {
    var a: Int

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

Maintenant, voyons ce qui se passe si nous sous Foo- classons , d'accord?

class Bar: Foo {
    var b: Int

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

Droite? Nous avons ajouté une variable, et nous avons ajouté un initialiseur pour définir une valeur bafin qu'il compile. En fonction de la langue que vous venez, vous pourriez attendre à ce que Bara hérité Foode initialiseur de », init(a: Int). Mais ce n'est pas le cas. Et comment le pourrait-il? Comment Foode » init(a: Int)le savoir comment attribuer une valeur à la bvariable Barajoutée? Ce n'est pas le cas. Nous ne pouvons donc pas initialiser une Barinstance avec un initialiseur qui ne peut pas initialiser toutes nos valeurs.

Qu'est-ce que tout cela a à voir avec convenience?

Eh bien, regardons les règles sur l'héritage d'initialiseur :

Règle 1

Si votre sous-classe ne définit aucun initialiseur désigné, elle hérite automatiquement de tous ses initialiseurs désignés par superclasse.

Règle 2

Si votre sous-classe fournit une implémentation de tous ses initialiseurs désignés de superclasse - soit en les héritant selon la règle 1, soit en fournissant une implémentation personnalisée dans le cadre de sa définition - alors elle hérite automatiquement de tous les initialiseurs de commodité de superclasse.

Remarquez la règle 2, qui mentionne les initialiseurs de commodité.

Ainsi, ce que fait le conveniencemot - clé , c'est nous indiquer quels initialiseurs peuvent être hérités par des sous-classes qui ajoutent des variables d'instance sans valeurs par défaut.

Prenons cet exemple de Baseclasse:

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

Notez que nous avons trois convenienceinitialiseurs ici. Cela signifie que nous avons trois initialiseurs qui peuvent être hérités. Et nous avons un initialiseur désigné (un initialiseur désigné est simplement n'importe quel initialiseur qui n'est pas un initialiseur pratique).

Nous pouvons instancier des instances de la classe de base de quatre manières différentes:

entrez la description de l'image ici

Alors, créons une sous-classe.

class NonInheritor: Base {
    let c: Int

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

Nous héritons de Base. Nous avons ajouté notre propre variable d'instance et nous ne lui avons pas donné de valeur par défaut, nous devons donc ajouter nos propres initialiseurs. Nous avons ajouté un, init(a: Int, b: Int, c: Int)mais il ne correspond pas à la signature de la Baseclasse est désignée initialiseur: init(a: Int, b: Int). Cela signifie que nous n'héritons d' aucun initialiseur de Base:

entrez la description de l'image ici

Alors, que se passerait-il si nous héritions de Base, mais que nous allions de l'avant et implémentions un initialiseur qui correspondait à l'initialiseur désigné 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)
    }
}

Maintenant, en plus des deux initialiseurs que nous avons implémentés directement dans cette classe, parce que nous avons implémenté l'initialiseur Basedésigné d' une classe correspondant à un initialiseur, nous pouvons hériter de tous Baseles convenienceinitialiseurs de classe :

entrez la description de l'image ici

Le fait que l'initialiseur avec la signature correspondante soit marqué comme conveniencene fait aucune différence ici. Cela signifie seulement qu'il Inheritorn'y a qu'un seul initialiseur désigné. Donc, si nous héritons de Inheritor, nous aurions juste à implémenter cet initialiseur désigné, puis nous hériterions Inheritorde l'initialiseur de commodité, ce qui signifie que nous avons implémenté tous Baseles initialiseurs désignés et que nous pouvons hériter de ses convenienceinitialiseurs.

nhgrif
la source
16
La seule réponse qui répond réellement à la question et suit la documentation. Je l'accepterais si j'étais l'OP.
FreeNickname le
12
Vous devriez écrire un livre;)
coolbeet
1
@SLN Cette réponse couvre beaucoup de choses sur le fonctionnement de l'héritage d'initialisation Swift.
nhgrif le
1
@SLN Parce que créer une barre avec init(a: Int)laisserait bnon initialisé.
Ian Warburton
2
@IanWarburton Je ne connais pas la réponse à ce "pourquoi" particulier. Votre logique dans la deuxième partie de votre commentaire me semble saine, mais la documentation indique clairement que c'est ainsi que cela fonctionne, et donner un exemple de ce que vous demandez dans un Playground confirme que le comportement correspond à ce qui est documenté.
nhgrif
9

Surtout la clarté. De votre deuxième exemple,

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

est requis ou désigné . Il doit initialiser toutes vos constantes et variables. Les initialiseurs de commodité sont facultatifs et peuvent généralement être utilisés pour faciliter l'initialisation. Par exemple, disons que votre classe Person a une variable de genre facultative:

var gender: Gender?

où Gender est une énumération

enum Gender {
  case Male, Female
}

vous pourriez avoir des initialiseurs pratiques comme celui-ci

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

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

Les initialiseurs de commodité doivent appeler les initialiseurs désignés ou requis. Si votre classe est une sous-classe, elle doit appeler super.init() dans son initialisation.

Nate Mann
la source
2
Ce serait donc parfaitement évident pour le compilateur ce que j'essaie de faire avec plusieurs initialiseurs même sans conveniencemot clé, mais Swift serait toujours en train de boguer à ce sujet. Ce n'est pas le genre de simplicité que j'attendais d'Apple =)
Desmond Hume
2
Cette réponse ne répond à rien. Vous avez dit «clarté», mais vous n'avez pas expliqué comment cela rend quelque chose plus clair.
Robo Robok
7

Eh bien, la première chose qui me vient à l'esprit est qu'il est utilisé dans l'héritage de classe pour l'organisation et la lisibilité du code. Continuez avec votre Personclasse, pensez à un scénario comme celui-ci

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}

Avec l'initialiseur pratique, je suis capable de créer un Employee()objet sans valeur, d'où le motconvenience

u54r
la source
2
Avec les conveniencemots clés supprimés, Swift n'obtiendrait-il pas suffisamment d'informations pour se comporter exactement de la même manière?
Desmond Hume
Non, si vous supprimez le conveniencemot-clé, vous ne pouvez pas initialiser l' Employeeobjet sans aucun argument.
u54r
Plus précisément, l'appel Employee()appelle l' convenienceinitialiseur (hérité, dû à ) init(), qui appelle self.init(name: "Unknown"). init(name: String), également un initialiseur pratique pour Employee, appelle l'initialiseur désigné.
BallpointBen
1

Outre les points que d'autres utilisateurs ont expliqués, voici mon peu de compréhension.

Je ressens fortement la connexion entre l'initialiseur de commodité et les extensions. Quant à moi, les initialiseurs de commodité sont les plus utiles lorsque je veux modifier (dans la plupart des cas, le rendre court ou facile) l'initialisation d'une classe existante.

Par exemple, une classe tierce que vous utilisez a initavec quatre paramètres mais dans votre application les deux derniers ont la même valeur. Pour éviter plus de frappe et rendre votre code propre, vous pouvez définir un convenience initavec seulement deux paramètres et à l'intérieur, appeler self.initavec last to paramètres avec des valeurs par défaut.

Abdullah
la source
1
Pourquoi Swift me forcerait-il à mettre conveniencedevant mon initialiseur simplement parce que j'ai besoin d'appeler self.initdepuis celui-ci? Cela semble redondant et un peu gênant.
Desmond Hume
1

Selon la documentation Swift 2.1 , les convenienceinitialiseurs doivent adhérer à certaines règles spécifiques:

  1. Un convenienceinitialiseur ne peut appeler les initiateurs que dans la même classe, pas dans les super classes (uniquement à travers, pas en haut)

  2. Un convenienceinitialiseur doit appeler un initialiseur désigné quelque part dans la chaîne

  3. Un convenienceinitialiseur ne peut pas changer de propriété ANY avant d'avoir appelé un autre initialiseur - alors qu'un initialiseur désigné doit initialiser les propriétés introduites par la classe actuelle avant d'appeler un autre initialiseur.

En utilisant le conveniencemot - clé, le compilateur Swift sait qu'il doit vérifier ces conditions - sinon il ne le pourrait pas.

L'oeil
la source
On peut soutenir que le compilateur pourrait probablement résoudre ce problème sans le conveniencemot clé.
nhgrif le
De plus, votre troisième point est trompeur. Un initialiseur pratique ne peut modifier que les propriétés (et ne peut pas modifier les letpropriétés). Il ne peut pas initialiser les propriétés. Un initialiseur désigné a la responsabilité d'initialiser toutes les propriétés introduites avant d'appeler un superinitialiseur désigné.
nhgrif
1
Au moins, le mot-clé de commodité indique clairement au développeur, c'est aussi la lisibilité qui compte (en plus de vérifier l'initialiseur par rapport aux attentes du développeur). Votre deuxième point est bon, j'ai changé ma réponse en conséquence.
TheEye
1

Une classe peut avoir plusieurs initialiseurs désignés. Un initialiseur pratique est un initialiseur secondaire qui doit appeler un initialiseur désigné de la même classe.

Abdul Yasin
la source