în acest articol, voi explora unele dintre detaliile curajos de Inițializatori Swift. De ce să abordăm un subiect atât de interesant? În principal pentru că uneori m-am trezit confuz de erorile de compilare atunci când încerc să creez o subclasă de UIView sau când încerc să creez o versiune rapidă a unei vechi clase Objective-C, de exemplu. Așa că am decis să sap mai adânc pentru a înțelege mai bine nuanțele Inițializatorilor Swift. Deoarece experiența/experiența mea a implicat în cea mai mare parte lucrul cu Groovy, Java și Javascript, voi compara unele dintre mecanismele de inițializare ale Swift cu cele ale Java & Groovy. Vă încurajez să porniți un loc de joacă în XCode și să vă jucați singur cu exemplele de cod. De asemenea, vă încurajez să citiți secțiunea de inițializare a Ghidului lingvistic Swift 2.1, care este sursa principală de informații pentru acest articol. În cele din urmă, accentul meu va fi pe tipuri de referință (clase), mai degrabă decât tipuri de valoare (structs).
Ce Este Un Inițializator?
un inițializator este un tip special de metodă într-o Struct Swift, clasa, sau enum care este responsabil pentru a face sigur că o instanță nou creat de struct, clasa, sau enum este complet inițializat înainte de a fi gata pentru a fi utilizate. Ele joacă același rol ca și cel al unui „constructor” în Java și Groovy. Dacă sunteți familiarizat cu Objective-C, trebuie să rețineți că inițializatorii Swift diferă de inițializatorii Objective-C prin faptul că nu returnează o valoare.
subclasele, în general, nu moștenesc Inițializatorii
unul dintre primele lucruri de reținut este că „subclasele Swift nu moștenesc inițializatorii superclaselor în mod implicit”, conform Ghidului lingvistic. (Ghidul explică faptul că există scenarii în care inițializatoarele superclaselor sunt moștenite automat. Vom acoperi aceste scenarii excepționale mai târziu). Acest lucru este în concordanță cu modul în care funcționează Java (și, prin extensie, Groovy). Luați în considerare următoarele:
la fel ca în cazul Java & Groovy, este logic că acest lucru nu este permis (deși, la fel ca în majoritatea lucrurilor, pot exista unele argumente în acest sens. A se vedea acest post StackOverflow). Dacă ar fi permis, inițializarea proprietății” instrument ” a muzicianului ar fi ocolită, lăsând potențial instanța dvs. de muzician într-o stare nevalidă. Cu toate acestea, cu Groovy în mod normal, nu m-aș deranja să scriu inițializatori (adică Constructori). Mai degrabă, aș folosi doar constructorul de hărți Groovy oferă implicit, ceea ce vă permite să alegeți liber și să alegeți ce proprietăți doriți să setați la construcție. De exemplu, următorul cod Groovy este perfect valabil:
observați că puteți include orice proprietate, inclusiv cele furnizate de superclase, dar nu trebuie să specificați toate (sau oricare) dintre ele și nu trebuie să fie specificate într-o anumită ordine. Acest tip de inițializator ultra-flexibil nu este furnizat de Swift. Cel mai apropiat lucru din Swift este inițializatorii memberwise furnizați automat pentru structuri. Dar ordinea argumentelor într – un inițializator memberwise este semnificativă, chiar dacă sunt numite și depinde de ordinea în care sunt definite:
oricum, înapoi la clase-filosofia Groovy cu privire la valabilitatea obiectului post-construcție este în mod clar foarte diferită de cea a lui Swift. acesta este doar unul dintre multele moduri în care Groovy diferă de Swift.
desemnat vs comoditate Inițializatoare
înainte de a ajunge prea departe în jos gaura de iepure, ar trebui să clarifice un concept important: În Swift, un inițializator este clasificat ca fiind fie un inițializator desemnat, fie un inițializator convenabil. Voi încerca să explic diferența atât conceptual, cât și sintactic. Fiecare clasă trebuie să aibă cel puțin un inițializator desemnat, dar poate avea mai multe („desemnat” nu implică „singur”). Un inițializator desemnat este considerat un inițializator primar. Ei sunt head-honchos. Ei sunt în cele din urmă responsabili pentru a se asigura că toate proprietățile sunt inițializate. Din cauza acestei responsabilități, uneori poate deveni o durere să folosești tot timpul un inițializator desemnat, deoarece acestea pot necesita mai multe argumente. Imaginați-vă că lucrați cu o clasă care are mai multe proprietăți și trebuie să creați mai multe instanțe ale acelei clase care sunt aproape identice, cu excepția uneia sau a două proprietăți. (De dragul argumentului, să presupunem, de asemenea, că nu există valori implicite sensibile care ar fi putut fi atribuite proprietăților atunci când sunt declarate). De exemplu, să presupunem că și clasa noastră de persoane a mâncat alimente și se bucurăproprietăți muzicale. Desigur, aceste două lucruri ar fi stabilite la adevărat cele mai multe ori, dar nu se știe niciodată. Aruncați o privire:
acum, clasa noastră de persoane are patru proprietăți care trebuie setate și avem un inițializator desemnat care poate face treaba. Inițializatorul desemnat este primul, cel care ia 4 argumente. De cele mai multe ori, aceste ultime două argumente vor avea valoarea „adevărată”. Ar fi o durere să trebuiască să le specificăm de fiecare dată când vrem să creăm o persoană tipică. Aici intră ultimele două inițializatoare, cele marcate cu modificatorul de confort. Acest model ar trebui să pară familiar unui dezvoltator Java. Dacă aveți un constructor care ia mai multe argumente decât aveți cu adevărat nevoie pentru a face față tot timpul, puteți scrie Constructori simplificați care iau un subset al acestor argumente și oferă valori implicite sensibile pentru ceilalți. Inițializatorii de comoditate trebuie să delege fie unui alt inițializator de comoditate, poate mai puțin convenabil, sau unui inițializator desemnat. În cele din urmă, un inițializator desemnat trebuie să se implice. Mai mult, dacă aceasta este o subclasă, inițializatorul desemnat trebuie să apeleze un inițializator desemnat din superclasa sa imediată.
un exemplu din lumea reală pentru utilizarea modificatorului de comoditate provine din clasa Uibezierpath a UIKit. Sunt sigur că vă puteți imagina că există mai multe moduri de a specifica o cale. Ca atare, UIBezierPath oferă mai multe inițializatoare de comoditate:
init de comoditate publică(rect: CGRect)
init de comoditate publică(ovalInRect rect: CGRect)
init de comoditate publică(roundedRect rect: cgrect, cornerRadius: CGFloat)
init de comoditate publică(roundedRect rect: CGRect, byRoundingCorners corners: uirectcorner, Cornerradii: cgsize)
public convenience init(arccenter Center: Cgpoint, raza: cgfloat, startAngle: CGFloat, pune în pericol: CGFloat, sensul acelor de ceasornic: Bool)
comoditate publică init(CGPath: CGPath)
mai devreme, am spus o clasă poate avea mai multe initializatori desemnate. Deci, cum arată asta? O diferență importantă, aplicată de compilator, între inițializatorii desemnați și inițializatorii de comoditate este că inițializatorii desemnați nu pot delega unui alt inițializator din aceeași clasă (dar trebuie să delege unui inițializator desemnat în superclasa sa imediată). Uită-te la al doilea inițializator de persoană, cel care ia un singur argument numit „unDead”. Deoarece acest inițializator nu este marcat cu modificatorul de comoditate, Swift îl tratează ca un inițializator desemnat. Ca atare, nu se poate delega la un alt inițializator în persoană. Încercați să comentați primele patru rânduri și să necomentați ultima linie. Compilatorul se va plânge, iar XCode ar trebui să încerce să vă ajute sugerând să îl remediați adăugând modificatorul de comoditate.
acum ia în considerare subclasa muzician de persoană. Are un singur inițializator și, prin urmare, trebuie să fie un inițializator desemnat. Ca atare, trebuie să numească un inițializator desemnat al superclasei imediate, persoană. Amintiți-vă: în timp ce inițializatorii desemnați nu pot delega unui alt inițializator din aceeași clasă, inițializatorii de comoditate trebuie să facă acest lucru. De asemenea, un inițializator desemnat trebuie să apeleze un inițializator desemnat al superclasei sale imediate. Consultați ghidul lingvistic pentru mai multe detalii (și grafică frumoasă).
faze de inițializare
după cum explică Ghidul lingvistic Swift, există două faze de inițializare. Fazele sunt delimitate de apelul către inițializatorul desemnat de superclasă. Faza 1 este înainte de apelul la inițializator desemnat superclasă, faza 2 este după. O subclasă trebuie să inițializeze toate proprietățile proprii în faza 1 și este posibil să nu stabilească proprietăți definite de superclasă până în faza 2.
iată un eșantion de cod, adaptat dintr-un eșantion furnizat în ghidul lingvistic, care arată că trebuie să inițializați propriile proprietăți ale unei subclase înainte de a invoca inițializatorul desemnat de superclasă. Nu puteți accesa proprietățile furnizate de superclasă decât după invocarea inițializatorului desemnat de superclasă. În cele din urmă, este posibil să nu modificați proprietățile stocate constante după ce a fost invocat inițializatorul desemnat de superclasă.
suprascrierea unui Inițializator
acum, că suntem convinși că subclasele, în general, nu moștenesc inițializatori, și suntem clare cu privire la sensul și distincția dintre inițializatori desemnate și comoditate, să ia în considerare ceea ce se întâmplă atunci când doriți o subclasă pentru a suprascrie un inițializator din superclasa imediată. Există patru scenarii pe care aș dori să acopere, având în vedere că există două tipuri de inițializator. Deci, să le luăm unul câte unul, cu exemple simple de cod pentru fiecare caz:
un inițializator desemnat care se potrivește cu un inițializator desemnat superclasă
acesta este un scenariu tipic. Când faceți acest lucru, trebuie să aplicați modificatorul de suprascriere. Rețineți că acest scenariu este în vigoare chiar și atunci când „suprascrieți” un inițializator implicit furnizat automat (adică atunci când superclasa nu definește Niciun inițializator explicit. În acest caz, Swift oferă unul implicit. Dezvoltatorii Java ar trebui să fie familiarizați cu acest comportament). Acest inițializator implicit furnizat automat este întotdeauna un inițializator desemnat.
un inițializator desemnat care se potrivește cu un inițializator de comoditate superclasă
acum să presupunem că doriți să adăugați un inițializator desemnat la subclasa dvs. care se potrivește cu un inițializator de comoditate din părinte. Prin regulile delegării inițializatorului prevăzute în ghidul lingvistic, inițializatorul desemnat de subclasă trebuie să delege până la un inițializator desemnat al superclasei imediate. Adică, este posibil să nu delegați până la inițializatorul de comoditate de potrivire al părintelui. De dragul argumentului, să presupunem, de asemenea, că subclasa nu se califică pentru a moșteni inițializatorii superclasei. Apoi, din moment ce nu ați putea crea niciodată o instanță a subclasei dvs. invocând direct inițializatorul de comoditate superclasă, acel inițializator de comoditate potrivit nu este și nu ar putea fi niciodată implicat în procesul de inițializare oricum. Prin urmare, nu îl suprascrieți cu adevărat, iar modificatorul de suprascriere nu se aplică.
un inițializator de comoditate care se potrivește cu un inițializator desemnat de superclasă
în acest scenariu, imaginați-vă că aveți o subclasă care adaugă propriile proprietăți a căror valoare implicită poate fi (dar nu trebuie să fie) calculată din valorile atribuite uneia sau mai multor proprietăți din clasa părinte. Să presupunem că doriți doar să aveți un inițializator desemnat pentru subclasa dvs. Puteți adăuga un inițializator de comoditate la subclasa dvs. a cărei semnătură se potrivește cu cea a unui inițializator desemnat al clasei părinte. În acest caz, noul dvs. inițializator ar avea nevoie atât de modificatori de confort, cât și de suprascriere. Iată un exemplu de cod valid pentru a ilustra acest caz:
un inițializator de comoditate care se potrivește cu un inițializator de comoditate de superclasă
dacă doriți să adăugați un inițializator de comoditate la subclasa dvs. care se potrivește cu semnătura unui inițializator de comoditate al superclasei dvs., mergeți mai departe. Așa cum am explicat mai sus, oricum nu puteți trece peste inițializatoarele de comoditate. Deci, ați include modificatorul de comoditate, dar omiteți modificatorul de suprascriere și tratați-l la fel ca orice alt inițializator de comoditate.
o cheie takeaway din această secțiune este că modificatorul de suprascriere este utilizat numai, și trebuie să fie utilizat, dacă suprascrie un inițializator desemnat superclasă. (Clarificare minoră de făcut aici: dacă suprascrieți un inițializator necesar, atunci utilizați modificatorul necesar în locul modificatorului de suprascriere. Modificatorul necesar implică modificatorul de suprascriere. Consultați secțiunea Inițializatoare necesare de mai jos).
când Inițializatoarele sunt moștenite
acum pentru scenariile menționate mai sus în care inițializatoarele superclaselor sunt moștenite. După cum explică Ghidul lingvistic Swift, dacă subclasa dvs. oferă valori implicite pentru toate proprietățile proprii la declarație și nu definește niciunul dintre inițializatorii desemnați, atunci va moșteni automat toți inițializatorii desemnați de superclasă. Sau, dacă subclasa dvs. oferă o implementare a tuturor inițializatorilor desemnați de superclasă, atunci moștenește automat toți inițializatorii de comoditate superclasă. Acest lucru este în concordanță cu regula din Swift că inițializarea claselor (și structurilor) nu trebuie să lase proprietățile stocate într-o stare nedeterminată.
am dat peste un comportament „interesant” în timp ce experimentam cu inițializatori de comoditate, inițializatori desemnați și regulile de moștenire. Am constatat că este posibil să configurați un cerc vicios din neatenție. Luați în considerare următorul exemplu:
clasa RecipeIngredient înlocuiește toți inițializatorii desemnați din clasa alimentară și, prin urmare, moștenește automat toți inițializatorii de confort superclass. Dar inițializatorul de confort alimentar deleagă în mod rezonabil propriul inițializator desemnat, care a fost înlocuit de subclasa RecipeIngredient. Deci, nu este versiunea alimentară a inițializatorului init (Nume: String) care este invocat, ci versiunea suprascrisă în RecipeIngredient. Versiunea suprascrisă profită de faptul că subclasa a moștenit inițializatorul de confort al alimentelor și acolo este – aveți un ciclu. Nu știu dacă acest lucru ar fi considerat un programator-greșeală sau un bug compilator (l-am raportat ca un bug: https://bugs.swift.org/browse/SR-512 ). Imaginați-vă că mâncarea este o clasă de la o petrecere a 3-a și nu aveți acces la codul sursă, astfel încât să nu știți cum este implementat de fapt. Nu ați ști (până la rulare) că utilizarea acestuia în modul prezentat în acest exemplu vă va prinde într-un ciclu. Deci cred că ar fi mai bine dacă compilatorul ne-ar ajuta aici.
Inițializatori Eșuabili
Imaginați-vă că ați proiectat o clasă care are anumiți invarianți și doriți să impuneți acei invarianți din momentul în care este creată o instanță a clasei. De exemplu, poate modelați o factură și doriți să vă asigurați că suma este întotdeauna non-negativă. Dacă ați adăugat un inițializator care ia un argument sumă de tip dublu, cum ai putea să vă asigurați că nu încalcă invariant dumneavoastră? O strategie este de a verifica pur și simplu dacă argumentul este non-negativ. Dacă este, folosește-l. În caz contrar, implicit la 0. De exemplu:
acest lucru ar funcționa și poate fi acceptabil dacă documentați ce face inițializatorul dvs. (mai ales dacă intenționați să vă puneți clasa la dispoziția altor dezvoltatori). Dar s-ar putea să vă fie greu să apărați această strategie, deoarece într-un fel mătură problema sub covor. O altă abordare susținută de Swift ar fi aceea de a lăsa inițializarea să eșueze. Asta este, v-ar face inițializator failable.
după cum descrie acest articol, inițializatorii eșuabili au fost adăugați la Swift ca o modalitate de a elimina sau reduce nevoia de metode din fabrică, „care anterior erau singura modalitate de a raporta eșecul” în timpul construcției obiectelor. Pentru a face un inițializator failable, pur și simplu adăugați ? sau ! caracter după cuvântul cheie init (adică, init? sau init! ). Apoi, după ce toate proprietățile au fost setate și toate celelalte reguli privind delegarea au fost îndeplinite, ați adăuga o logică pentru a verifica dacă argumentele sunt valide. Dacă acestea nu sunt valide, declanșați un eșec de inițializare cu return nil. Rețineți că acest lucru nu implică faptul că inițializatorul returnează vreodată ceva. Iată cum ar putea arăta clasa noastră de facturi cu un inițializator failable:
observați ceva diferit despre modul în care folosim rezultatul creării obiectului? E ca și cum l-am trata ca pe un opțional, nu? Ei bine, asta este exact ceea ce facem! Când vom folosi un inițializator failable, vom obține fie zero(în cazul în care un eșec de inițializare a fost declanșat) sau un opțional (Factură). Adică, dacă inițializarea a avut succes, vom ajunge la un opțional care înfășoară instanța de factură care ne interesează, așa că trebuie să o desfacem. (Ca o paranteză, rețineți că Java are și opțiuni începând cu Java 8).
inițializatoare Failable sunt la fel ca și celelalte tipuri de inițializatoare care le-am discutat cu privire la suprascriere și delegare, desemnat vs comoditate, etc… de fapt, puteți suprascrie chiar și un inițializator failable cu un inițializator nefailable. Cu toate acestea, nu puteți suprascrie un inițializator care nu poate fi defectat cu unul care poate fi defectat.
este posibil să fi observat inițializatori failable de a face cu UIView sau UIViewController, care oferă atât un inițializator failable init?(coder aDecoder: NSCoder). Acest inițializator este apelat atunci când vizualizarea sau ViewController este încărcat de la o peniță. Este important să înțelegeți cum funcționează inițializatoarele eșuate. Vă recomandăm insistent să citiți secțiunea Inițializatori Failable a Ghidului lingvistic Swift pentru o explicație aprofundată.
Inițializatoare necesare
modificatorul necesar este utilizat pentru a indica faptul că toate subclasele trebuie să implementeze inițializatorul afectat. Pe fata de ea, care sună destul de simplu și direct. Se poate obține un pic confuz uneori dacă nu înțelegeți cum intră în joc regulile privind moștenirea inițializatorului discutate mai sus. Dacă o subclasă îndeplinește criteriile prin care inițializatorii superclasei sunt moșteniți, atunci setul de inițializatori moșteniți include cele marcate necesare. Prin urmare, subclasa satisface implicit contractul impus de modificatorul necesar. Ea pune în aplicare inițializator necesar (e), tu chiar nu-l văd în codul sursă.
dacă o subclasă oferă o implementare explicită (adică nu moștenită) a unui inițializator necesar, atunci suprascrie și implementarea superclasei. Modificatorul necesar implică suprascrierea, astfel încât modificatorul de suprascriere nu este utilizat. Puteți să-l includeți dacă doriți, dar acest lucru ar fi redundant și XCode vă va bate la cap despre asta.
Ghidul lingvistic Swift nu spune prea multe despre modificatorul necesar, așa că am pregătit un eșantion de cod (vezi mai jos) cu comentarii pentru a explica scopul și a descrie cum funcționează. Pentru mai multe informații, consultați acest articol de Anthony Levings.
caz Special: extinderea UIView
unul dintre lucrurile care m-au determinat să sap adânc în inițializatoarele Swift a fost încercarea mea de a găsi o modalitate de a crea un set de inițializatoare desemnate fără a duplica logica de inițializare. De exemplu, Lucram prin acest tutorial UIView de Ray Wenderlich, convertind codul său Objective-C în Swift pe măsură ce mergeam (puteți arunca o privire la versiunea mea Swift aici). Dacă te uiți la acel tutorial, vei vedea că subclasa lui RateView de UIView are o metodă baseInit pe care ambii inițializatori desemnați o folosesc pentru a efectua sarcini comune de inițializare. Aceasta pare a fi o abordare bună pentru mine – nu doriți să duplicați acele lucruri în fiecare dintre aceste inițializatoare. Am vrut să recreez această tehnică în versiunea mea rapidă a RateView. Dar mi s-a părut dificil, deoarece un inițializator desemnat nu poate delega unui alt inițializator din aceeași clasă și nu poate apela metode ale propriei clase decât după ce deleagă inițializatorul superclasei. În acel moment, este prea târziu pentru a stabili proprietăți constante. Desigur, ați putea rezolva această limitare prin faptul că nu utilizați constante, dar aceasta nu este o soluție bună. Așa că m-am gândit că este mai bine să furnizați doar valori implicite pentru proprietățile stocate unde sunt declarate. Aceasta este încă cea mai bună soluție pe care o cunosc în prezent. Cu toate acestea, mi-am dat seama de o tehnică alternativă care folosește inițializatoare.
uitați-vă la următorul exemplu. Acesta este un fragment din RateViewWithConvenienceInits.swift, care este o versiune alternativă a portului meu Swift din RateView. Fiind o subclasă de UIView care nu oferă valori implicite pentru toate proprietățile proprii stocate la Declarație, această versiune alternativă a RateView trebuie să ofere cel puțin o implementare explicită a init-ului necesar UIView?(coder aDecoder: NSCoder) inițializator. De asemenea, vom dori să oferim o implementare explicită a init-ului UIView(cadru: Cgrect) inițializator pentru a vă asigura că procesul de inițializare este consecvent. Dorim ca proprietățile noastre stocate să fie configurate în același mod, indiferent de inițializatorul utilizat.
observați că am adăugat modificatorul de confort la versiunile suprascrise ale inițializatorilor UIView. Am adăugat, de asemenea, un inițializator desemnat failable la subclasa, care ambele suprascrise (comoditate) inițializatori delega. Acest inițializator unic desemnat are grijă să configureze toate proprietățile stocate (inclusiv constantele – nu a trebuit să recurg la utilizarea var pentru tot). Acesta funcționează, dar cred că este destul de kludgy. Aș prefera să furnizeze doar valori implicite pentru proprietățile mele stocate în cazul în care acestea sunt declarate, dar este bine să știu că această opțiune există, dacă este necesar.