Exploring Swift Initializers | Object Partners

i den här artikeln kommer jag att utforska några av de gritty detaljerna i Swift initializers. Varför ta på sig ett så spännande ämne? Främst för att jag ibland har befunnit mig förvirrad av kompileringsfel när jag försöker skapa en underklass av UIView, eller när jag försöker skapa en Swift-version av en gammal Objective-C-klass, till exempel. Så jag bestämde mig för att gräva djupare för att få en bättre förståelse för nyanser av Swift initializers. Eftersom min bakgrund / erfarenhet mest har involverat att arbeta med Groovy, Java och Javascript, jämför jag några av Swifts initialiseringsmekanismer med Java & Groovy. Jag uppmuntrar dig att skjuta upp en lekplats i XCode och leka med kodexemplen själv. Jag uppmuntrar dig också att läsa igenom Initialiseringsavsnittet i Swift 2.1-språkguiden, som är den primära informationskällan för den här artikeln. Slutligen kommer mitt fokus att vara på referenstyper (klasser) snarare än värdetyper (strukturer).

Vad Är En Initialiserare?

en initialiserare är en speciell typ av metod i en Swift struct, class eller enum som ansvarar för att se till att en nyskapad instans av struct, class eller enum är helt initialiserad innan de är redo att användas. De spelar samma roll som en ”konstruktör” i Java och Groovy. Om du är bekant med Objective-C bör du notera att Swift-initialisatorer skiljer sig från Objective-C-initialisatorer genom att de inte returnerar ett värde.

underklasser ärver i allmänhet inte Initialisatorer

en av de första sakerna att tänka på är att ”Swift-underklasser inte ärver sina superklassinitiatorer som standard”, enligt språkguiden. (Guiden förklarar att det finns scenarier där superklass initializers automatiskt ärvs. Vi kommer att täcka dessa exceptionella scenarier senare). Detta överensstämmer med hur Java (och i förlängning, Groovy) fungerar. Tänk på följande:

precis som med Java & Groovy, är det meningsfullt att detta inte är tillåtet (även om det som med de flesta saker kan finnas något argument på denna punkt. Se detta StackOverflow-inlägg). Om det var tillåtet skulle initialiseringen av musikerens” instrument ” – egendom kringgås, vilket potentiellt skulle lämna din Musikerinstans i ett ogiltigt tillstånd. Men med Groovy skulle jag normalt inte bry mig om att skriva initialisatorer (dvs konstruktörer). Snarare skulle jag bara använda kartkonstruktören Groovy ger implicit, vilket gör att du fritt kan välja och välja vilka egenskaper du vill ställa in vid konstruktion. Till exempel är följande helt giltig Groovy-kod:

Observera att du kan inkludera alla fastigheter, inklusive de som tillhandahålls av superclasses, men du behöver inte ange alla (eller några) av dem, och de behöver inte anges i någon särskild ordning. Denna typ av ultraflexibel initialiserare tillhandahålls inte av Swift. Det närmaste I Swift är de automatiskt tillhandahållna memberwise-initialiserarna för strukturer. Men ordningen på argumenten i en memberwise initializer är signifikant, även om de heter, och beror på i vilken ordning de definieras:
hur som helst, tillbaka till klasser – Groovys filosofi om post-construction object validity är tydligt väldigt annorlunda än Swifts. Detta är bara ett av många sätt på vilka Groovy skiljer sig från Swift.

Designated vs Convenience Initializers

innan vi kommer för långt ner i kaninhålet bör vi klargöra ett viktigt koncept: I Swift kategoriseras en initialiserare som antingen en utsedd eller en bekvämlighetsinitierare. Jag ska försöka förklara skillnaden både konceptuellt och syntaktiskt. Varje klass måste ha minst en utsedd initialiserare, men kan ha flera (”betecknad” innebär inte ”singel”). En utsedd initialiserare anses vara en primär initialiserare. De är head-honchos. De är ytterst ansvariga för att se till att alla egenskaper initieras. På grund av det ansvaret kan det ibland bli en smärta att använda en utsedd initialiserare hela tiden, eftersom de kan kräva flera argument. Tänk dig att arbeta med en klass som har flera egenskaper, och du måste skapa flera instanser av den klassen som är nästan identiska förutom en eller två egenskaper. (För argumentets skull, låt oss också anta att det inte finns några förnuftiga standardvärden som kunde ha tilldelats egenskaper när de deklareras). Till exempel, låt oss säga att vår Personklass också hade ätermat och njutermusikegenskaper. Naturligtvis skulle dessa två saker vara sanna för det mesta, men du vet aldrig. Ta en titt:

nu har vår Personklass fyra egenskaper som måste ställas in, och vi har en utsedd initialiserare som kan göra jobbet. Den utsedda initialiseraren är den första, den som tar 4 argument. För det mesta kommer de två sista argumenten att ha värdet ”sant”. Det skulle vara en smärta att behöva fortsätta specificera dem varje gång vi vill skapa en typisk Person. Det är där de två sista initialiserarna kommer in, de som är markerade med bekvämlighetsmodifieraren. Detta mönster ska se bekant ut för en Java-utvecklare. Om du har en konstruktör som tar fler argument än du verkligen behöver hantera hela tiden kan du skriva förenklade konstruktörer som tar en delmängd av dessa argument och ger förnuftiga standardvärden för de andra. Convenience initializers måste delegera antingen till en annan, kanske mindre bekväm, convenience initializer eller till en utsedd initializer. I slutändan måste en utsedd initialiserare engagera sig. Vidare, om detta är en underklass, måste den utsedda initialiseraren anropa en utsedd initialiserare från dess omedelbara superklass.

ett verkligt exempel för användningen av bekvämlighetsmodifieraren kommer från uikits uibezierpath-klass. Jag är säker på att du kan föreställa dig att det finns flera sätt att specificera en väg. Som sådan, uibezierpath ger flera bekvämlighet initializers:

Offentlig bekvämlighet init(rect: CGRect)
Offentlig bekvämlighet init(ovalInRect rect: CGRect)
Offentlig bekvämlighet init(roundedirect rect: cgrect, cornerRadius: cgfloat)
Offentlig bekvämlighet init(roundedirect rect: CGRect, byRoundingCorners hörn: uirectcorner, Cornerradii: cgsize)
allmän bekvämlighet init(arccenter Center: Cgpoint, radie: CGFloat, startAngle: CGFloat, äventyra: CGFloat, medurs: Bool)
public convenience init(CGPath: CGPath)

tidigare sa jag att en klass kan ha flera utsedda initialisatorer. Så hur ser det ut? En viktig skillnad, verkställs av kompilatorn, mellan utsedda initializers och bekvämlighet initializers är att utsedda initializers får inte delegera till en annan initializer i samma klass (men måste delegera till en utsedd initializer i sin omedelbara superklass). Titta på den andra initialiseraren av personen, den som tar ett enda argument som heter ”unDead”. Eftersom denna initialiserare inte är markerad med bekvämlighetsmodifieraren, behandlar Swift den som en utsedd initialiserare. Som sådan kan den inte delegera till en annan initialiserare personligen. Försök kommentera de fyra första raderna och kommentera den sista raden. Kompilatorn kommer att klaga, och XCode bör försöka hjälpa dig genom att föreslå att du fixar det genom att lägga till bekvämlighetsmodifieraren.

nu överväga musiker underklass av Person. Den har en enda initialiserare, och den måste därför vara en utsedd initialiserare. Som sådan måste den ringa en utsedd initialiserare av den omedelbara superklassen, Person. Kom ihåg: medan utsedda initialisatorer inte kan delegera till en annan initialiserare i samma klass, måste bekvämlighetsinitiatorer göra det. En utsedd initialiserare måste också ringa en utsedd initialiserare av sin omedelbara superklass. Se språkguiden för mer detaljer (och vacker grafik).

Initialiseringsfaser

som Swift-språkguiden förklarar finns det två initialiseringsfaser. Faserna avgränsas av samtalet till den superklass som utsetts initializer. Fas 1 är före samtalet till superklass utsedda initializer, fas 2 är efter. En underklass måste initiera alla egna egenskaper i fas 1, och det kan inte ange några egenskaper som definieras av superklassen förrän fas 2.
här är ett kodexempel, anpassat från ett prov som tillhandahålls i språkguiden, som visar att du måste initiera de egna egenskaperna hos en underklass innan du åberopar den superklass som anges initializer. Du får inte komma åt egenskaper som tillhandahålls av superklassen förrän efter att ha åberopat den superklass som utsetts initializer. Slutligen kan du inte ändra konstant lagrade egenskaper efter superklass utsedda initializer har åberopats.

åsidosätter en Initialiserare

nu när vi är övertygade om att underklasser i allmänhet inte ärver initialisatorer, och vi är tydliga på betydelsen av och skillnaden mellan utsedda och bekvämlighetsinitiatorer, låt oss överväga vad som händer när du vill att en underklass ska åsidosätta en initialiserare från den omedelbara superklassen. Det finns fyra scenarier som jag skulle vilja täcka, eftersom det finns två typer av initialiserare. Så låt oss ta dem en efter en, med enkla kodexempel för varje fall:

en utsedd initialiserare som matchar en superklass betecknad initialiserare
Detta är ett typiskt scenario. När du gör detta måste du använda modifieraren för åsidosättning. Observera att detta scenario är i kraft även när du ”åsidosätter” en automatiskt tillhandahållen standard initializer (dvs. när superklassen inte definierar någon explicit initializer. I detta fall ger Swift en implicit. Java-utvecklare bör känna till detta beteende). Den automatiskt tillhandahållna standardinitieraren är alltid en utsedd initialiserare.
en utsedd initializer som matchar en superklass bekvämlighet initializer
låt oss nu anta att du vill lägga till en utsedd initializer till din underklass som råkar matcha en bekvämlighet initializer i den överordnade. Enligt reglerna för initialiseringsdelegering som anges i språkguiden måste din underklass utsedda initialiserare delegera upp till en utsedd initialiserare av den omedelbara superklassen. Det vill säga, du får inte delegera upp till förälderns matchande bekvämlighetsinitierare. För argumentets skull antar du också att underklass inte kvalificerar sig för att ärva superklassinitialisatorer. Eftersom du aldrig kunde skapa en instans av din underklass genom att direkt åberopa superclass convenience initializer, är den matchande convenience initializer inte och kan aldrig vara involverad i initialiseringsprocessen ändå. Därför åsidosätter du inte det, och modifieraren för åsidosättning gäller inte.

en bekvämlighetsinitierare som matchar en superklass betecknad initialiserare
i det här scenariot kan du föreställa dig att du har en underklass som lägger till egna egenskaper vars standardvärde kan (men behöver inte) beräknas från de värden som tilldelats en eller flera överordnade klassegenskaper. Antag att du bara vill ha en utsedd initialiserare för din underklass. Du kan lägga till en bekvämlighetsinitierare i din underklass vars signatur matchar den för en utsedd initialiserare i den överordnade klassen. I det här fallet skulle din nya initializer behöva både bekvämlighet och åsidosätta modifierare. Här är ett giltigt kodexempel för att illustrera detta fall:
en bekvämlighetsinitierare som matchar en superklass bekvämlighetsinitierare
om du vill lägga till en bekvämlighetsinitierare i din underklass som råkar matcha signaturen för en bekvämlighetsinitierare i din superklass, gå bara vidare. Som jag förklarade ovan kan du inte riktigt åsidosätta bekvämlighetsinitiatorer ändå. Så du skulle inkludera bekvämlighetsmodifieraren, men utelämna åsidosättningsmodifieraren och behandla den precis som någon annan bekvämlighetsinitierare.

en nyckel takeaway från detta avsnitt är att åsidosättnings modifieraren endast används, och måste användas, om du åsidosätter en superklass betecknad initializer. (Mindre förtydligande att göra här: om du åsidosätter en nödvändig initialiserare, skulle du använda den önskade modifieraren istället för åsidosättningsmodifieraren. Den erforderliga modifieraren innebär åsidosättnings modifieraren. Se avsnittet nödvändiga Initialisatorer nedan).

när Initialisatorer ärvs

nu för de ovan nämnda scenarierna där superklassinitiatorer ärvs. Som Swift – Språkguiden förklarar, om din underklass ger standardvärden för alla egna egenskaper vid deklarationen och inte definierar någon av sina egna utsedda initialisatorer, kommer den automatiskt att ärva alla sina superklass-utsedda initialisatorer. Eller, om din underklass ger en implementering av alla superklass utsedda initializers, då ärver det automatiskt alla superklass bekvämlighet initializers. Detta överensstämmer med regeln i Swift att initiering av klasser (och strukturer) inte får lämna lagrade egenskaper i ett obestämt tillstånd.

jag snubblat över något ”intressant” beteende medan jag experimenterade med bekvämlighetsinitiatorer, utsedda initialisatorer och arvreglerna. Jag fann att det är möjligt att ställa in en ond cirkel av misstag. Tänk på följande exempel:

RecipeIngredient-klassen åsidosätter alla de livsmedelsklasser som utsetts initializers, och därför ärver den automatiskt alla superclass convenience initializers. Men Food convenience initializer delegerar rimligen till sin egen utsedda initialiserare, som har åsidosatts av RecipeIngredient-underklassen. Så det är inte Matversionen av den initialiseraren init(namn: sträng) som åberopas, utan den åsidosatta versionen i RecipeIngredient. Den åsidosatta versionen utnyttjar det faktum att underklassen har ärvt matens bekvämlighetsinitierare, och där är det – du har en cykel. Jag vet inte om detta skulle betraktas som ett programmeringsfel eller ett kompilatorfel (jag rapporterade det som ett fel: https://bugs.swift.org/browse/SR-512 ). Föreställ dig att mat är en klass från en 3: e part, och du har inte tillgång till källkoden så att du inte vet hur den faktiskt implementeras. Du skulle inte veta (tills runtime) att använda det på det sätt som visas i det här exemplet skulle få dig att fånga dig i en cykel. Så jag tror att det skulle vara bättre om kompilatorn hjälpte oss här ute.

Failable Initializers

Föreställ dig att du har utformat en klass som har vissa invarianter, och du vill genomdriva dessa invarianter från det ögonblick som en instans av klassen skapas. Till exempel kanske du modellerar en faktura och du vill se till att beloppet alltid är icke-negativt. Om du har lagt till en initialiserare som tar ett belopp argument av typen Dubbel, hur kan du se till att du inte bryter mot din invariant? En strategi är att helt enkelt kontrollera om argumentet är icke-negativt. Om det är, använd det. Annars standard till 0. Till exempel:

detta skulle fungera och kan vara acceptabelt om du dokumenterar vad din initializer gör (speciellt om du planerar att göra din klass tillgänglig för andra utvecklare). Men du kan ha svårt att försvara den strategin, eftersom den typ av sopa frågan under mattan. Ett annat tillvägagångssätt som stöds av Swift skulle vara att låta initialisering misslyckas. Det vill säga, du skulle göra din initializer misslyckad.

som den här artikeln beskriver, lades failable initializers till Swift som ett sätt att eliminera eller minska behovet av fabriksmetoder, ”som tidigare var det enda sättet att rapportera fel” under objektkonstruktion. För att göra en initializer misslyckas, lägger du helt enkelt till ? eller ! tecken efter init-nyckelordet (dvs. init? eller init! ). Sedan, efter att alla egenskaper har ställts in och alla andra regler för delegering har uppfyllts, lägger du till lite logik för att verifiera att argumenten är giltiga. Om de inte är giltiga utlöser du ett initialiseringsfel med return nil. Observera att detta inte innebär att initialiseraren någonsin returnerar någonting. Så här kan vår Fakturaklass se ut med en misslyckad initialiserare:
Lägg märke till något annat om hur vi använder resultatet av objektskapandet? Det är som om vi behandlar det som en valfri, eller hur? Tja, det är precis vad vi gör! När vi använder en misslyckad initialiserare får vi antingen noll (om ett initialiseringsfel utlöstes) eller en valfri(Faktura). Det vill säga om initialiseringen lyckades kommer vi att sluta med ett valfritt som sveper Fakturainstansen vi är intresserade av, så vi måste packa upp den. (Som en sida, notera att Java också har alternativ från Java 8).

Failable initializers är precis som de andra typerna av initializers vi har diskuterat med avseende på åsidosättande och delegering, betecknad vs bekvämlighet, etc… i själva verket kan du till och med åsidosätta en failable initializer med en nonfailable initializer. Du kan dock inte åsidosätta en icke-failable initializer med en failable en.

du kanske har märkt failable initializers från att hantera UIView eller UIViewController, som båda ger en failable initializer init?(kodare aDecoder: NSCoder). Den här initialiseraren anropas när din vy eller ViewController laddas från en spets. Det är viktigt att förstå hur misslyckade initialisatorer fungerar. Jag rekommenderar starkt att du läser igenom avsnittet Failable Initializers i Swift language guide för en grundlig förklaring.

nödvändiga Initialisatorer

den nödvändiga modifieraren används för att indikera att alla underklasser måste implementera den berörda initialiseraren. På framsidan av det låter det ganska enkelt och enkelt. Det kan ibland bli lite förvirrande om du inte förstår hur reglerna om initialiseringsarv som diskuterats ovan spelar in. Om en underklass uppfyller kriterierna för att superklassinitialisatorer ärvs, innehåller uppsättningen ärvda initialisatorer de markerade som krävs. Därför uppfyller underklassen implicit det kontrakt som åläggs av den erforderliga modifieraren. Det implementerar de nödvändiga initialiserarna, du ser det bara inte i källkoden.

om en underklass ger en explicit (dvs. inte ärvt) implementering av en nödvändig initialiserare, åsidosätter den också superklassimplementeringen. Den nödvändiga modifieraren innebär åsidosättning, så åsidosättningsmodifieraren används inte. Du kan inkludera det om du vill, men det skulle vara överflödigt och XCode kommer att tjata dig om det.

Swift – språkguiden säger inte mycket om den nödvändiga modifieraren, så jag förberedde ett kodexempel (se nedan) med kommentarer för att förklara dess syfte och beskriva hur det fungerar. För mer information, se den här artikeln av Anthony Levings.

specialfall: utvidga UIView

en av de saker som fick mig att gräva djupt i Swift initializers var mitt försök att räkna ut ett sätt att skapa en uppsättning utsedda initializers utan att duplicera initialiseringslogik. Till exempel arbetade jag genom denna UIView-handledning av Ray Wenderlich och konverterade sin Objective-C-kod till Swift när jag gick (du kan ta en titt på min Swift-version här). Om du tittar på den handledningen ser du att hans RateView-underklass av UIView har en baseInit-metod som båda de utsedda initialiserarna använder för att utföra vanliga initialiseringsuppgifter. Det verkar som ett bra tillvägagångssätt för mig-du vill inte duplicera de sakerna i var och en av dessa initialisatorer. Jag ville återskapa den tekniken i min Swift-version av RateView. Men jag fann det svårt eftersom en utsedd initialiserare inte kan delegera till en annan initialiserare i samma klass och inte kan ringa metoder för sin egen klass förrän den delegerar till superklassens initialiserare. Vid den tiden är det för sent att ställa in konstanta egenskaper. Naturligtvis kan du komma runt denna begränsning genom att inte använda konstanter, men det är inte en bra lösning. Så jag tänkte att det var bäst att bara ange standardvärden för de lagrade egenskaperna där de deklareras. Det är fortfarande den bästa lösningen som jag för närvarande känner till. Men jag räknade ut en alternativ teknik som använder initialisatorer.

ta en titt på följande exempel. Detta är ett utdrag från RateViewWithConvenienceInits.swift, som är en alternativ version av min Swift port of RateView. Att vara en underklass av UIView som inte ger standardvärden för alla egna lagrade egenskaper vid deklarationen, måste den här alternativa versionen av RateView åtminstone ge en uttrycklig implementering av uiviews obligatoriska init?(kodare aDecoder: NSCoder) initialiserare. Vi vill också ge en uttrycklig implementering av Uiviews init (frame: Cgrect) initializer för att se till att initieringsprocessen är konsekvent. Vi vill att våra lagrade egenskaper ska konfigureras på samma sätt oavsett vilken initialiserare som används.
Lägg märke till att jag lade till bekvämlighetsmodifieraren till de åsidosatta versionerna av uiviews initialisatorer. Jag lade också till en misslyckad utsedd initialiserare till underklassen, som båda de åsidosatta (bekvämlighet) initialiserarna delegerar till. Denna enda utsedda initialiserare tar hand om att ställa in alla lagrade egenskaper (inklusive konstanter – jag behövde inte använda var för allt). Det fungerar, men jag tycker att det är ganska kludgy. Jag föredrar att bara ange standardvärden för mina lagrade egenskaper där de deklareras, men det är bra att veta att det här alternativet finns om det behövs.

You might also like

Lämna ett svar

Din e-postadress kommer inte publiceras.