In dit artikel zal ik enkele van de gruizige details van Swift initializers verkennen. Waarom zo ‘ n spannend onderwerp? Vooral omdat ik af en toe in de war ben geraakt door compilatiefouten bij het maken van een subklasse van UIView, of bij het maken van een snelle versie van een oude Objective-C klasse, bijvoorbeeld. Dus besloot ik dieper te graven om een beter begrip te krijgen van de nuances van Swift initializers. Aangezien mijn achtergrond / ervaring vooral te maken heeft met het werken met Groovy, Java en Javascript, zal ik enkele van Swift ‘ s initialisatiemechanismen vergelijken met die van Java & Groovy. Ik moedig je aan om een speeltuin in XCode op te starten en zelf met de codevoorbeelden te spelen. Ik moedig u ook aan om de sectie initialisatie van de Swift 2.1 Taalgids door te lezen, die de primaire bron van informatie voor dit artikel is. Tot slot zal mijn focus liggen op referentietypen (klassen) in plaats van waardetypen (structs).
Wat Is Een Initializer?
een initializer is een speciaal type methode in een Swift struct, class of enum die verantwoordelijk is voor het zorgen dat een nieuw aangemaakte instantie van de struct, class of enum volledig geïnitialiseerd is voordat ze klaar zijn om gebruikt te worden. Ze spelen dezelfde rol als die van een” constructeur ” in Java en Groovy. Als je bekend bent met Objective-C, moet je er rekening mee houden dat Swift initializers verschillen van Objective-C initializers in dat ze geen waarde retourneren.
subklassen erven doorgaans geen Initializers
een van de eerste dingen om in gedachten te houden is dat “Swift-subklassen standaard hun superclass initializers niet erven”, volgens de language guide. (De gids legt uit dat er scenario ‘ s zijn waar superclass initializers automatisch worden overgenomen. We zullen die uitzonderlijke scenario ‘ s later behandelen). Dit is consistent met hoe Java (en bij uitbreiding, Groovy) werkt. Overweeg het volgende:
net als bij Java & Groovy, is het logisch dat dit niet is toegestaan (hoewel, zoals bij de meeste dingen, er op dit punt enig argument kan zijn. Zie deze StackOverflow post). Als het zou worden toegestaan, zou de initialisatie van de “instrument” eigendom van muzikant worden omzeild, waardoor uw muzikant instantie mogelijk in een ongeldige staat. Echter, met Groovy zou ik normaal gesproken geen moeite doen met het schrijven van initializers (dwz constructors). Liever, ik zou gewoon gebruik maken van de kaart constructor Groovy biedt impliciet, waarmee u vrij te kiezen en te kiezen welke eigenschappen u wilt instellen op de bouw. Bijvoorbeeld, het volgende is perfect geldig Groovy code:
merk op dat je elke eigenschap kunt opnemen, inclusief die van superclasses, maar je hoeft niet alle (of een) van hen op te geven, en ze hoeven niet in een bepaalde volgorde te worden gespecificeerd. Dit soort ultra-flexibele initializer wordt niet geleverd door Swift. Het dichtstbijzijnde ding in Swift is de automatisch verstrekte ledenwise initializers voor structs. Maar de volgorde van de argumenten in een ledenwise initializer is significant, ook al zijn ze genoemd, en hangt af van de volgorde waarin ze zijn gedefinieerd:
hoe dan ook, back to classes – Groovy ’s filosofie met betrekking tot post-construction object validity is duidelijk heel anders dan Swift’ s. Dit is slechts een van de vele manieren waarop Groovy verschilt van Swift.
aangewezen vs Convenience Initializers
voordat we te ver in het konijnenhol gaan, moeten we een belangrijk concept verduidelijken: In Swift, een initializer is gecategoriseerd als ofwel een aangewezen of een gemak initializer. Ik zal proberen het verschil zowel conceptueel als syntactisch uit te leggen. Elke klasse moet minstens één aangewezen initializer hebben, maar kan meerdere hebben (“aangewezen “betekent niet”single”). Een aangewezen initializer wordt beschouwd als een primaire initializer. Zij zijn de head-honchos. Zij zijn uiteindelijk verantwoordelijk om ervoor te zorgen dat alle eigenschappen worden geïnitialiseerd. Vanwege die verantwoordelijkheid, kan het soms een pijn worden om een aangewezen initializer de hele tijd te gebruiken, omdat ze meerdere argumenten kunnen vereisen. Stel je voor dat je werkt met een klasse die meerdere eigenschappen heeft, en je moet meerdere instanties van die klasse maken die bijna identiek zijn, behalve een één of twee eigenschappen. (In het belang van het argument, laten we ook aannemen dat er geen verstandige standaardwaarden die kunnen zijn toegewezen aan eigenschappen wanneer ze worden verklaard). Bijvoorbeeld, laten we zeggen dat onze persoon klasse had ook eatsFood en enjoysMusic eigenschappen. Natuurlijk, die twee dingen zouden worden ingesteld op waar de meeste van de tijd, maar je weet maar nooit. Neem een kijkje:
nu heeft onze persoon klasse vier eigenschappen die moeten worden ingesteld, en we hebben een aangewezen initializer die het werk kan doen. De aangewezen initializer is de eerste, degene die 4 argumenten heeft. Meestal zullen die laatste twee argumenten de waarde “Waar”hebben. Het zou vervelend zijn om ze te moeten specificeren elke keer als we een typische persoon willen creëren. Dat is waar de laatste twee initializers komen in, degenen gemarkeerd met de convenience modifier. Dit patroon moet vertrouwd lijken voor een Java-ontwikkelaar. Als je een constructor hebt die meer argumenten nodig heeft dan je echt de hele tijd nodig hebt, kun je vereenvoudigde constructors schrijven die een deelverzameling van die argumenten nemen en verstandige standaardwaarden voor de anderen bieden. De convenience initializers moeten ofwel delegeren aan een andere, misschien minder handige, convenience initializer of aan een aangewezen initializer. Uiteindelijk moet een aangewezen initializer betrokken raken. Verder, als dit een subklasse is, moet de aangewezen initializer een aangewezen initializer aanroepen vanuit zijn directe superklasse.
een echt voorbeeld voor het gebruik van de convenience modifier komt uit UIKit ‘ s uibezierpath klasse. Ik weet zeker dat je je kunt voorstellen dat er verschillende manieren zijn om een pad te specificeren. Als zodanig biedt UIBezierPath verschillende convenience initializers:
public convenience init(rect: CGRect)
public convenience init(ovalInRect: CGRect)
public convenience init(roundedRect: CGRect, cornerRadius: CGFloat)
public convenience init(roundedRect: CGRect, byRoundingCorners corners: UIRectCorner, cornerRadii: cgsize)
public convenience init(arccenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, in gevaar: CGFloat, met de klok mee: Bool)
public convenience init(CGPath: CGPath)
eerder zei ik dat een klasse meerdere aangewezen initializers kan hebben. Hoe ziet dat eruit? Een belangrijk verschil, afgedwongen door de compiler, tussen aangewezen initializers en gemak initializers is dat aangewezen initializers mag niet delegeren aan een andere initializer in dezelfde klasse (maar moet delegeren aan een aangewezen initializer in zijn onmiddellijke superklasse). Kijk naar de tweede initializer van persoon, degene die een enkel argument genaamd “unDead” neemt. Aangezien deze initializer niet is gemarkeerd met de convenience modifier, behandelt Swift het als een aangewezen initializer. Als zodanig, het mag niet delegeren aan een andere initializer in persoon. Probeer de eerste vier regels te becommentariëren en de laatste regel af te zetten. De compiler zal klagen, en XCode moet proberen om u te helpen door te suggereren dat u het te repareren door het toevoegen van de convenience modifier.
beschouw nu de musicus subklasse van persoon. Het heeft een enkele initializer, en het moet daarom een aangewezen initializer zijn. Als zodanig, moet het een aangewezen initializer van de onmiddellijke superklasse, persoon noemen. Onthoud: hoewel aangewezen initializers niet kunnen delegeren aan een andere initializer van dezelfde klasse, moeten convenience initializers dit doen. Ook moet een aangewezen initializer een aangewezen initializer van zijn directe superklasse aanroepen. Zie de taalgids voor meer details (en mooie afbeeldingen).
Initialisatiefasen
zoals de Swift language guide uitlegt, zijn er twee initialisatiefasen. De fasen worden afgebakend door de aanroep naar de superklasse aangewezen initializer. Fase 1 is voorafgaand aan de aanroep naar de superklasse aangewezen initializer, fase 2 is na. Een subklasse moet al zijn eigen eigenschappen initialiseren in Fase 1, en het mag geen eigenschappen instellen die gedefinieerd zijn door de superklasse tot fase 2.
hier is een code sample, aangepast van een sample in de taalgids, die laat zien dat je de eigen eigenschappen van een subklasse moet initialiseren voordat je de superclass aangewezen initializer aanroept. U mag geen toegang krijgen tot eigenschappen die door de superclass worden verstrekt tot na het aanroepen van de superclass aangewezen initializer. Tot slot mag u de eigenschappen van constant opgeslagen niet wijzigen nadat de superclass aangewezen initializer is aangeroepen.
overschrijven van een Initializer
nu we ervan overtuigd zijn dat subklassen over het algemeen geen initializers erven, en we duidelijk zijn over de Betekenis van en het onderscheid tussen aangewezen en gemak initializers, laten we eens kijken wat er gebeurt als je wilt dat een subklasse een initializer overschrijft uit zijn directe superklasse. Er zijn vier scenario ‘ s die ik zou willen behandelen, gezien het feit dat er twee soorten initializer. Dus laten we ze een voor een nemen, met eenvoudige code voorbeelden voor elk geval:
een aangewezen initializer die overeenkomt met een superclass aangewezen initializer
dit is een typisch scenario. Wanneer u dit doet, moet u de override modifier toepassen. Merk op dat dit scenario van kracht is zelfs wanneer je een automatisch gegeven standaard initializer “overstijgt” (dat wil zeggen wanneer de superklasse geen expliciete initializer definieert. In dit geval biedt Swift er impliciet een. Java-ontwikkelaars moeten bekend zijn met dit gedrag). Deze automatisch geleverde standaard initializer is altijd een aangewezen initializer.
een aangewezen initializer die overeenkomt met een superclass convenience initializer
laten we nu aannemen dat u een aangewezen initializer aan uw subklasse wilt toevoegen die overeenkomt met een gemak initializer in de ouder. Volgens de regels van initializer delegeren zoals uiteengezet in de taalgids, moet je subklasse aangewezen initializer delegeren aan een aangewezen initializer van de onmiddellijke superklasse. Dat wil zeggen, U mag niet delegeren aan de overeenkomende gemak initializer van de ouder. In het belang van het argument, veronderstel ook dat subklasse niet in aanmerking komt om superclass initializers te erven. Dan, omdat je nooit een instantie van je subklasse kon maken door direct de superclass convenience initializer aan te roepen, is die overeenkomende convenience initializer toch niet betrokken bij het initialisatieproces, en zou dat ook nooit kunnen zijn. Daarom, je bent niet echt overschrijven het, en de override modifier is niet van toepassing.
een convenience initializer die overeenkomt met een superklasse aangewezen initializer
in dit scenario, stel je voor dat je een subklasse hebt die zijn eigen eigenschappen toevoegt waarvan de standaardwaarde berekend kan worden (maar niet hoeft te worden) uit de waarden die zijn toegewezen aan een of meer eigenschappen van de ouderklasse. Stel dat je ook maar één aangewezen initializer voor je subklasse wilt hebben. U kunt een convenience initializer toevoegen aan uw subklasse waarvan de handtekening overeenkomt met die van een aangewezen initializer van de bovenliggende klasse. In dit geval zou je nieuwe initializer zowel het gemak als de override modifiers nodig hebben. Hier is een voorbeeld van een geldige code om dit voorbeeld te illustreren:
een convenience initializer die overeenkomt met een superclass convenience initializer
Als u een convenience initializer aan uw subklasse wilt toevoegen die overeenkomt met de handtekening van een convenience initializer van uw superclass, ga dan gewoon uw gang. Zoals ik hierboven uitgelegd, je kunt niet echt overschrijven gemak initializers toch. Dus je zou de convenience modifier opnemen, maar de override modifier weglaten, en behandelen net als elke andere convenience initializer.
een belangrijke afhaalmogelijkheid uit deze sectie is dat de override-modifier alleen wordt gebruikt, en moet worden gebruikt, als u een superclass-initializer overschrijft. (Kleine verduidelijking om hier te maken: als je een vereiste initializer overschrijft, dan zou je de vereiste modifier gebruiken in plaats van de override modifier. De vereiste modifier impliceert de override modifier. Zie de vereiste Initializers sectie hieronder).
wanneer Initializers worden overgeërfd
nu voor de bovengenoemde scenario ‘ s waar superclass initializers worden overgeërfd. Zoals de Swift Language Guide uitlegt, als je subklasse standaardwaarden biedt voor al zijn eigen eigenschappen bij declaratie, en geen van zijn eigen aangewezen initializers definieert, dan zal het automatisch al zijn superclass aangewezen initializers erven. Of, als je subklasse een implementatie biedt van alle superclass aangewezen initializers, dan erft het automatisch alle superclass gemak initializers. Dit is in overeenstemming met de regel in Swift dat initialisatie van klassen (en structuren) opgeslagen eigenschappen niet in een onbepaalde staat mag laten.
ik stuitte op een aantal “interessante” gedrag tijdens het experimenteren met gemak initializers, aangewezen initializers, en de overerving regels. Ik ontdekte dat het mogelijk is om onbedoeld een vicieuze cirkel op te zetten. Neem het volgende voorbeeld:
de klasse Receptingredient heeft voorrang op alle initializers van de voedselklasse en erft daarom automatisch alle superclass convenience initializers. Maar het voedsel gemak initializer redelijk gedelegeerd aan zijn eigen aangewezen initializer, die is overschreven door de Receptingredient subklasse. Het is dus niet de Voedselversie van die init(naam: String) initializer die wordt aangeroepen, maar de overgeschreven versie in RecipeIngredient. De overschreven versie maakt gebruik van het feit dat de subklasse de convenience initializer van voedsel heeft geërfd, en daar is het – je hebt een cyclus. Ik weet niet of dit zou worden beschouwd als een programmer-fout of een compiler bug (ik meldde het als een bug: https://bugs.swift.org/browse/SR-512 ). Stel je voor dat eten een klas is van een derde partij, en je hebt geen toegang tot de broncode, dus je weet niet hoe het daadwerkelijk wordt geïmplementeerd. Je zou niet weten (tot runtime) dat het gebruik van het op de manier zoals weergegeven in dit voorbeeld zou je gevangen in een cyclus. Dus ik denk dat het beter is als de compiler ons Hier helpt.
Failable Initializers
stel je voor dat je een klasse hebt ontworpen die bepaalde invarianten heeft, en je zou die invarianten willen afdwingen vanaf het moment dat een instantie van de klasse wordt gemaakt. Bijvoorbeeld, misschien bent u het modelleren van een factuur en u wilt ervoor zorgen dat het bedrag is altijd niet-negatief. Als je een initializer hebt toegevoegd die een getalargument van het type Double heeft, hoe kun je er dan voor zorgen dat je invariant niet wordt geschonden? Een strategie is om simpelweg te controleren of het argument niet-negatief is. Als dat zo is, gebruik het dan. Anders, standaard op 0. Bijvoorbeeld:
dit zou werken, en kan aanvaardbaar zijn als je documenteert wat je initializer doet (vooral als je van plan bent om je Klasse beschikbaar te maken voor andere ontwikkelaars). Maar je zou het moeilijk kunnen hebben om die strategie te verdedigen, omdat het de kwestie onder het tapijt veegt. Een andere aanpak die wordt ondersteund door Swift zou zijn om initialisatie te laten mislukken. Dat wil zeggen, je zou je initializer falen.
zoals dit artikel beschrijft, werden failable initializers toegevoegd aan Swift als een manier om de behoefte aan fabrieksmethoden te elimineren of te verminderen, “die voorheen de enige manier waren om fouten te melden” tijdens de constructie van objecten. Om een initializer failable te maken, voeg je gewoon de ? of ! karakter na het init trefwoord (i. e., init? of init! ). Dan, nadat alle eigenschappen zijn ingesteld en alle andere regels met betrekking tot delegatie zijn voldaan, zou je wat logica toevoegen om te controleren of de argumenten geldig zijn. Als ze niet geldig zijn, trigger je een initialisatiefout met return nil. Merk op dat dit niet impliceert dat de initializer ooit iets retourneert. Hier is hoe onze factuur klasse eruit zou kunnen zien met een failable initializer:
merk je iets anders op over hoe we het resultaat van de objectcreatie gebruiken? Het is alsof we het als een optie behandelen, toch? Nou, dat is precies wat we doen! Wanneer we een failable initializer gebruiken, krijgen we ofwel nul (als een initialisatiefout is veroorzaakt) of een optionele(Factuur). Dat wil zeggen, als initialisatie succesvol was zullen we eindigen met een optionele die wraps de factuur instantie we geïnteresseerd zijn in, dus we moeten uitpakken. (Even terzijde, merk op dat Java ook Optionals heeft vanaf Java 8).
Failable initializers zijn net als de andere soorten initializers die we hebben besproken met betrekking tot overschrijven en delegeren, aangewezen vs gemak, enz… In feite kunt u zelfs een failable initializer overschrijven met een niet-beschikbare initializer. U kunt echter een niet-beschikbaare initializer niet overschrijven met een niet-bereikbare initializer.
u hebt mogelijk failable initializers opgemerkt van het omgaan met UIView of UIViewController, die beide een failable initializer init bieden?(coder aDecoder: NSCoder). Deze initializer wordt aangeroepen als je View of ViewController wordt geladen vanaf een punt. Het is belangrijk om te begrijpen hoe failable initializers werken. Ik raad je ten zeerste aan om de Failable Initializers sectie van de Swift language guide door te lezen voor een grondige uitleg.
Required Initializers
de vereiste modifier wordt gebruikt om aan te geven dat alle subklassen de betreffende initializer moeten implementeren. Op het eerste gezicht klinkt dat vrij eenvoudig en rechttoe rechtaan. Het kan soms een beetje verwarrend worden als je niet begrijpt hoe de regels met betrekking tot initializer overerving hierboven besproken in het spel komen. Als een subklasse voldoet aan de criteria waarmee superklasse initializers worden geërfd, dan bevat de set van overgenomen initializers die gemarkeerd vereist zijn. Daarom voldoet de subklasse impliciet aan het contract dat door de vereiste modifier wordt opgelegd. Het implementeert de vereiste initializer (s), je ziet het alleen niet in de broncode.
als een subklasse een expliciete (d.w.z. niet overgenomen) implementatie van een vereiste initializer biedt, dan overschrijft het ook de superclass-implementatie. De vereiste modifier impliceert override, dus de override modifier wordt niet gebruikt. U kunt het opnemen als je wilt, maar dit te doen zou overbodig zijn en XCode zal zeuren u over het.
de Swift language guide zegt niet veel over de vereiste modifier, dus heb ik een code voorbeeld voorbereid (zie hieronder) met commentaar om het doel uit te leggen en te beschrijven hoe het werkt. Voor meer informatie, zie dit artikel van Anthony Levings.
speciaal geval: UIView
een van de dingen die me ertoe brachten om diep in Swift initializers te graven was mijn poging om een manier te vinden om een set van aangewezen initializers te maken zonder initialisatielogica te dupliceren. Bijvoorbeeld, Ik werkte door middel van deze UIView tutorial door Ray Wenderlich, het omzetten van zijn Objective-C code in Swift als ik ging (u kunt een kijkje nemen op mijn Swift versie hier). Als je naar die tutorial kijkt, zul je zien dat zijn RateView subklasse van UIView een baseinit methode heeft die beide aangewezen initializers gebruiken om gemeenschappelijke initialisatietaken uit te voeren. Dat lijkt me een goede benadering – je wilt dat spul niet dupliceren in elk van die initializers. Ik wilde die techniek nabootsen in mijn snelle versie van RateView. Maar ik vond het moeilijk omdat een aangewezen initializer niet kan delegeren naar een andere initializer in dezelfde klasse en niet methoden van zijn eigen klasse kan aanroepen totdat nadat het Gedelegeerd is naar de superclass initializer. Op dat moment is het te laat om constante eigenschappen in te stellen. Natuurlijk kun je deze beperking omzeilen door geen constanten te gebruiken, maar dat is geen goede oplossing. Dus ik dacht dat het het beste was om gewoon standaardwaarden op te geven voor de opgeslagen eigenschappen waar ze zijn gedeclareerd. Dat is nog steeds de beste oplossing die ik momenteel ken. Ik heb echter een alternatieve techniek gevonden die initializers gebruikt.
bekijk het volgende voorbeeld. Dit is een fragment van Ratevieww met convenience in haar.swift, dat is een alternatieve versie van mijn Swift haven van RateView. Omdat deze alternatieve versie van RateView een subklasse van UIView is die geen standaardwaarden biedt voor al zijn eigen opgeslagen eigenschappen bij declaratie, moet deze alternatieve versie van RateView op zijn minst een expliciete implementatie bieden van UIView ‘ s required init?(coder aDecoder: NSCoder) initializer. We zullen ook een expliciete implementatie van UIView ‘ s init (frame: Cgrect) initializer om ervoor te zorgen dat het initialisatieproces consistent is. We willen dat onze opgeslagen eigenschappen op dezelfde manier worden ingesteld, ongeacht welke initializer wordt gebruikt.
merk op dat ik de convenience modifier heb toegevoegd aan de overschreven versies van UIView ‘ s initializers. Ik heb ook een failable aangewezen initializer toegevoegd aan de subklasse, die beide van de overgeschreven (gemak) initializers delegeren aan. Deze enkele aangewezen initializer zorgt voor het opzetten van alle opgeslagen eigenschappen (inclusief constanten – ik hoefde niet mijn toevlucht te nemen tot het gebruik van var voor alles). Het werkt, maar ik vind het nogal klungelig. Ik geef de voorkeur aan gewoon standaardwaarden voor mijn opgeslagen eigenschappen waar ze zijn gedeclareerd, maar het is goed om te weten dat deze optie bestaat indien nodig.