ebben a cikkben a Swift inicializátorok néhány apró részletét fedezem fel. Miért vállal egy ilyen izgalmas témát? Főleg azért, mert időnként összezavarodtam a fordítási hibák miatt, amikor megpróbáltam létrehozni az UIView alosztályát, vagy amikor például egy régi Objective-C osztály Swift verzióját próbáltam létrehozni. Ezért úgy döntöttem, hogy mélyebbre ások, hogy jobban megértsem a Swift inicializálók árnyalatait. Mivel a hátterem / tapasztalatom többnyire a Groovy, a Java és a Javascript használatával kapcsolatos, összehasonlítom a Swift néhány inicializálási mechanizmusát a Java & Groovy mechanizmusaival. Arra biztatlak, hogy gyújts fel egy játszóteret az XCode-ban,és játssz a kód példákkal. Javasoljuk továbbá, hogy olvassa el a Swift 2.1 nyelvi útmutató inicializálási szakaszát, amely a cikk elsődleges információforrása. Végül a referenciatípusokra (osztályokra) fogok összpontosítani, nem pedig az értéktípusokra (struktúrákra).
Mi Az Inicializáló?
az inicializáló a Swift struct, class vagy enum speciális metódusa, amely felelős annak biztosításáért, hogy a struct, class vagy enum újonnan létrehozott példánya teljesen inicializálva legyen, mielőtt készen állnának a használatra. Ugyanazt a szerepet játsszák, mint egy “konstruktor” a Java-ban és a Groovy-ban. Ha ismeri az Objective-C-t, vegye figyelembe, hogy a Swift inicializálók abban különböznek az Objective-C inicializálóktól, hogy nem adnak vissza értéket.
az alosztályok általában nem öröklik az Inicializálókat
az egyik első dolog, amit szem előtt kell tartani, hogy “a Swift alosztályok alapértelmezés szerint nem öröklik a superclass inicializálóikat”, a nyelvi útmutató szerint. (Az útmutató elmagyarázza, hogy vannak olyan forgatókönyvek, ahol a superclass inicializálók automatikusan öröklődnek. Ezeket a kivételes forgatókönyveket később ismertetjük). Ez összhangban van a Java (és kiterjesztéssel A Groovy) működésével. Vegye figyelembe a következőket:
csakúgy, mint a Java & Groovy, van értelme, hogy ez nem megengedett (bár, mint a legtöbb dolog, lehet némi érv ebben a kérdésben. Lásd ezt a StackOverflow bejegyzést). Ha megengedett lenne, a zenész “eszköz” tulajdonságának inicializálása megkerülhető lenne, potenciálisan érvénytelen állapotban hagyva a zenész példányát. A Groovy – val azonban általában nem foglalkoznék inicializátorok (azaz konstruktorok) írásával. Inkább csak a Groovy által implicit módon biztosított térképkonstruktort használnám, amely lehetővé teszi, hogy szabadon kiválassza, mely tulajdonságokat kívánja beállítani az építkezéskor. Például a következő tökéletesen érvényes Groovy kód:
vegye figyelembe, hogy bármilyen tulajdonságot felvehet, beleértve a szuperosztályok által biztosított tulajdonságokat is, de nem kell megadnia mindet (vagy egyiket), és nem kell megadnia őket semmilyen sorrendben. Ez a fajta ultra-rugalmas inicializáló nem biztosítja a Swift. A Swift-ben a legközelebbi dolog az automatikusan biztosított memberwise inicializálók a struktúrákhoz. De az argumentumok sorrendje a memberwise inicializálóban jelentős, annak ellenére, hogy meg vannak nevezve, és attól függ, hogy milyen sorrendben vannak definiálva:
mindegy, vissza az osztályokhoz – Groovy filozófiája az építés utáni objektum érvényességéről egyértelműen különbözik a Swiftétől. ez csak egy a sok közül, amiben Groovy különbözik a Swifttől.
kijelölt vs kényelmi Inicializálók
mielőtt túl messzire jutnánk a nyúl lyukán, tisztáznunk kell egy fontos fogalmat: A Swift-ben az inicializáló kijelölt vagy kényelmi inicializáló kategóriába tartozik. Megpróbálom elmagyarázni a különbséget mind fogalmilag, mind szintaktikailag. Minden osztálynak rendelkeznie kell legalább egy kijelölt inicializálóval, de lehet több is (a”kijelölt” nem jelenti az “egyetlenet”). A kijelölt inicializáló elsődleges inicializálónak számít. Ők a fej-honchók. Végső soron felelősek azért, hogy minden tulajdonság inicializálódjon. E felelősség miatt néha fájdalmat okozhat a kijelölt inicializáló használata, mivel ezek több érvet igényelhetnek. Képzelje el, hogy több tulajdonsággal rendelkező osztályokkal dolgozik, és több olyan példányt kell létrehoznia az osztályból, amelyek majdnem azonosak, kivéve egy vagy két tulajdonságot. (Az érvelés kedvéért tegyük fel azt is, hogy nincsenek olyan ésszerű alapértelmezések, amelyeket a tulajdonságokhoz rendelhettek volna, amikor deklarálják őket). Tegyük fel például, hogy a személyi osztályunk is eatsFood and enjoysMusic tulajdonságokkal rendelkezik. Természetesen, ez a két dolog az idő nagy részében igaz lenne, de soha nem lehet tudni. Vessen egy pillantást:
most a Person osztálynak négy tulajdonsága van, amelyeket be kell állítani, és van egy kijelölt inicializáló, amely képes elvégezni a munkát. A kijelölt inicializáló az első, amely 4 argumentumot vesz fel. Az idő nagy részében, az utolsó két érv lesz az értéke “igaz”. Ez lenne a fájdalom, hogy folyamatosan meghatározza őket minden alkalommal, amikor azt akarjuk, hogy hozzon létre egy tipikus személy. Itt jön be az utolsó két inicializáló, a kényelmi módosítóval jelölt. Ennek a mintának ismerősnek kell lennie egy Java fejlesztő számára. Ha van olyan konstruktorod, amely több argumentumot vesz igénybe, mint amennyit valójában állandóan foglalkoznod kell, akkor írhatsz egyszerűsített konstruktorokat, amelyek az argumentumok egy részét veszik fel, és ésszerű alapértelmezéseket adnak a többiek számára. A kényelmi inicializálóknak vagy egy másik, talán kevésbé kényelmes kényelmi inicializálóra, vagy egy kijelölt inicializálóra kell delegálniuk. Végül egy kijelölt inicializálónak részt kell vennie. Továbbá, ha ez egy alosztály, a kijelölt inicializálónak meg kell hívnia egy kijelölt inicializálót az azonnali szuperosztályából.
egy valós példa a kényelmi módosító használatára az UIKit UIBezierPath osztályából származik. Biztos vagyok benne, hogy el tudja képzelni, hogy az útvonal megadásának többféle módja van. Mint ilyen, az UIBezierPath számos kényelmi inicializátort kínál:
nyilvános kényelem init(rect: CGRect)
nyilvános kényelem init(ovalInRect rect: CGRect)
nyilvános kényelem init(roundedRect rect: CGRect, cornerRadius: cgfloat)
nyilvános kényelem init(roundedRect rect: CGRect, byRoundingCorners sarkok: uirectcorner, Cornerradii: cgsize)
nyilvános kényelem init(arccenter Center: CGPoint, sugár: CGFloat, startAngle: CGFloat, veszélyeztetés: CGFloat, óramutató járásával megegyező irányban: Bool)
nyilvános kényelem init(CGPath: CGPath)
korábban azt mondtam, hogy egy osztálynak több kijelölt inicializátora lehet. Szóval, hogy néz ki? Az egyik fontos különbség, amelyet a fordító érvényesít a kijelölt inicializálók és a kényelmi inicializálók között, az, hogy a kijelölt inicializálók nem delegálhatnak egy másik inicializálót ugyanabban az osztályban (de delegálniuk kell egy kijelölt inicializálót az azonnali szuperosztályában). Nézd meg a személy második inicializálóját, amely egyetlen “élőhalott”nevű érvet vesz fel. Mivel ez az inicializáló nincs megjelölve a kényelmi módosítóval, a Swift kijelölt inicializálóként kezeli. Mint ilyen, nem ruházhatja át személyesen egy másik inicializálóra. Próbálja meg kommentálni az első négy sort, és kommentálja az utolsó sort. A fordító panaszkodni fog, és az XCode – nak meg kell próbálnia segíteni azzal, hogy javasolja, hogy javítsa ki a kényelmi módosító hozzáadásával.
most vegye figyelembe a személy zenész alosztályát. Egyetlen inicializátorral rendelkezik, ezért kijelölt inicializálónak kell lennie. Mint ilyen, meg kell hívnia az azonnali szuperosztály kijelölt inicializálóját, személy. Ne feledje: míg a kijelölt inicializálók nem delegálhatnak egy másik inicializálót ugyanabba az osztályba, a kényelmi inicializálóknak ezt meg kell tenniük. Ezenkívül a kijelölt inicializálónak meg kell hívnia az azonnali szuperosztály kijelölt inicializálóját. Lásd a nyelvi útmutatót további részletekért (és szép grafika).
inicializálási fázisok
ahogy a Swift nyelvi útmutató elmagyarázza, két inicializálási fázis van. A fázisokat a superclass kijelölt inicializáló hívása határolja el. Az 1. fázis a superclass által kijelölt inicializáló hívása előtt van, a 2.fázis után. Az alosztálynak inicializálnia kell az összes saját tulajdonságát az 1.fázisban, és a 2. fázisig nem állíthat be a szuperosztály által meghatározott tulajdonságokat.
itt van egy kódminta, amelyet a nyelvi útmutatóban megadott mintából adaptáltak, amely megmutatja, hogy inicializálnia kell egy alosztály saját tulajdonságait, mielőtt meghívná a superclass kijelölt inicializálót. A superclass által biztosított tulajdonságokhoz csak a superclass által kijelölt inicializáló meghívása után férhet hozzá. Végül nem módosíthatja az állandó tárolt tulajdonságokat a superclass kijelölt inicializáló meghívása után.
az inicializáló felülírása
most, hogy meg vagyunk győződve arról, hogy az alosztályok általában nem öröklik az inicializálókat, és tisztában vagyunk a kijelölt és a kényelmi inicializálók jelentésével és megkülönböztetésével, nézzük meg, mi történik, ha azt szeretné, hogy egy alosztály felülírja az inicializálót az azonnali szuperosztályából. Négy forgatókönyv van, amelyeket szeretnék lefedni, tekintettel arra, hogy kétféle inicializáló létezik. Tehát vegyük őket egyenként, egyszerű kódpéldákkal minden esetre:
kijelölt inicializáló, amely megfelel egy superclass kijelölt inicializálónak
ez egy tipikus forgatókönyv. Amikor ezt megteszi, alkalmaznia kell a felülbíráló módosítót. Ne feledje, hogy ez a forgatókönyv akkor is érvényes, ha” felülírja ” az automatikusan megadott alapértelmezett inicializálót (azaz amikor a szuperosztály nem határoz meg semmilyen explicit inicializálót. Ebben az esetben a Swift implicit módon nyújt egyet. A Java fejlesztőknek ismerniük kell ezt a viselkedést). Ez az automatikusan megadott alapértelmezett inicializáló mindig egy kijelölt inicializáló.
kijelölt inicializáló, amely megfelel a superclass kényelmi inicializálójának
tegyük fel, hogy hozzá szeretne adni egy kijelölt inicializálót az alosztályához, amely történetesen megfelel a szülő kényelmi inicializálójának. A nyelvi útmutatóban lefektetett inicializáló delegálás szabályai szerint a kijelölt inicializáló alosztálynak a közvetlen szuperosztály kijelölt inicializálójára kell delegálnia. Ez azt jelenti, hogy nem ruházhatja át a szülő megfelelő kényelmi inicializálóját. Az érvelés kedvéért tegyük fel azt is, hogy az alosztály nem jogosult a superclass initializers öröklésére. Ezután, mivel soha nem hozhatsz létre egy példányt az alosztályodból a superclass convenience inicializer közvetlen meghívásával, ez a megfelelő kényelmi inicializáló egyébként sem vesz részt az inicializálási folyamatban. Ezért nem igazán írja felül, és a felülbíráló módosító nem érvényes.
kényelmi inicializáló, amely megfelel egy superclass kijelölt inicializálónak
ebben az esetben képzelje el, hogy van egy alosztálya, amely hozzáadja a saját tulajdonságait, amelyek alapértelmezett értéke kiszámítható (de nem kötelező) az egy vagy több szülőosztály tulajdonságához rendelt értékekből. Tegyük fel, hogy csak egy kijelölt inicializátort szeretne az alosztályához. Hozzáadhat egy kényelmi inicializálót az alosztályához, amelynek aláírása megegyezik a szülő osztály kijelölt inicializálójának aláírásával. Ebben az esetben az új inicializálónak mind a kényelmi, mind a felülbírálási módosítókra szüksége lenne. Itt van egy érvényes kódminta az eset szemléltetésére:
kényelmi inicializáló, amely megfelel a szuperosztály kényelmi inicializálójának
ha olyan kényelmi inicializátort szeretne hozzáadni az alosztályához, amely történetesen megegyezik a szuperosztály kényelmi inicializálójának aláírásával, csak menjen előre. Mint fentebb kifejtettem, egyébként nem igazán lehet felülbírálni a kényelmi inicializálókat. Tehát magában foglalja a kényelmi módosítót, de kihagyja a felülbíráló módosítót, és úgy kezeli, mint bármely más kényelmi inicializátort.
az egyik kulcs elvihető ebből a szakaszból, hogy a felülbíráló módosítót csak akkor használja, és akkor kell használni, ha felülírja a superclass által kijelölt inicializátort. (Kisebb pontosítás itt: ha felülírja a szükséges inicializálót, akkor a szükséges módosítót használja a felülbíráló módosító helyett. A szükséges módosító a felülbíráló módosítót jelenti. Lásd a szükséges Inicializálók részt alább).
amikor az Inicializálók öröklődnek
most a fent említett forgatókönyvekhez, ahol a superclass inicializálók öröklődnek. Ahogy a Swift nyelvi útmutató elmagyarázza, ha az alosztály alapértelmezett értékeket ad meg az összes saját tulajdonságához at deklaráció, és nem határozza meg a saját kijelölt inicializálóit, akkor automatikusan örökli az összes superclass kijelölt inicializálóját. Vagy, ha az alosztály biztosítja az összes superclass kijelölt inicializáló megvalósítását, akkor automatikusan örökli az összes superclass convenience initializert. Ez összhangban van a Swift azon szabályával, hogy az osztályok (és struktúrák) inicializálása nem hagyhatja a tárolt tulajdonságokat határozatlan állapotban.
néhány “érdekes” viselkedésbe botlottam, miközben kísérleteztem a kényelmi inicializálókkal, a kijelölt inicializálókkal és az öröklési szabályokkal. Azt találtuk, hogy lehetséges, hogy beállít egy ördögi kör véletlenül. Tekintsük a következő példát:
a RecipeIngredient osztály felülírja az összes kijelölt Inicializátort, ezért automatikusan örökli az összes superclass kényelmi inicializátort. De az élelmiszer-kényelmi inicializáló ésszerűen átruházza saját kijelölt inicializálójára, amelyet a RecipeIngredient alosztály felülírott. Tehát nem az init(name: String) Inicializer Ételváltozatát hívják meg, hanem a RecipeIngredient felülbírált változatát. A felülírt változat kihasználja azt a tényt, hogy az alosztály örökölte az élelmiszer kényelmét inicializáló, és ott van – van egy ciklus. Nem tudom, hogy ez programozói hibának vagy fordítói hibának tekinthető-e (hibaként jelentettem: https://bugs.swift.org/browse/SR-512 ). Képzeljük el, hogy az étel egy osztály egy 3rd party, és nem férnek hozzá a forráskódot, így nem tudom, hogyan valójában végre. Nem tudhatja (futásidejűig), hogy az ebben a példában bemutatott módon történő használata csapdába esik egy ciklusban. Szerintem jobb lenne, ha a fordító segítene nekünk.
hibás Inicializálók
képzelje el, hogy olyan osztályt tervezett, amely bizonyos invariánsokkal rendelkezik, és ezeket az invariánsokat az osztály egy példányának létrehozásától kezdve érvényesíteni szeretné. Például, lehet, hogy egy számlát modellez, és meg akarja győződni arról, hogy az összeg mindig nem negatív. Ha hozzáadott egy inicializáló, hogy vesz egy összeg argumentum típusú Double, hogyan lehetne, hogy megbizonyosodjon arról, hogy a nem sérti a invariáns? Az egyik stratégia az, hogy egyszerűen ellenőrizze, hogy az érv nem negatív-e. Ha igen, használja. Ellenkező esetben az alapértelmezett érték 0. Például:
ez működne, és elfogadható lehet, ha dokumentálod, hogy mit csinál az inicializáló (különösen, ha azt tervezed, hogy elérhetővé teszed az osztályodat más fejlesztők számára). De lehet, hogy nehezen tudja megvédeni ezt a stratégiát, mivel ez egyfajta szőnyeg alá söpörte a kérdést. A Swift által támogatott másik megközelítés az lenne, ha az inicializálás meghiúsulna. Ez azt jelenti, hogy az inicializáló sikertelen lesz.
mint ez a cikk leírja, a sikertelen inicializálókat hozzáadták a Swifthez, hogy kiküszöböljék vagy csökkentsék a gyári módszerek szükségességét, amelyek “korábban az egyetlen módja voltak a hiba bejelentésének” az objektumépítés során. Ahhoz, hogy egy inicializáló sikertelen, egyszerűen hozzáfűzni a ? vagy ! karakter az init kulcsszó után (azaz init? vagy init! ). Ezután, miután az összes tulajdonság be van állítva, és az összes többi delegálási szabály teljesül, hozzáad néhány logikát az argumentumok érvényességének ellenőrzéséhez. Ha ezek nem érvényesek, inicializálási hibát vált ki nulla visszatéréssel. Ne feledje, hogy ez nem jelenti azt, hogy az inicializáló valaha is visszaad valamit. Így nézhet ki a számla osztályunk egy sikertelen inicializálóval:
észrevesz valami mást abban, hogy hogyan használjuk az objektum létrehozásának eredményét? Olyan, mintha opcionálisan kezelnénk, igaz? Nos, pontosan ezt csináljuk! Ha sikertelen inicializálót használunk, akkor vagy nullát kapunk (ha inicializálási hibát váltottunk ki), vagy opcionális(számla). Vagyis, ha az inicializálás sikeres volt, akkor egy Opcionálisat kapunk, amely becsomagolja az érdeklődő számlap példányt, ezért ki kell csomagolnunk. (Félretéve, vegye figyelembe, hogy a Java-nak is vannak opciói a Java 8-tól).
a sikertelen inicializálók olyanok, mint a többi inicializáló típus, amelyet a felülbírálás és a delegálás, a kijelölt vs kényelem stb.tekintetében tárgyaltunk… valójában még egy sikertelen inicializálót is felülbírálhat egy nem sikertelen inicializálóval. Nem, azonban, felülírhatja a sikertelen inicializálót egy sikertelen inicializálóval.
lehet, hogy észrevette a sikertelen inicializálókat az UIView vagy az UIViewController kezeléséből, amelyek mindkettő sikertelen inicializáló init?(adecoder kódoló: NSCoder). Ez az inicializáló akkor kerül meghívásra, amikor a nézet vagy a ViewController betöltődik egy hegyről. Fontos megérteni, hogy a sikertelen inicializálók hogyan működnek. Erősen ajánlom, hogy olvassa el a Swift nyelvi útmutató sikertelen Inicializálók szakaszát az alapos magyarázatért.
kötelező Inicializálók
a szükséges módosító jelzi, hogy minden alosztálynak végre kell hajtania az érintett inicializálót. Az arca is, hogy úgy hangzik, elég egyszerű és egyértelmű. Időnként kissé zavarossá válhat, ha nem érti, hogyan lépnek életbe a fent tárgyalt inicializáló öröklésre vonatkozó szabályok. Ha egy alosztály megfelel azoknak a kritériumoknak, amelyek alapján a szuperosztály inicializálói öröklődnek, akkor az örökölt inicializálók halmaza tartalmazza azokat, amelyek szükségesek. Ezért az alosztály implicit módon kielégíti a szükséges módosító által előírt szerződést. Végrehajtja a szükséges inicializáló(k) t, csak nem látja a forráskódban.
ha egy alosztály a szükséges inicializáló explicit (azaz nem örökölt) megvalósítását biztosítja, akkor felülbírálja a szuperosztály megvalósítását is. A szükséges módosító felülírást jelent, ezért a felülbíráló módosítót nem használják. Felveheti, ha akarja, de ez felesleges lenne, és az XCode nyaggatni fogja.
a Swift nyelvi útmutató nem sokat mond a szükséges módosítóról, ezért készítettem egy kódmintát (lásd alább) megjegyzésekkel, hogy elmagyarázzam a célját és leírjam, hogyan működik. További információért, lásd ezt a cikket Anthony Levings.
különleges eset: az UIView kiterjesztése
az egyik dolog, ami arra késztetett, hogy mélyen belemerüljek a Swift inicializálókba, az volt, hogy megpróbáltam kitalálni a kijelölt inicializálók készletének létrehozásának módját az inicializálási logika megkettőzése nélkül. Például Ray Wenderlich ezen az UIView oktatóanyagon dolgoztam, az Objective-C kódját Swift-re konvertálva, ahogy mentem (itt megnézheti a Swift verziómat). Ha megnézi ezt az oktatóanyagot, látni fogja, hogy az uiview RateView Alosztályának van egy baseInit metódusa, amelyet mindkét kijelölt inicializáló használ a közös inicializálási feladatok elvégzéséhez. Ez számomra jó megközelítésnek tűnik – nem akarja megismételni ezeket a dolgokat mindegyik inicializálóban. Azt akartam, hogy újra ezt a technikát az én Swift változata RateView. De nehéznek találtam, mert egy kijelölt inicializáló nem delegálhat egy másik inicializálóba ugyanabban az osztályban, és nem hívhatja meg a saját osztályának metódusait, amíg nem delegál a superclass inicializerbe. Ezen a ponton túl késő az állandó tulajdonságok beállításához. Természetesen megkerülheti ezt a korlátozást azáltal, hogy nem használ állandókat, de ez nem jó megoldás. Tehát úgy gondoltam, hogy a legjobb, ha csak alapértelmezett értékeket adunk meg a tárolt tulajdonságokhoz, ahol deklarálják őket. Ez még mindig a legjobb megoldás, amit jelenleg tudok. Azonban kitaláltam egy alternatív technikát, amely inicializátorokat használ.
nézze meg a következő példát. Ez egy részlet a Rateviewithconvenienceinits – ből.swift, amely egy alternatív változata az én Swift port RateView. Mivel az UIView alosztálya, amely nem ad alapértelmezett értékeket az összes saját tárolt tulajdonságához a deklarációkor, a RateView ezen alternatív verziójának legalább az UIView szükséges init explicit megvalósítását kell biztosítania?(kódoló aDecoder: NSCoder) inicializáló. Azt is szeretnénk, hogy egy explicit végrehajtása UIView a init (frame: CGRect) inicializáló, hogy megbizonyosodjon arról, hogy az inicializálási folyamat következetes. Azt akarjuk, hogy tárolt tulajdonságainkat ugyanúgy állítsuk be, függetlenül attól, hogy melyik inicializálót használjuk.
vegye figyelembe, hogy hozzáadtam a kényelmi módosítót az UIView inicializátorainak felülbírált verzióihoz. Hozzáadtam egy sikertelen kijelölt inicializátort is az alosztályhoz, amelyet mindkét felülbírált (kényelmi) inicializáló delegál. Ez az egyetlen kijelölt inicializáló gondoskodik az összes tárolt tulajdonság beállításáról (beleértve az állandókat is – nem kellett mindent a var használatához folyamodnom). Működik, de szerintem elég kludgy. Inkább csak alapértelmezett értékeket adnék meg a tárolt tulajdonságaimhoz, ahol deklarálják őket, de jó tudni, hogy ez a lehetőség létezik, ha szükséges.