Utforske Swift Initializers /Object Partners

I denne artikkelen vil jeg utforske noen av De gritty detaljene I Swift initializers. Hvorfor ta på seg et så spennende tema? Hovedsakelig fordi jeg noen ganger har funnet meg forvirret av kompileringsfeil når jeg prøver å lage En underklasse Av UIView, eller når jeg prøver Å lage En Swift-versjon av en gammel Objective-C-klasse, for eksempel. Så jeg bestemte meg for å grave dypere for å få en bedre forståelse av nyansene Til Swift initializers. Siden min bakgrunn / erfaring har for det meste involvert å jobbe Med Groovy, Java Og Javascript, vil jeg sammenligne Noen Av Swifts initialiseringsmekanismer Til Java & Groovy. Jeg oppfordrer deg til å fyre Opp En Lekeplass I XCode og leke med kodeeksemplene selv. Jeg oppfordrer deg også til å lese Gjennom Initialiseringsdelen Av Språkveiledningen For Swift 2.1, som er den primære informasjonskilden for denne artikkelen. Til slutt vil fokuset mitt være på referansetyper (klasser) i stedet for verdityper (structs).

Hva Er En Initialiserer?

en initialiserer er en spesiell type metode I En Swift struct, klasse eller enum som er ansvarlig for å sørge for at en nyopprettet forekomst av struct, klasse eller enum er fullt initialisert før de er klare til bruk. De spiller samme rolle som en «konstruktør» I Java og Groovy. Hvis Du er kjent Med Objective-C, bør Du være oppmerksom På At Swift initializers skiller Seg fra Objective-C initializers ved at De ikke returnerer en verdi.

Underklasser Arver Vanligvis Ikke Initialisatorer

En av de første tingene å huske på er at «Swift-underklasser ikke arver sine superklasseinitialisatorer som standard», ifølge språkveiledningen. (Guiden forklarer at det er scenarier der superklasseinitialisatorer automatisk arves. Vi vil dekke de eksepsjonelle scenariene senere). Dette stemmer overens med Hvordan Java (Og I tillegg Groovy) fungerer. Vurder følgende:

Akkurat som Med Java & Groovy, er det fornuftig at dette ikke er tillatt (selv om det som med de fleste ting kan være noe argument på dette punktet. Se dette stackoverflow-innlegget). Hvis det var tillatt, ville Initialiseringen av «instrument» – egenskapen Til Musiker bli omgått, og potensielt forlate Musikereksemplet i ugyldig tilstand. Men med Groovy ville jeg normalt ikke bry meg med å skrive initialisatorer (dvs.konstruktører). Snarere vil jeg bare bruke map constructor Groovy gir implisitt, som lar deg fritt velge og velge hvilke egenskaper du vil sette på konstruksjon. For eksempel er følgende helt gyldig Groovy kode:

Legg Merke til at du kan inkludere alle eiendommer, inkludert de som leveres av superklasser, men du trenger ikke å spesifisere alle (eller noen) av dem, og de trenger ikke å spesifiseres i en bestemt rekkefølge. Denne typen ultra-fleksibel initializer leveres ikke Av Swift. Det nærmeste I Swift er de automatisk leverte memberwise initializers for structs. Men rekkefølgen på argumentene i en memberswise initializer er signifikant, selv om de er navngitt, og avhenger av rekkefølgen de er definert i:
uansett, tilbake til klasser-Groovys filosofi om gyldighet etter konstruksjonsobjekt er tydelig veldig forskjellig fra Swift. Dette er bare en av mange måter Som Groovy skiller Seg fra Swift.

Designated vs Convenience Initializers

før vi kommer for langt ned i kaninhullet, bør vi klargjøre et viktig konsept: I Swift er en initialiserer kategorisert som enten en utpekt eller en praktisk initialiserer. Jeg vil prøve å forklare forskjellen både konseptuelt og syntaktisk. Hver klasse må ha minst en utpekt initialiserer, men kan ha flere («utpekt» betyr ikke «singel»). En utpekt initialiserer regnes som en primær initialiserer. De er head-honchos. De er i siste instans ansvarlige for å sørge for at alle eiendommer er initialisert. På grunn av dette ansvaret kan det noen ganger bli vondt å bruke en utpekt initialiserer hele tiden, siden de kan kreve flere argumenter. Tenk deg å jobbe med en klasse som har flere egenskaper, og du må opprette flere forekomster av den klassen som er nesten identiske med unntak av en eller to egenskaper. (For argumentets skyld, la oss også anta at det ikke er noen fornuftige standardverdier som kunne ha blitt tildelt egenskaper når de er erklært). For eksempel, la oss si at Vår Personklasse også hadde spisermat og nytermusikkegenskaper. Selvfølgelig, disse to tingene ville bli satt til sant mesteparten av tiden, men du vet aldri. Ta en titt:

Nå Har Vår Personklasse fire egenskaper som må settes, og vi har en utpekt initializer som kan gjøre jobben. Den utpekte initialisatoren er den første, den som tar 4 argumenter. Mesteparten av tiden vil de to siste argumentene ha verdien «sann». Det ville være en smerte å måtte fortsette å spesifisere dem hver gang vi ønsker å lage en typisk Person. Det er der de to siste initialisatorene kommer inn, de som er merket med bekvemmelighetsmodifikatoren. Dette mønsteret bør se kjent For En Java-utvikler. Hvis du har en konstruktør som tar flere argumenter enn du virkelig trenger å håndtere hele tiden, kan du skrive forenklede konstruktører som tar en delmengde av disse argumentene og gir fornuftige standardverdier for de andre. Convenience initializers må delegere enten til en annen, kanskje mindre praktisk, convenience initializer eller til en utpekt initializer. Til slutt må en utpekt initialiserer bli involvert. Videre, hvis dette er en underklasse, må den utpekte initialisatoren kalle en utpekt initialiserer fra den umiddelbare superklassen.

Et eksempel på bruk av bekvemmelighetsmodifikatoren kommer fra uikits uibezierpath-klasse. Jeg er sikker på at du kan forestille deg at det er flere måter å spesifisere en bane på. Som sådan gir UIBezierPath flere praktiske initialisatorer:

offentlig bekvemmelighet init(rect: CGRect)
offentlig bekvemmelighet init(ovalInRect rect: CGRect)
offentlig bekvemmelighet init(roundedRect rect: Cgrect, cornerRadius: CGFloat)
offentlig bekvemmelighet init(roundedRect rect: CGRect, byRoundingCorners hjørner: UIRectCorner, cornerradii: Cgsize)
offentlig bekvemmelighet init(arccenter center: Cgfloat, endAngle: CGFloat, med klokken: Bool)
offentlig bekvemmelighet init(CGPath: CGPath)

Tidligere sa jeg at en klasse kan ha flere utpekte initialisatorer. Så hvordan ser det ut? En viktig forskjell, håndhevet av kompilatoren, mellom utpekte initialisatorer og bekvemmelighetsinitialisatorer er at utpekte initialisatorer ikke kan delegere til en annen initialiserer i samme klasse (men må delegere til en utpekt initialiserer i sin umiddelbare superklasse). Se på Den Andre initialiseringen Av Personen, den som tar et enkelt argument som heter «unDead». Siden denne initialisatoren ikke er merket med bekvemmelighetsmodifikatoren, behandler Swift Den som en utpekt initialiserer. Som sådan kan det ikke delegere Til En annen initialiserer Personlig. Prøv å kommentere de første fire linjene, og uncommenting den siste linjen. Kompilatoren vil klage, Og XCode bør prøve å hjelpe deg ved å foreslå at du fikser det ved å legge til bekvemmelighetsmodifikatoren.

vurder Nå Musikerens underklasse Av Person. Den har en enkelt initialiserer, og det må derfor være en utpekt initialiserer. Som sådan må den kalle en utpekt initialiserer av den umiddelbare superklassen, Personen. Husk: selv om utpekte initialisatorer ikke kan delegere til en annen initialiserer av samme klasse, må bekvemmelighetsinitialisatorer gjøre det. Også, en utpekt initializer må kalle en utpekt initializer av det umiddelbare superklasse. Se språkveiledningen for flere detaljer (og pen grafikk).

Initialiseringsfaser

Som Swift-språkveiledningen forklarer, er det to initialiseringsfaser. Fasene er avgrenset av kallet til superklasse utpekt initializer. Fase 1 er før anropet til superklassen utpekt initializer, fase 2 er etter. En underklasse må initialisere ALLE sine egne egenskaper i fase 1, og det kan ikke angi noen egenskaper definert av superklassen til fase 2.
her er en kodeeksempel, tilpasset fra et eksempel som er gitt i språkveiledningen, som viser at DU må initialisere EGNE egenskaper for en underklasse før du starter den superklasse-utpekte initialisatoren. Du kan ikke få tilgang til egenskaper levert av superklassen før etter å ha påkalt superklassens utpekte initializer. Til slutt kan du ikke endre konstant lagrede egenskaper etter at superclass utpekt initializer har blitt påkalt.

Overstyre En Initialisator

Nå som vi er overbevist om at underklasser generelt ikke arver initialisatorer, og vi er klare på betydningen av og skille mellom utpekte og praktiske initialisatorer, la oss vurdere hva som skjer når du vil at en underklasse skal overstyre en initialisator fra den umiddelbare superklassen. Det er fire scenarier som jeg vil dekke, gitt at det er to typer initializer. Så la oss ta dem en etter en, med enkle kodeeksempler for hvert tilfelle:

en utpekt initializer som samsvarer med en superklasse utpekt initializer
Dette er et typisk scenario. Når du gjør dette, må du bruke overstyringsmodifikatoren. Vær oppmerksom på at dette scenariet er i kraft selv når du «overstyrer» en automatisk gitt standard initialiserer (dvs.når superklassen ikke definerer noen eksplisitt initialiserer. I Dette tilfellet Gir Swift en implisitt. Java-utviklere bør være kjent med denne oppførselen). Dette automatisk gitt standard initializer er alltid en utpekt initializer.
en utpekt initializer som samsvarer med en superclass convenience initializer
la Oss nå anta at du vil legge til en utpekt initializer til underklassen din som skjer for å matche en praktisk initializer i foreldrene. Ved reglene for initialiseringsdelegering som er lagt ut i språkveiledningen, må din underklasse utpekt initialisator delegere opp til en utpekt initialisator av den umiddelbare superklassen. Det vil si at du ikke kan delegere opp til foreldrenes matchende bekvemmelighet initializer. For argumentets skyld, anta også at underklassen ikke kvalifiserer til å arve superklasseinitialisatorer. Da, siden du aldri kunne opprette en forekomst av underklassen din ved å direkte påkalle superclass convenience initializer, er den matchende convenience initializer ikke, og kan aldri være involvert i initialiseringsprosessen uansett. Derfor overstyrer du det egentlig ikke, og overstyringsmodifikatoren gjelder ikke.

en praktisk initializer som samsvarer med en superklasse utpekt initializer
i dette scenariet kan du forestille deg at du har en underklasse som legger til egne egenskaper hvis standardverdi kan (men ikke må) beregnes fra verdiene som er tilordnet en eller flere egenskaper i overordnet klasse. Anta at du også bare vil ha en utpekt initialiserer for underklassen din. Du kan legge til en praktisk initialiserer til underklassen din hvis signatur samsvarer med en utpekt initialiserer av foreldreklassen. I dette tilfellet vil din nye initialiserer trenge både bekvemmeligheten og overstyre modifikatorer. Her er en gyldig kodeeksempel for å illustrere denne saken:
en bekvemmelighetsinitialisator som samsvarer med en superklasseinitialisator
hvis du vil legge til en bekvemmelighetsinitialisator til underklassen din som skjer for å matche signaturen til en bekvemmelighetsinitialisator av superklassen din, bare gå rett fram. Som jeg forklarte ovenfor, kan du egentlig ikke overstyre bekvemmelighetsinitialisatorer uansett. Så du vil inkludere bekvemmelighetsmodifikatoren, men utelat overstyringsmodifikatoren, og behandle den akkurat som alle andre bekvemmelighetsinitialisatorer.

en viktig takeaway fra denne delen er at overstyringsmodifikatoren bare brukes, og må brukes, hvis du overstyrer en superklasse utpekt initializer. (Mindre avklaring å gjøre her: hvis du overstyrer en nødvendig initialiserer, vil du bruke den nødvendige modifikatoren i stedet for overstyringsmodifikatoren. Den nødvendige modifikatoren innebærer overstyringsmodifikatoren. Se Delen Nødvendige Initialisatorer nedenfor).

Når Initialisatorer Er Arvet

nå For de nevnte scenariene der superclass initialisatorer er arvet. Som Swift – Språkveiledningen forklarer, hvis underklassen din gir standardverdier for alle sine egne egenskaper ved deklarasjon, og ikke definerer noen av sine egne utpekte initialisatorer, vil den automatisk arve alle sine superklasse-utpekte initialisatorer. Eller, hvis underklassen din gir en implementering av alle superclass-utpekte initialisatorer, arver den automatisk alle superclass convenience initializers. Dette er i samsvar med Regelen I Swift at initialisering av klasser (og strukturer) ikke må forlate lagrede egenskaper i ubestemt tilstand.

jeg snublet over noe «interessant» oppførsel mens jeg eksperimenterte med praktiske initialisatorer, utpekte initialisatorer og arvereglene. Jeg fant ut at det er mulig å sette opp en ond sirkel utilsiktet. Tenk på følgende eksempel:

RecipeIngredient-klassen overstyrer alle Matklassen som er utpekt initialisatorer, og derfor arver den automatisk alle superclass convenience initializers. Men Food convenience initializer delegerer rimelig til sin egen utpekte initializer, som har blitt overstyrt av RecipeIngredient-underklassen. Så Det er Ikke Matversjonen av den init(name: String) initializer som påberopes, men den overstyrte versjonen I RecipeIngredient. Den overstyrte versjonen utnytter det faktum at underklassen har arvet Food ‘ s convenience initializer, og der er det-du har en syklus. Jeg vet ikke om dette ville bli betraktet som en programmerer-feil eller en kompilatorfeil (jeg rapporterte det som en feil: https://bugs.swift.org/browse/SR-512 ). Tenk Deg At Mat er en klasse fra en 3. part, og du har ikke tilgang til kildekoden, slik at du ikke vet hvordan den faktisk implementeres. Du ville ikke vite (før kjøretid) at bruk av den på den måten som vises i dette eksemplet, vil få deg fanget i en syklus. Så jeg tror det ville være bedre hvis kompilatoren hjalp oss her ute.

Failable Initializers

Tenk deg at du har designet en klasse som har visse invariants, og du vil håndheve disse invariants fra det øyeblikket en forekomst av klassen er opprettet. For eksempel, kanskje du modellerer En Faktura, og du vil sørge for at beløpet alltid er ikke-negativt. Hvis du har lagt til en initialiserer som tar et beløp argument Av typen Dobbel, hvordan kan du sørge for at du ikke bryter din invariant? En strategi er å bare sjekke om argumentet er ikke-negativt. Hvis det er, bruk det. Ellers standard til 0. For eksempel:

Dette ville fungere, og kan være akseptabelt hvis du dokumenterer hva initializer gjør (spesielt hvis du planlegger å gjøre klassen tilgjengelig for andre utviklere). Men du kan ha en hard tid å forsvare den strategien, siden det gjør slags feie problemet under teppet. En annen tilnærming som Støttes Av Swift ville være å la initialisering mislykkes. Det vil si at du vil gjøre initialiseringen din feilbarlig.

Som denne artikkelen beskriver, ble failable initializers lagt Til Swift som en måte å eliminere eller redusere behovet for fabrikkmetoder, «som tidligere var den eneste måten å rapportere feil» under objektkonstruksjon. For å gjøre en initializer failable, legger du bare til ? eller ! tegn etter init-søkeordet (dvs. init? eller init! ). Etter at alle egenskapene er angitt og alle de andre reglene om delegering er oppfylt, vil du legge til litt logikk for å bekrefte at argumentene er gyldige. Hvis de ikke er gyldige, utløser du en initialiseringsfeil med return nil. Merk at dette ikke innebærer at initialisatoren noen gang returnerer noe. Slik Ser Fakturaklassen vår ut med en feilbar initialiserer:
Legg Merke til noe annet om hvordan vi bruker resultatet av objektopprettelsen? Det er som om vi behandler det som et valgfritt, ikke sant? Vel, det er akkurat det vi gjør! Når vi bruker en failable initializer, får vi enten null (hvis en initialiseringsfeil ble utløst) eller En Valgfri(Faktura). Det vil si at hvis initialiseringen var vellykket, vil vi ende Opp med En Valgfri Som bryter Fakturainstansen vi er interessert i, så vi må pakke den ut. (Som en side, merk At Java også har Opsjoner Fra Java 8).

Failable initializers er akkurat som de andre typer initializers vi har diskutert med hensyn til overstyring og delegering, utpekt vs convenience, etc… Faktisk kan du til og med overstyre en failable initializer med en nonfailable initializer. Du kan imidlertid ikke overstyre en ikke-tilgjengelig initializer med en feilbar.

du har kanskje lagt merke til failable initializers fra å håndtere UIView eller UIViewController, som begge gir en failable initializer init?(koder aDecoder: NSCoder). Dette initializer kalles når Visningen Eller ViewController er lastet fra en spiss. Det er viktig å forstå hvordan feilbare initialisatorer fungerer. Jeg anbefaler sterkt at du leser gjennom Failable Initializers-delen Av Swift language guide for en grundig forklaring.

Nødvendige Initialisatorer

den nødvendige modifikatoren brukes til å indikere at alle underklasser må implementere den berørte initialisatoren. På forsiden av det høres det ganske enkelt og greit ut. Det kan bli litt forvirrende til tider hvis du ikke forstår hvordan reglene om initializer arv diskutert ovenfor kommer inn i bildet. Hvis en underklasse oppfyller kriteriene ved hvilke superklasseinitialisatorer arves, inkluderer settet av arvede initialisatorer de som er merket påkrevd. Derfor oppfyller underklassen implisitt kontrakten pålagt av den nødvendige modifikatoren. Det implementerer den nødvendige initialiseringen( e), du ser det bare ikke i kildekoden.

Hvis en underklasse gir en eksplisitt (dvs. ikke arvet) implementering av en nødvendig initialiserer, overstyrer den også superclass-implementeringen. Den nødvendige modifikatoren innebærer overstyring, slik at overstyringsmodifikatoren ikke brukes. Du kan inkludere det hvis du vil, men det ville være overflødig og XCode vil mase deg om det.

Swift-språkveiledningen sier ikke mye om den nødvendige modifikatoren, så jeg utarbeidet en kodeeksempel (se nedenfor) med kommentarer for å forklare formålet og beskrive hvordan det fungerer. For mer informasjon, se Denne artikkelen Av Anthony Levings.

Spesielt Tilfelle: Utvide UIView

En av tingene som ba meg grave dypt inn I Swift initializers var mitt forsøk på å finne ut en måte å lage et sett med utpekte initializers uten å duplisere initialiseringslogikk. For eksempel jobbet jeg gjennom Denne uiview-opplæringen Av Ray Wenderlich, og konverterte Sin Objective-C-kode til Swift mens jeg gikk (du kan se På Min Swift-versjon her). Hvis du ser på den opplæringen, ser du at Hans RateView-underklasse Av UIView har en baseInit-metode som begge de utpekte initialisatorene bruker til å utføre vanlige initialiseringsoppgaver. Det virker som en god tilnærming til meg-du vil ikke duplisere det i hver av disse initialisatorene. Jeg ønsket å gjenskape den teknikken i Min Swift-versjon Av RateView. Men jeg fant det vanskelig fordi en utpekt initialiserer ikke kan delegere til en annen initialiserer i samme klasse og ikke kan kalle metoder for sin egen klasse før etter at den delegerer til superclass initialiserer. På det tidspunktet er det for sent å sette konstante egenskaper. Selvfølgelig kan du omgå denne begrensningen ved ikke å bruke konstanter, men det er ikke en god løsning. Så jeg skjønte det var best å bare gi standardverdier for de lagrede egenskapene der de er erklært. Det er fortsatt den beste løsningen som jeg for tiden vet om. Imidlertid fant jeg ut en alternativ teknikk som bruker initialisatorer.

Ta en titt på følgende eksempel. Dette er et utdrag Fra RateViewWithConvenienceInits.swift, som er en alternativ versjon av Min Swift port Of RateView. Å være En underklasse Av UIView som ikke gir standardverdier for alle sine egne lagrede egenskaper ved erklæring, må Denne alternative versjonen Av RateView i det minste gi en eksplisitt implementering av UIView er nødvendig init?(koder aDecoder: NSCoder) initializer. Vi vil også gi en eksplisitt implementering Av UIView ‘ s init (frame: Cgrect) initializer for å sikre at initialiseringsprosessen er konsistent. Vi ønsker at våre lagrede egenskaper skal settes opp på samme måte uansett hvilken initializer som brukes.
Legg Merke til at jeg la til bekvemmelighetsmodifikatoren til De overstyrte versjonene Av UIView ‘ s initializers. Jeg har også lagt til en failable utpekt initializer til underklassen, som begge de overstyrte (convenience) initializers delegerer til. Denne enkelt utpekte initialisatoren tar seg av å sette opp alle lagrede egenskaper – inkludert konstanter – jeg måtte ikke ty til å bruke var for alt). Det fungerer, men jeg synes det er ganske kludgy. Jeg foretrekker å bare gi standardverdier for mine lagrede egenskaper der de er deklarert, men det er godt å vite at dette alternativet eksisterer om nødvendig.

You might also like

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert.