tässä artikkelissa tutustun Swift initializersin karuihin yksityiskohtiin. Miksi tarttua näin jännittävään aiheeseen? Lähinnä siksi, että olen joskus huomannut hämmentyneeni käännösvirheistä yrittäessäni luoda Uiviewin alaluokkaa tai yrittäessäni luoda esimerkiksi vanhan Objective-C-luokan nopeaa versiota. Niinpä päätin kaivaa syvemmältä saadakseni paremman käsityksen Swift-alustajien vivahteista. Koska taustani / kokemukseni on ollut lähinnä Groovyn, Javan ja JavaScriptin parissa työskentelemistä, vertaan Swiftin alustusmekanismeja Java & Groovyyn. Kannustan sinua sytyttämään leikkikentän Xcodessa ja leikkimään itse koodiesimerkeillä. Kehotan teitä myös lukemaan Swift 2.1-Kielioppaan Alustusosion, joka on tämän artikkelin ensisijainen tietolähde. Lopuksi keskityn vertailutyyppeihin (luokat) arvotyyppien (structs) sijaan.
Mikä On Alustaja?
initializer on Swiftin, luokan tai enumin erityinen menetelmä, jonka tehtävänä on varmistaa, että vasta luotu struct, class tai enum on täysin initialisoitu ennen kuin ne ovat valmiita käytettäväksi. Niillä on sama rooli kuin ”rakentajalla” Jaavalla ja Groovyssa. Jos olet perehtynyt Objective-C: hen, huomaa, että Swift-initializers eroaa Objective-C-initializersista siinä, että ne eivät palauta arvoa.
alaluokat eivät yleensä peri Initializers
yksi ensimmäisistä asioista, jotka on pidettävä mielessä on, että ”Swiftin alaluokat eivät peri superluokan initializers by default”, kielioppaan mukaan. (Oppaassa kerrotaan, että on olemassa skenaarioita, joissa superluokan alustajat periytyvät automaattisesti. Käsittelemme näitä poikkeuksellisia skenaarioita myöhemmin). Tämä on sopusoinnussa sen kanssa, miten Java (ja laajemmin Groovy) toimii. Harkitsehan seuraavia asioita:
aivan kuten Jaavan & Groovyn kohdalla, on järkevää, että tämä ei ole sallittua (tosin, kuten useimmissa asioissa, tästä asiasta voi olla joku argumentti. Katso tämä StackOverflow post). Jos se olisi sallittua, alustus ”väline” ominaisuus muusikko olisi ohitettu, mahdollisesti jättäen muusikko esimerkiksi invaliditilassa. Kuitenkin Groovy en yleensä vaivaudu kirjallisesti initializers (eli rakentajat). Pikemminkin, olisin vain käyttää kartta constructor Groovy tarjoaa implisiittisesti, jonka avulla voit vapaasti valita, mitkä ominaisuudet haluat asettaa rakentamisen. Esimerkiksi seuraava on täysin pätevä Groovy-koodi:
huomaa, että voit sisällyttää mukaan minkä tahansa ominaisuuden, mukaan lukien superluokkien tarjoamat ominaisuudet, mutta sinun ei tarvitse määritellä kaikkia (tai mitään) niistä, eikä niitä tarvitse määritellä missään tietyssä järjestyksessä. Tällaista erittäin joustavaa alustinta Swift ei tarjoa. Swiftissä lähimpänä ovat automaattisesti toimitetut jäsentensä alustajat struktuureille. Mutta argumenttien järjestys jäsentenvälisessä initializerissa on merkittävä, vaikka ne nimetäänkin, ja riippuu siitä, missä järjestyksessä ne määritellään:
joka tapauksessa, takaisin luokkiin – Groovyn filosofia Konstruktion jälkeisestä objektin pätevyydestä on selvästi hyvin erilainen kuin Swiftin. tämä on vain yksi monista tavoista, joilla Groovy eroaa Swiftistä.
nimetyt vs. Mukavuusaloittajat
ennen kuin menemme liian pitkälle kaninkoloon, meidän tulisi selventää tärkeää käsitettä: Swift, initializer luokitellaan joko nimetty tai convenience initializer. Yritän selittää eron sekä käsitteellisesti että syntaktisesti. Jokaisella luokalla on oltava vähintään yksi nimetty initializer, mutta niitä voi olla useita (”nimetty” ei tarkoita ”yksi”). Nimettyä initializeria pidetään ensisijaisena initializerina. He ovat Pää-honchoja. He ovat viime kädessä vastuussa siitä, että kaikki ominaisuudet alustetaan. Koska tämä vastuu, se voi joskus tulla kipu käyttää nimettyä initializer koko ajan, koska ne voivat vaatia useita argumentteja. Kuvittele työskentelyä luokan, joka on useita ominaisuuksia, ja sinun täytyy luoda useita tapauksia, että luokka, jotka ovat lähes identtisiä paitsi yksi tai kaksi ominaisuutta. (Todistelun vuoksi oletetaan myös, että ei ole olemassa järkeviä oletusarvoja, jotka olisi voitu määrittää ominaisuuksille, kun ne on ilmoitettu). Sanotaan esimerkiksi, että Henkilöluokassamme oli myös eatsFood-ja enjoysMusic-ominaisuuksia. Tietenkin, nämä kaksi asiaa olisi asetettu totta suurimman osan ajasta, mutta ei koskaan tiedä. Katso:
nyt meidän Henkilöluokassa on neljä ominaisuutta, jotka pitää asettaa, ja meillä on nimikkokirjain, joka voi hoitaa homman. Nimetty initializer on, että ensimmäinen, joka kestää 4 argumentteja. Useimmiten nämä kaksi viimeistä argumenttia tulevat olemaan arvo ”true”. Olisi tuskallista täsmentää niitä aina, kun haluamme luoda tyypillisen ihmisen. Siinä kaksi viimeistä nimikirjainta tulevat kuvaan, ne jotka on merkitty convenience modifierilla. Tämän kuvion pitäisi näyttää tutulta Java-kehittäjälle. Jos sinulla on rakentaja, joka vie enemmän argumentteja kuin sinun todella tarvitsee käsitellä koko ajan, voit kirjoittaa yksinkertaistettuja rakentajia, jotka ottavat osajoukon näistä argumenteista ja tarjoavat järkeviä oletusarvoja muille. Convenience initializers on delegoitava joko toiselle, ehkä vähemmän kätevälle, convenience initializerille tai nimetylle initializerille. Viime kädessä nimetyn alustajan on osallistuttava. Edelleen, Jos tämä on alaluokka, nimetty initializer on kutsuttava nimetty initializer sen välittömästä superluokka.
yksi reaalimaailman esimerkki convenience-modifierin käytöstä tulee Uikitin UIBezierPath-luokasta. Voit varmasti kuvitella, että on olemassa useita tapoja määrittää polku. Sellaisenaan, UIBezierPath tarjoaa useita mukavuusaloitteita:
public convenience init(rect: CGRect)
public convenience init(ovalInRect rect: CGRect)
public convenience init(roundedRect rect: cgrect, cornerRadius: cgfloat)
public convenience init(roundedRect rect: cgrect, byRoundingCorners corners: uirectcorner, Cornerradii: cgsize)
public convenience init(arccenter Center: CGPoint, radius: cgfloat, startAngle: CGFloat, vaarantavat: CGFloat, myötäpäivään: Bool)
public convenience init(CGPath: CGPath)
Earlier, I said a class may have multiple designed initializers. Miltä se näyttää? Yksi tärkeä ero, jota kääntäjä valvoo, nimettyjen alustajien ja mukavuusaloittajien välillä on, että nimetyt alustajat eivät saa delegoida toiselle alustajalle samassa luokassa (mutta on delegoitava nimetylle alustajalle sen välittömässä superluokassa). Katso toinen initializer henkilö, joka vie yhden argumentin nimeltä ”unDead”. Koska tätä initializeria ei ole merkitty convenience-muokkaimella, Swift käsittelee sitä nimettynä initializerina. Sellaisenaan se ei voi siirtää toiselle alustajalle henkilökohtaisesti. Yritä kommentoida neljää ensimmäistä riviä ja olla kommentoimatta viimeistä riviä. Kääntäjä valittaa, ja XCode yrittää auttaa sinua ehdottamalla, että korjaat sen lisäämällä convenience-modifierin.
katsotaan nyt muusikon alaluokkaan henkilö. Sillä on yksi initializer, joten sen on oltava nimetty initializer. Sellaisena sen on kutsuttava nimettyä nimikirjainta välittömästä superluokasta, henkilöstä. Muista: vaikka nimetyt alustajat eivät voi delegoida toiselle saman luokan alustajalle, convenience-alustajien on tehtävä niin. Myös nimetyn alustajan on kutsuttava nimettyä alustajaa sen välittömästä superluokasta. Katso lisätietoja kielioppaasta (ja kaunis grafiikka).
Alustusvaiheet
kuten Swift – kielioppaassa kerrotaan, alustusvaiheita on kaksi. Vaiheet rajataan kutsulla superluokkaan nimetty initializer. Vaihe 1 on ennen kutsua superluokan nimetty initializer, Vaihe 2 on jälkeen. Alaluokan on alustettava kaikki omat ominaisuutensa vaiheessa 1, eikä se saa asettaa mitään superluokan määrittelemiä ominaisuuksia ennen vaihetta 2.
tässä on kielioppaassa annetusta otoksesta mukautettu koodinäyte, joka osoittaa, että aliluokan omat ominaisuudet on alustettava ennen kuin vedotaan superluokan nimettyyn initializeriin. Et voi käyttää superluokan tarjoamia ominaisuuksia ennen kuin olet käyttänyt superluokan nimettyä initializeria. Lopuksi, et saa muuttaa jatkuvasti tallennettuja ominaisuuksia, kun superclass nimetty initializer on vedottu.
Initializerin ohittaminen
nyt kun olemme vakuuttuneita siitä, että alaluokat eivät yleensä peri initializersia, ja olemme selvillä nimettyjen ja convenience initializers-nimikkeiden merkityksestä ja erosta, Mietitäänpä mitä tapahtuu, kun haluat alaluokan ohittavan initializerin sen välittömästä superluokasta. On neljä skenaarioita, jotka haluaisin kattaa, koska on olemassa kahdenlaisia initializer. Otetaan siis ne yksi kerrallaan, yksinkertaisilla koodiesimerkeillä jokaiselle tapaukselle:
nimetty initializer, joka vastaa superluokan nimettyä initializeria
tämä on tyypillinen skenaario. Kun teet tämän, sinun on käytettävä ohitusmuunninta. Huomaa, että tämä skenaario on voimassa silloinkin, kun ”ohitat” automaattisesti toimitetun oletusalgoritmin (eli kun superluokka ei määrittele mitään nimenomaista alustinta. Tässä tapauksessa Swift tarjoaa sellaisen implisiittisesti. Java-kehittäjien tulisi tuntea tämä käyttäytyminen). Tämä automaattisesti annettu oletusalgoritmi on aina nimetty alustaja.
nimetty alustaja, joka vastaa superluokan mukavuusluokan alustajaa
oletetaan nyt, että haluat lisätä nimetyn alustajan aliluokkaan, joka sattuu vastaamaan vanhemman mukavuusluokan alustajaa. Kielioppaassa vahvistettujen initializer-delegoinnin sääntöjen mukaan aliluokkasi nimetyn initializerin on delegoitava enintään välittömän superluokan nimetylle initializerille. Toisin sanoen, et voi siirtää jopa vanhemman matching convenience initializer. Väitteen vuoksi oletetaan myös, että alaluokka ei ole oikeutettu perimään superluokan initializers. Sitten, koska et voinut koskaan luoda esimerkiksi oman aliluokan vetoamalla suoraan superluokan convenience initializer, että matching convenience initializer ei ole, eikä voisi koskaan olla, mukana alustusprosessissa joka tapauksessa. Siksi et oikeasti ohita sitä,eikä ohitusmuunnos päde.
convenience initializer, joka vastaa superluokan nimettyä initializeria
tässä skenaariossa, kuvittele, että sinulla on alaluokka, joka lisää omia ominaisuuksiaan, joiden oletusarvo voidaan (mutta ei tarvitse) laskea yhdelle tai useammalle yläluokan ominaisuuksille annetuista arvoista. Oletetaan myös, että haluat vain yhden nimetyn alustajan alaluokallesi. Voit lisätä aliluokkaan kätevän alustajan, jonka allekirjoitus vastaa vanhemman luokan nimetyn alustajan allekirjoitusta. Tässä tapauksessa Uusi initializer tarvitsisi sekä convenience-että override-muuntimet. Tässä on kelvollinen koodinäyte tämän tapauksen havainnollistamiseksi:
mukavuusalgoritmi, joka vastaa superluokan mukavuusalgoritmia
jos haluat lisätä aliluokkaan mukavuusalgoritmin, joka sattuu vastaamaan superluokan mukavuusalgoritmin allekirjoitusta, anna mennä vain. Kuten edellä selitin, et voi oikeastaan ohittaa convenience initializers muutenkaan. Joten voit sisällyttää convenience modifier, mutta jättää ohitus modifier, ja kohdella sitä kuin mitä tahansa muuta convenience initializer.
yksi keskeinen takeaway tästä osiosta on, että ohitusmuunninta käytetään vain, ja sitä on käytettävä, jos ohitat superluokan nimetyn initializerin. (Pieni selvennys tähän: Jos ohitat vaaditun initializerin, käytät vaadittua modifieria ohitusmuunnoksen sijaan. Tarvittava modifier tarkoittaa ohitus modifier. Katso tarvittavat Initializers kohta alla).
kun Initialisoijat periytyvät
nyt edellä mainituissa skenaarioissa, joissa superluokan initialisoijat periytyvät. Kuten Swift Language Guide selittää, jos aliluokkasi tarjoaa oletusarvot kaikille sen omille ominaisuuksille julistuksessa, eikä määrittele mitään omista nimetyistä initializerseista, se perii automaattisesti kaikki sen superluokan nimetyt initializers. Tai, jos aliluokka tarjoaa täytäntöönpanon kaikki superluokan nimetty initializers, niin se automaattisesti perii kaikki superluokan convenience initializers. Tämä on sopusoinnussa Swiftin säännön kanssa, jonka mukaan luokkien (ja rakenteiden) alustaminen ei saa jättää tallennettuja ominaisuuksia määrittelemättömään tilaan.
törmäsin johonkin ”mielenkiintoiseen” käyttäytymiseen kokeillessani mukavuusaloittimia, nimettyjä alustajia ja perintösääntöjä. Huomasin, että noidankehä on mahdollista virittää epähuomiossa. Mietitäänpä seuraavaa esimerkkiä:
Reseptipredienttiluokka ohittaa kaikki elintarvikeluokan nimetyt initializers, ja siksi se perii automaattisesti kaikki superluokan convenience initializers. Mutta ruoka convenience initializer kohtuullisesti delegoi oman nimetyn initializer, joka on syrjäyttänyt RecipeIngredient alaluokka. Joten se ei ole Food versio, että init(nimi: merkkijono) initializer, joka vedotaan, vaan ohitettu versio RecipeIngredient. Ohitettu versio hyödyntää sitä, että alaluokka on perinyt ruoan kätevän alustajan, ja siinä se on – sinulla on sykli. En tiedä, pidettäisiinkö tätä ohjelmointivirheenä vai kääntäjävirheenä (ilmoitin sen viaksi: https://bugs.swift.org/browse/SR-512 ). Kuvittele, että ruoka on luokka 3rd party, ja sinulla ei ole pääsyä lähdekoodiin, joten et tiedä, miten se todella toteutetaan. Et tiedä (ennen runtime), että käyttämällä sitä tavalla esitetty tässä esimerkissä saisi sinut ansaan sykli. Olisi parempi, jos kääntäjä auttaisi meitä.
epäonnistuneet Initializers
Kuvittele, että olet suunnitellut luokan, jossa on tiettyjä invariantteja, ja haluaisit pakottaa nämä invariantit siitä hetkestä lähtien, kun luokan instanssi on luotu. Ehkä olet esimerkiksi mallintamassa laskua ja haluat varmistaa, että summa on aina ei-negatiivinen. Jos olet lisännyt initializer joka vie määrä argumentti tyyppi kaksinkertainen, miten voit varmistaa, että et riko invariant? Yksi strategia on yksinkertaisesti tarkistaa, onko argumentti ei-negatiivinen. Jos on, käytä sitä. Muussa tapauksessa oletusarvo on 0. Esimerkiksi:
tämä toimisi ja voi olla hyväksyttävää, jos dokumentoit, mitä alustajasi tekee (varsinkin jos aiot antaa luokkasi muiden kehittäjien käyttöön). Mutta sinun voi olla vaikea puolustaa tuota strategiaa, koska se tavallaan lakaisee asian maton alle. Toinen Swiftin tukema lähestymistapa olisi antaa alustuksen epäonnistua. Toisin sanoen, tekisit nimikirjaimestasi epäonnistuvan.
kuten tässä artikkelissa kuvaillaan, Swiftiin lisättiin epäonnistuneita alustuksia keinona poistaa tai vähentää tehdasmenetelmien tarvetta,” jotka olivat aiemmin ainoa tapa ilmoittaa epäonnistumisesta ” objektin rakentamisen aikana. Jotta initializer epäonnistua, voit yksinkertaisesti liittää ? tai ! merkki jälkeen init avainsanan (ts., init? tai sisään! ). Sitten, kun kaikki ominaisuudet on asetettu ja kaikki muut delegointia koskevat säännöt on täytetty, lisäät logiikkaa varmistaaksesi, että argumentit ovat päteviä. Jos ne eivät ole kelvollisia, käynnistät alustusvirheen palauttamalla nollan. Huomaa, että tämä ei tarkoita, että alustaja olisi koskaan palauttamassa mitään. Tältä Invoice class saattaa näyttää epäonnistuneella alustajalla:
Huomaatko mitään erilaista siinä, miten käytämme objektin luomisen tulosta? Ihan kuin se olisi vapaaehtoinen. Juuri niin me teemme! Kun käytämme epäonnistuvaa alustajaa, saamme joko nollan(jos alustusvirhe laukesi) tai valinnaisen (Laskun). Toisin sanoen, jos alustus onnistui, päädymme valinnaiseen, joka käärii Laskun ilmentymän, josta olemme kiinnostuneita, joten meidän on avattava se. (Sivuhuomautuksena, huomaa, että Java on myös vaihtoehtoja kuin Java 8).
epäonnistuvat alustajat ovat aivan kuten muut alustajat, joista olemme keskustelleet koskien ohittamista ja delegointia, nimettyä vs mukavuutta jne… Itse asiassa, voit jopa ohittaa epäonnistuvan alustajan pettämättömällä alustajalla. Et kuitenkaan voi ohittaa epäonnistunutta alustinta.
olet saattanut huomata vikaantuvia initializereita UIView-tai UIViewController-ohjelman käsittelystä, jotka molemmat tarjoavat virheellisen init-alustajan?(koodari aDecoder: NSCoder). Tätä alustinta kutsutaan, kun näkymä tai ViewController Ladataan nib: stä. On tärkeää ymmärtää, miten epäonnistuvat alustajat toimivat. Suosittelen, että luet Swift-kielioppaan epäonnistuvan Initializers-osion läpi perusteellisen selityksen.
vaaditut alustajat
vaadittua muokkaajaa käytetään osoittamaan, että kaikkien alaluokkien on toteutettava kyseinen alustaja. Päällisin puolin se kuulostaa melko yksinkertaiselta ja suoraviivaiselta. Se voi saada hieman hämmentävää ajoittain, jos et ymmärrä, miten edellä käsitellyt initializer-perinnön säännöt tulevat peliin. Jos alaluokka täyttää kriteerit, joilla superluokka initializers periytyvät, niin perittyjen initializers-joukko sisältää ne, jotka on merkitty vaadittaviksi. Näin ollen alaluokka täyttää implisiittisesti vaaditun muokkaajan asettaman sopimuksen. Se toteuttaa tarvittavat initializer(s), et vain näe sitä lähdekoodissa.
jos alaluokka tarjoaa vaaditun alustajan eksplisiittisen (eli perimättömän) toteutuksen, se ohittaa myös superluokan toteutuksen. Vaadittu muunnos merkitsee ohitusta, joten ohitusmuunnosta ei käytetä. Voit sisällyttää sen, jos haluat, mutta niin olisi tarpeetonta ja XCode nalkuttaa sinulle siitä.
Swift – kielioppi ei kerro paljoakaan vaaditusta muuntajasta, joten laadin koodinäytteen (katso alla) kommenttien kera selittämään sen tarkoituksen ja kuvailemaan, miten se toimii. Lisätietoja tästä Anthony Levingsin kirjoituksesta.
erikoistapaus: Extending UIView
yksi asia, joka sai minut kaivautumaan syvälle Swift initializersiin, oli yritykseni keksiä tapa luoda joukko nimettyjä initializers ilman päällekkäistä initialization logiikkaa. Esimerkiksi, työskentelin tämän UIView opetusohjelma Ray Wenderlich, muuntaa hänen Objective-C koodi Swift kuin menin (voit katsoa minun Swift versio täällä). Jos katsot tätä opetusohjelma, näet, että hänen RateView alaluokka UIView on baseInit menetelmä, jota molemmat nimetyt initializers käyttää suorittaa yhteisiä alustustehtäviä. Se tuntuu minusta hyvältä lähestymistavalta – et halua kopioida tuota tavaraa jokaiseen noista nimikirjaimista. Halusin luoda sen tekniikan uudelleen nopeassa RateView – versiossani. Mutta minusta oli vaikeaa, koska nimetty alustaja ei voi delegoida toiselle alustajalle samassa luokassa eikä voi kutsua oman luokkansa menetelmiä ennen kuin se delegoi superluokan alustajalle. Siinä vaiheessa on liian myöhäistä asettaa vakio-ominaisuuksia. Tietenkin, voit kiertää tämän rajoituksen käyttämättä vakioita, mutta se ei ole hyvä ratkaisu. Joten ajattelin, että oli parasta vain antaa oletusarvot tallennetuille ominaisuuksille, joissa ne on ilmoitettu. Se on edelleen paras ratkaisu, jonka tällä hetkellä tiedän. Keksin kuitenkin vaihtoehtoisen tekniikan, joka käyttää alustimia.
Katso seuraavaa esimerkkiä. Tämä on pätkä Rateview with convenience in its.swift, joka on vaihtoehtoinen versio minun Swift port of RateView. Koska UIView on aliluokka, joka ei anna oletusarvoja kaikille sen omille tallennetuille ominaisuuksille ilmoituksessa, tämän vaihtoehtoisen RateView-version on ainakin tarjottava tarkka toteutus UIView ’ n vaatimista ominaisuuksista?(kooderi aDecoder: NSCoder) alustaja. Haluamme myös tarjota selkeän toteutuksen UIView ’ n init(frame: CGRect) initializer varmistaa, että alustusprosessi on johdonmukainen. Haluamme, että tallennetut ominaisuutemme asetetaan samalla tavalla riippumatta siitä, mitä alustinta käytetään.
huomaa, että lisäsin convenience-modifierin UIView ’ n initializers-versioiden ohitettuihin versioihin. Lisäsin myös epäonnistuvan nimetyn alustajan alaluokkaan, johon molemmat ohitetut (mukavuus) alustajat delegoivat. Tämä yksittäinen nimetty initializer huolehtii kaikkien tallennettujen ominaisuuksien perustamisesta (mukaan lukien vakiot – minun ei tarvinnut turvautua var: n käyttämiseen kaikkeen). Se toimii, mutta minusta se on aika kömpelö. Haluaisin vain antaa oletusarvot tallennetuille ominaisuuksilleni, joissa ne on ilmoitettu, mutta on hyvä tietää, että tämä vaihtoehto on olemassa tarvittaessa.