Perché la parola chiave convenienza è necessaria anche in Swift?


132

Poiché Swift supporta il sovraccarico di metodi e inizializzatori, puoi metterne più inituno accanto all'altro e utilizzare quello che ritieni conveniente:

class Person {
    var name:String

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

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

Quindi perché una convenienceparola chiave dovrebbe esistere? Cosa rende sostanzialmente meglio quanto segue?

class Person {
    var name:String

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

    convenience init() {
        self.init(name: "John")
    }
}

13
Stavo solo leggendo questo nella documentazione e ne ero confuso. : /
boidkan,

Risposte:


235

Le risposte esistenti raccontano solo metà della conveniencestoria. L'altra metà della storia, la metà che nessuna delle risposte esistenti copre, risponde alla domanda che Desmond ha pubblicato nei commenti:

Perché Swift mi costringerebbe a mettere conveniencedavanti al mio inizializzatore solo perché ho bisogno di chiamarlo self.init? `

L'ho toccato leggermente in questa risposta , in cui tratterò dettagliatamente alcune delle regole di inizializzazione di Swift, ma il focus principale era sulla requiredparola. Ma quella risposta stava ancora affrontando qualcosa che è rilevante per questa domanda e questa risposta. Dobbiamo capire come funziona l'ereditarietà dell'inizializzatore Swift.

Poiché Swift non consente variabili non inizializzate, non è garantito che erediti tutti (o alcuno) gli inizializzatori dalla classe da cui erediti. Se eseguiamo la sottoclasse e aggiungiamo variabili di istanza non inizializzate alla nostra sottoclasse, abbiamo smesso di ereditare gli inizializzatori. E finché non aggiungiamo i nostri inizializzatori, il compilatore ci urlerà contro.

Per essere chiari, una variabile di istanza non inizializzata è una qualsiasi variabile di istanza a cui non viene assegnato un valore predefinito (tenendo presente che gli opzionali e quelli implicitamente non scartati assumono automaticamente un valore predefinito di nil).

Quindi in questo caso:

class Foo {
    var a: Int
}

aè una variabile di istanza non inizializzata. Questo non verrà compilato a meno che non diamo aun valore predefinito:

class Foo {
    var a: Int = 0
}

o inizializza acon un metodo di inizializzazione:

class Foo {
    var a: Int

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

Ora, vediamo cosa succede se effettuiamo la sottoclasse Foo, vero?

class Bar: Foo {
    var b: Int

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

Destra? Abbiamo aggiunto una variabile e aggiunto un inizializzatore per impostare un valore in bmodo che venga compilato. A seconda di quale lingua si sta venendo da, ci si potrebbe aspettare che Barha ereditato Foo's inizializzatore, init(a: Int). Ma non lo fa. E come potrebbe? Come fa Foo's init(a: Int)know how per assegnare un valore alla bvariabile che Barha aggiunto? Non Quindi non possiamo inizializzare aBar un'istanza con un inizializzatore che non può inizializzare tutti i nostri valori.

Cosa c'entra tutto questo convenience ?

Bene, diamo un'occhiata alle regole sull'ereditarietà dell'inizializzatore :

Regola 1

Se la sottoclasse non definisce alcun inizializzatore designato, eredita automaticamente tutti i suoi inizializzatori designati superclasse.

Regola 2

Se la sottoclasse fornisce un'implementazione di tutti i suoi inizializzatori designati della superclasse, ereditandoli secondo la regola 1 o fornendo un'implementazione personalizzata come parte della sua definizione, eredita automaticamente tutti gli inizializzatori di convenienza della superclasse.

Nota la Regola 2, che menziona gli inizializzatori di convenienza.

Così che la convenienceparola chiave non indicare a noi che inizializzatori possono essere ereditate dalle sottoclassi che le variabili di istanza aggiuntivo senza valori di default.

Prendiamo questa Baseclasse di esempio :

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

Si noti che ne abbiamo tre convenience inizializzatori qui. Ciò significa che abbiamo tre inizializzatori che possono essere ereditati. E abbiamo un inizializzatore designato (un inizializzatore designato è semplicemente un inizializzatore che non è un inizializzatore di convenienza).

Possiamo istanziare istanze della classe base in quattro modi diversi:

inserisci qui la descrizione dell'immagine

Quindi, creiamo una sottoclasse.

class NonInheritor: Base {
    let c: Int

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

Stiamo ereditando da Base. Abbiamo aggiunto la nostra variabile di istanza e non gli abbiamo dato un valore predefinito, quindi dobbiamo aggiungere i nostri inizializzatori. Abbiamo aggiunto uno, init(a: Int, b: Int, c: Int)ma non corrisponde alla firma del Basecodice categoria è designato inizializzatore: init(a: Int, b: Int). Ciò significa che non stiamo ereditando alcun inizializzatore da Base:

inserisci qui la descrizione dell'immagine

Quindi, cosa accadrebbe se ereditassimo Base, ma andassimo avanti e implementassimo un inizializzatore che corrispondeva all'inizializzatore designato 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)
    }
}

Ora, oltre ai due inizializzatori che abbiamo implementato direttamente in questa classe, poiché abbiamo implementato un inizializzatore che Baseidentifica l'inizializzatore designato della classe, possiamo ereditare tutti Basegli convenienceinizializzatori della classe :

inserisci qui la descrizione dell'immagine

Il fatto che l'inizializzatore con la firma corrispondente sia contrassegnato come conveniencequi non fa differenza. Significa solo che Inheritorha un solo inizializzatore designato. Quindi, se ereditiamo Inheritor, dovremmo solo implementare quell'inizializzatore designato e quindi erediteremoInheritor l'inizializzatore di convenienza, il che a sua volta significa che abbiamo implementato tutti gli Baseinizializzatori designati e possiamo ereditare i suoi convenienceinizializzatori.


16
L'unica risposta che risponde effettivamente alla domanda e segue i documenti. Lo accetterei se fossi il PO.
FreeNickname

12
Dovresti scrivere un libro;)
coolbeet,

1
@SLN Questa risposta riguarda molto il funzionamento dell'ereditarietà dell'inizializzatore Swift.
nhgrif,

1
@SLN Perché la creazione di una barra con init(a: Int)verrebbe lasciata bnon inizializzata.
Ian Warburton,

2
@IanWarburton Non conosco la risposta a questo particolare "perché". La tua logica nella seconda parte del tuo commento mi sembra sana, ma la documentazione afferma chiaramente che funziona così e vomitare un esempio di ciò che stai chiedendo in un Playground conferma che il comportamento corrisponde a ciò che è documentato.
nhgrif,

9

Principalmente chiarezza. Dal tuo secondo esempio,

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

è richiesto o designato . Deve inizializzare tutte le costanti e le variabili. Gli inizializzatori convenienti sono opzionali e in genere possono essere utilizzati per rendere più semplice l'inizializzazione. Ad esempio, supponiamo che la tua classe Person abbia un genere variabile facoltativo:

var gender: Gender?

dove Gender è un enum

enum Gender {
  case Male, Female
}

potresti avere inizializzatori di convenienza come questo

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

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

Gli inizializzatori di convenienza devono chiamare gli inizializzatori designati o richiesti in essi contenuti. Se la tua classe è una sottoclasse, deve chiamare super.init() all'interno della sua inizializzazione.


2
Quindi sarebbe perfettamente ovvio per il compilatore cosa sto cercando di fare con più inizializzatori anche senza convenienceparole chiave, ma Swift continuerebbe a infastidirlo. Non è il tipo di semplicità che mi aspettavo da Apple =)
Desmond Hume,

2
Questa risposta non risponde a nulla. Hai detto "chiarezza", ma non hai spiegato come chiarire qualcosa.
Robo Robok,

7

Bene, la prima cosa che mi viene in mente è che viene utilizzato in eredità di classe per l'organizzazione del codice e la leggibilità. Continuando con la tua Personclasse, pensa a uno scenario come questo

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}

Con un comodo inizializzatore sono in grado di creare un Employee()oggetto senza valore, da cui la parolaconvenience


2
Con le convenienceparole chiave rimosse, Swift non otterrebbe abbastanza informazioni per comportarsi nello stesso modo esatto?
Desmond Hume,

No, se togli la convenienceparola chiave non puoi inizializzare l' Employeeoggetto senza alcun argomento.
u54r,

In particolare, chiamando Employee()chiama l' convenienceinizializzatore (ereditato, dovuto a ) init(), che chiama self.init(name: "Unknown"). init(name: String), anche un inizializzatore di convenienza per Employee, chiama l'inizializzatore designato.
BallpointBen,

1

A parte i punti che altri utenti hanno spiegato qui è la mia comprensione.

Sento fortemente la connessione tra inizializzatore di convenienza ed estensioni. Per quanto mi riguarda, gli inizializzatori sono molto utili quando voglio modificare (nella maggior parte dei casi lo rendono breve o facile) l'inizializzazione di una classe esistente.

Ad esempio, alcune classi di terze parti utilizzate hanno initquattro parametri, ma nell'applicazione le ultime due hanno lo stesso valore. Per evitare una maggiore digitazione e rendere più pulito il codice, è possibile definire unconvenience init con solo due parametri e al suo interno chiamare self.initcon last i parametri con valori predefiniti.


1
Perché Swift mi costringerebbe a mettere conveniencedavanti al mio inizializzatore solo perché ho bisogno di chiamarlo self.init? Questo sembra ridondante e un po 'scomodo.
Desmond Hume,

1

Secondo la documentazione di Swift 2.1 , gli convenienceinizializzatori devono aderire ad alcune regole specifiche:

  1. Un convenienceinizializzatore può chiamare solo inizializzatori nella stessa classe, non in superclassi (solo attraverso, non in alto)

  2. Un convenienceinizializzatore deve chiamare un inizializzatore designato da qualche parte nella catena

  3. Un convenienceinizializzatore non può modificare QUALSIASI proprietà prima di aver chiamato un altro inizializzatore, mentre un inizializzatore designato deve inizializzare le proprietà introdotte dalla classe corrente prima di chiamare un altro inizializzatore.

Usando la convenienceparola chiave, il compilatore Swift sa che deve verificare queste condizioni, altrimenti non potrebbe.


Probabilmente, il compilatore potrebbe probabilmente risolverlo senza la convenienceparola chiave.
nhgrif,

Inoltre, il tuo terzo punto è fuorviante. Un inizializzatore di convenienza può solo modificare le proprietà (e non può cambiare le letproprietà). Non può inizializzare le proprietà. Un inizializzatore designato ha la responsabilità di inizializzare tutte le proprietà introdotte prima di chiamare un superinizializzatore designato.
nhgrif,

1
Almeno la parola chiave convenienza chiarisce allo sviluppatore, è anche la leggibilità che conta (oltre a verificare l'inizializzatore rispetto alle aspettative dello sviluppatore). Il tuo secondo punto è positivo, ho modificato la mia risposta di conseguenza.
TheEye

1

Una classe può avere più di un inizializzatore designato. Un inizializzatore di convenienza è un inizializzatore secondario che deve chiamare un inizializzatore designato della stessa classe.

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.