V tomto článku, budu prozkoumat některé podrobnosti Swift inicializátory. Proč se na takové vzrušující téma? Hlavně proto, že jsem se občas ocitl zmatený chybami kompilace při pokusu o vytvoření podtřídy UIView nebo při pokusu o vytvoření Swift verze staré třídy Objective-C, například. Rozhodl jsem se tedy kopat hlouběji, abych lépe porozuměl nuancím Inicializátorů Swift. Od mého pozadí/zkušenosti se většinou podílejí práci s Groovy, Java a Javascript, budu porovnávat některé Swift inicializaci mechanismů pro ty, Java & Groovy. Doporučuji vám vypálit hřiště v XCode a hrát si s příklady kódu sami. Doporučuji vám také přečíst si Inicializační část jazykové příručky Swift 2.1, která je primárním zdrojem informací pro tento článek. Nakonec se zaměřím spíše na referenční typy (třídy) než na hodnotové typy (struktury).
Co Je Inicializátor?
inicializátor je speciální typ metody v Swift struct, class, nebo enum, který je zodpovědný za výrobu, zda nově vytvořené instance struct, class, nebo enum je plně inicializován před tím, než jsou připraveny k použití. Hrají stejnou roli jako“ konstruktor “ v Javě a Groovy. Pokud jste obeznámeni s Objective-C, měli byste si uvědomit, že Swift inicializátory liší od Objective-C inicializátory v tom, že nevrací hodnotu.
Podtřídy Obecně nezdědili Inicializátory
Jedna z prvních věcí, mít na paměti je, že „Swift podtřídy nedědí jejich nadtřídy inicializátory ve výchozím nastavení“, v závislosti na jazyk průvodce. (Průvodce vysvětluje, že existují scénáře, kdy jsou inicializátory supertřídy automaticky zděděny. Tyto výjimečné scénáře pokryjeme později). To je v souladu s tím, jak Java (a rozšířením, Groovy) funguje. Zvažte následující:
stejně jako u Java & Groovy, dává smysl, že to není povoleno (i když, stejně jako u většiny věcí, v tomto bodě může existovat nějaký argument. Viz tento příspěvek StackOverflow). Pokud by to bylo povoleno, inicializace vlastnosti“ nástroje “ hudebníka by byla vynechána, potenciálně ponechání instance hudebníka v neplatném stavu. S Groovy bych se však normálně neobtěžoval psát inicializátory (tj. Spíše bych použil implicitně Konstruktor map Groovy, který vám umožní svobodně vybrat a vybrat, které vlastnosti chcete nastavit při stavbě. Například, následující je dokonale platný Groovy kód:
Všimněte si, že můžete zahrnout jakýkoli majetek, včetně těch poskytnutých superclasses, ale nemusíte zadat všechny (nebo žádné), a nemusí být uvedeny v žádném konkrétním pořadí. Tento druh ultra-flexibilního inicializátoru není poskytován společností Swift. Nejbližší věcí ve Swiftu jsou automaticky poskytované inicializátory memberwise pro structs. Ale pořadí argumentů v memberwise inicializátoru je významný, i když jsou pojmenovány, a závisí na pořadí, ve kterém jsou definovány:
Každopádně, zpátky do třídy – Groovy filozofie týkající se post-stavební objekt platnost je zjevně velmi odlišné od Swift. To je jen jeden z mnoha způsobů, v němž Groovy liší od Swift.
určené inicializátory vs Convenience
než se dostaneme příliš daleko do králičí díry, měli bychom objasnit důležitý koncept: Ve Swiftu je inicializátor kategorizován jako určený nebo pohodlný inicializátor. Pokusím se vysvětlit rozdíl koncepčně i syntakticky. Každá třída musí mít alespoň jeden určený inicializátor, ale může mít několik („určený“ neznamená „jediný“). Určený inicializátor je považován za primární inicializátor. Jsou to hlavolamy. Jsou nakonec zodpovědní za to, že všechny vlastnosti jsou inicializovány. Kvůli této odpovědnosti, někdy se může stát bolestí používat určený inicializátor po celou dobu, protože mohou vyžadovat několik argumentů. Představte si práci s třídou, která má několik vlastností, a musíte vytvořit několik instancí této třídy, které jsou téměř identické, s výjimkou jedné nebo dvou vlastností. (Pro argument, předpokládejme také, že neexistují žádné rozumné výchozí hodnoty, které by mohly být přiřazeny k vlastnostem, když jsou deklarovány). Řekněme například, že naše třída osob měla také jídlo a užívala si hudební vlastnosti. Samozřejmě, tyto dvě věci by byly většinou nastaveny na pravdu, ale nikdy nevíte. Podívejte se:
nyní má naše třída osob čtyři vlastnosti, které je třeba nastavit, a máme určený inicializátor, který může tuto práci provést. Určený inicializátor je ten první, ten, který bere 4 argumenty. Většinu času, tyto poslední dva argumenty budou mít hodnotu „true“. Byla by to bolest, kdybychom je museli specifikovat pokaždé, když chceme vytvořit typického člověka. To je místo, kde přicházejí poslední dva inicializátory, ty označené modifikátorem pohodlí. Tento vzor by měl vypadat dobře pro vývojáře Java. Pokud máte konstruktor, který vyžaduje více argumentů, než je skutečně nutné řešit po celou dobu, můžete napsat zjednodušené konstruktory, které berou podmnožinu těchto argumentů a poskytují rozumné výchozí hodnoty pro ostatní. Inicializátory pohodlí musí delegovat buď na jiný, možná méně pohodlný, inicializátor pohodlí, nebo na určený inicializátor. Nakonec se musí zapojit určený inicializátor. Dále, pokud se jedná o podtřídu, určený inicializátor musí zavolat určený inicializátor z jeho okamžité supertřídy.
jeden skutečný příklad pro použití modifikátoru pohodlí pochází z třídy UIKit UIBezierPath. Jsem si jistý, že si dokážete představit, že existuje několik způsobů, jak určit cestu. Jako takový, UIBezierPath nabízí několik pohodlí inicializátory:
veřejné pohodlí init(rect: CGRect)
veřejné pohodlí init(ovalInRect rect: CGRect)
veřejné pohodlí init(roundedRect rect: CGRect, cornerRadius: CGFloat)
veřejné pohodlí init(roundedRect rect: CGRect, byRoundingCorners rohy: UIRectCorner, cornerRadii: CGSize)
veřejné pohodlí init(arcCenter centrum: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, ve směru hodinových ručiček: Bool)
veřejné pohodlí init(CGPath: CGPath)
Dříve jsem řekl, že třída může mít více určených inicializátory. Tak jak to vypadá? Jeden důležitý rozdíl, vynuceno kompilátor, mezi určených inicializátory a pohodlí inicializátory je, že v určených inicializátory nesmí přenést na další inicializátor ve stejné třídě (ale musí se přenést na určené inicializátoru v bezprostřední nadtřídy). Podívejte se na druhý inicializátor osoby, ten, který bere jediný argument s názvem „nemrtví“. Vzhledem k tomu, že tento inicializátor není označen modifikátorem pohodlí, Swift jej považuje za určený inicializátor. Jako takový, nesmí osobně delegovat na jiného inicializátora. Zkuste komentovat první čtyři řádky a spojit poslední řádek. Kompilátor si bude stěžovat a XCode by se vám měl pokusit pomoci tím, že navrhne, abyste jej opravili přidáním modifikátoru pohodlí.
nyní zvažte podtřídu hudebníka. Má jediný inicializátor, a proto musí být určený inicializátor. Jako takový, musí zavolat určeného inicializátora okamžité supertřídy, osoba. Pamatujte: zatímco určené inicializátory nemohou delegovat na jiný inicializátor stejné třídy, inicializátory pohodlí tak musí učinit. Určený inicializátor musí také zavolat určenému inicializátoru okamžité supertřídy. Další podrobnosti (a pěknou grafiku) najdete v jazykové příručce.
Fáze inicializace
jak vysvětluje jazyková příručka Swift, existují dvě fáze inicializace. Fáze jsou vymezeny voláním na inicializátor označený supertřídou. Fáze 1 je před voláním na inicializátor označený supertřídou, fáze 2 je po. Podtřída musí inicializovat všechny své vlastní vlastnosti ve fázi 1 a nesmí nastavit žádné vlastnosti definované supertřídou až do fáze 2.
Zde je ukázka kódu, adaptovaný od vzorku poskytnuty v jazyce, průvodce, ukazující, že musíte inicializovat VLASTNÍ vlastnosti podtřídy před vyvolání nadřazené třídy určené inicializátor. K vlastnostem poskytovaným supertřídou můžete přistupovat až po vyvolání inicializátoru určeného supertřídou. Konečně, po vyvolání inicializátoru označeného superclass nesmíte měnit konstantní uložené vlastnosti.
Převažujícího Inicializátor
Teď, že jsme přesvědčeni, že podtřídy obecně nezdědili inicializátory, a máme jasno o smyslu a rozlišení mezi určenými a pohodlí inicializátory, pojďme zvážit, co se stane, když chcete, podtřídy přepsat inicializátor z bezprostřední nadtřídy. Existují čtyři scénáře, které bych chtěl pokrýt, vzhledem k tomu, že existují dva typy inicializátoru. Takže pojďme si je jeden po druhém, s jednoduchými příklady kódu pro každý případ:
určené inicializátor, která odpovídá nadřazené třídě určené inicializátor
To je typický scénář. Když to uděláte, musíte použít modifikátor přepsání. Všimněte si, že tento scénář je v podstatě, i když jste „převažující“, automaticky za předpokladu, default inicializátoru (tj., když nadtřída nedefinuje žádné explicitní inicializátor. V tomto případě Swift implicitně poskytuje jeden. Vývojáři Java by měli být s tímto chováním obeznámeni). Tento automaticky poskytovaný výchozí inicializátor je vždy určený inicializátor.
určené inicializátor, který odpovídá nadtřídy pohodlí inicializátor
Nyní předpokládejme, že chcete přidat určených inicializátor na své podtřídy, které se stane, aby odpovídaly pohodlí inicializátoru v mateřské. Podle pravidel delegování inicializátoru stanovených v jazykové příručce musí váš inicializátor určený podtřídou delegovat na určeného inicializátora okamžité supertřídy. To znamená, že nesmíte delegovat na inicializátor pohodlí rodiče. Pro zajímavost, také předpokládejme, že podtřídy nemá nárok dědit nadtřídy inicializátory. Pak, protože jste nikdy nemohl vytvořit instanci své podtřídy přímo vyvolání nadřazené pohodlí inicializátor, že odpovídající pohodlí inicializátor není, a nikdy nemohl být, zapojeni do procesu inicializace stejně. Proto to opravdu nepřekonáte a modifikátor přepsání se nepoužije.
pohodlí inicializátor, která odpovídá nadřazené třídě určené inicializátor
V tomto scénáři, představte si, že máte podtřídy, že přidá své vlastní vlastnosti, jejichž výchozí hodnota může být (ale nemusí být) vypočítaný z hodnot přiřazených k jednomu nebo více rodič-třídní vlastnosti. Předpokládejme, že také chcete mít pouze jeden určený inicializátor pro vaši podtřídu. Do podtřídy můžete přidat inicializátor pohodlí, jehož podpis odpovídá podpisu určeného inicializátoru nadřazené třídy. V tomto případě by váš nový inicializátor potřeboval jak modifikátory pohodlí, tak přepsat. Tady je platný ukázka kódu pro ilustraci tomto případě:
pohodlí inicializátor, který odpovídá nadtřídy pohodlí inicializátor
Pokud chcete přidat pohodlí inicializátor na své podtřídy, které se stane, aby odpovídaly podpis pohodlí inicializátor své nadtřídy, jen do toho. Jak jsem vysvětlil výše, stejně nemůžete přepsat inicializátory pohodlí. Takže bys patří pohodlí, modifikátor, ale vynechat override modifikátor, a zacházet s ní jako každý jiný pohodlí inicializátor.
Jeden klíč stánek s jídlem z této sekce je, že override modifikátor se používá pouze, a musí být použity, pokud jsou nadřazené nadřazené třídě určené inicializátor. (Drobné objasnění zde: pokud přepisujete požadovaný inicializátor, pak byste místo modifikátoru přepsání použili požadovaný modifikátor. Požadovaný modifikátor znamená modifikátor přepsání. Viz část požadované inicializátory níže).
když jsou zděděny inicializátory
nyní pro výše uvedené scénáře, kde jsou zděděny inicializátory superclass. Jako Swift Jazyk Průvodce vysvětluje, pokud je váš podtřídy poskytuje výchozí hodnoty pro všechny vlastní vlastnosti v prohlášení, a nedefinuje žádné vlastní určených inicializátory, pak to bude automaticky zdědí všechny její nadtřídy určených inicializátory. Nebo, pokud vaše podtřída poskytuje implementaci všech nadtřídy určených inicializátory, pak automaticky zdědí všechny z nadtřídy pohodlí inicializátory. To je v souladu s pravidlem Swift, že inicializace tříd (a struktur) nesmí ponechat uložené vlastnosti v neurčitém stavu.
narazil jsem na nějaké „zajímavé“ chování při experimentování s inicializátory pohodlí, určenými inicializátory a pravidly dědičnosti. Zjistil jsem, že je možné neúmyslně nastavit začarovaný kruh. Zvažte následující příklad:
RecipeIngredient třída přepíše všechny Potraviny třídy určených inicializátory, a proto to automaticky zdědí všechny z nadtřídy pohodlí inicializátory. Inicializátor pohodlí jídla však rozumně deleguje na svůj vlastní určený inicializátor,který byl přepsán podtřídou RecipeIngredient. Není tedy vyvolána potravinová verze init (name: String) inicializátoru, ale přepsaná verze v Recipeingredientu. Přepsaná verze využívá skutečnosti, že podtřída zdědila inicializátor pohodlí jídla, a tady to je – máte cyklus. Nevím, jestli by to bylo považováno za chybu programátora nebo chybu kompilátoru (nahlásil jsem to jako chybu: https://bugs.swift.org/browse/SR-512). Představte si, že jídlo je třída od 3. strany a nemáte přístup ke zdrojovému kódu, takže nevíte, jak je skutečně implementováno. Nevěděli byste (až do běhu), že jeho použití způsobem uvedeným v tomto příkladu by vás uvěznilo v cyklu. Takže si myslím, že by bylo lepší, kdyby nám kompilátor pomohl tady.
neúspěšné inicializátory
Představte si, že jste navrhli třídu, která má určité invarianty, a chtěli byste tyto invarianty vynutit od okamžiku vytvoření instance třídy. Například možná modelujete fakturu a chcete se ujistit, že částka je vždy nezáporná. Pokud jste přidali inicializátor, který bere argument množství typu Double, jak byste se mohli ujistit, že neporušujete invariant? Jednou strategií je jednoduše zkontrolovat, zda argument není negativní. Pokud ano, použijte ji. V opačném případě výchozí hodnota 0. Například:
to by fungovalo a může být přijatelné, pokud dokumentujete, co váš inicializátor dělá (zejména pokud plánujete zpřístupnit svou třídu jiným vývojářům). Ale možná budete mít těžké bránit tuto strategii, protože to trochu zametá problém pod koberec. Dalším přístupem, který Swift podporuje, by bylo nechat inicializaci selhat. To znamená, že by váš inicializátor selhal.
Jako tento článek popisuje, failable inicializátory byly přidány do Swift jako způsob, jak eliminovat nebo snížit potřebu tovární metody“, které byly dříve jediný způsob, jak hlásit poruchy“ během výstavby objektu. Chcete-li, aby inicializátor selhal, jednoduše připojíte ? nebo ! znak za klíčovým slovem init(tj. nebo init! ). Poté, co byly nastaveny všechny vlastnosti a všechna ostatní pravidla týkající se delegování byla splněna, přidáte nějakou logiku, abyste ověřili, že argumenty jsou platné. Pokud nejsou platné, spustíte selhání inicializace s návratovou nulou. Všimněte si, že to neznamená, že inicializátor někdy něco vrací. Zde je návod, jak by naše třída faktur mohla vypadat s neúspěšným inicializátorem:
Všimněte si něčeho jiného o tom, jak používáme výsledek vytvoření objektu? Jako bychom to brali jako volitelné, že? No, to je přesně to, co děláme! Když použijeme neúspěšný inicializátor, dostaneme buď nulu (pokud došlo k selhání inicializace) nebo volitelnou (fakturu). To znamená, že pokud byla inicializace úspěšná, skončíme s volitelným, který zabalí instanci faktury, o kterou máme zájem, takže ji musíme rozbalit. (Jako stranou, všimněte si, že Java má také volby od Java 8).
Failable inicializátory jsou stejně jako jiné typy inicializátory jsme diskutovali s ohledem na převažující a delegace, určený vs pohodlí, atd… Ve skutečnosti, můžete dokonce přepsat failable inicializátor s nonfailable inicializátor. Nemůžete však přepsat nedostupný inicializátor s neúspěšným.
možná jste si všimli neúspěšných inicializátorů z jednání s UIView nebo UIViewController, které oba poskytují neúspěšný inicializátor init?(kodér aDecoder: NSCoder). Tento inicializátor se volá, když je váš pohled nebo ViewController načten z hrotu. Je důležité pochopit, jak selhávají inicializátory. Důrazně doporučujeme, abyste si pro důkladné vysvětlení přečetli část Inicializátorů, která selhala v Průvodci jazykem Swift.
Požadované Inicializátory
požadované modifikátor se používá k označení, že všechny podtřídy musí implementovat postižené inicializátor. Na první pohled to zní docela jednoduše a přímočaře. Někdy to může být trochu matoucí, pokud nerozumíte tomu, jak vstoupí do hry pravidla týkající se dědičnosti inicializátoru diskutovaná výše. Pokud podtřída splňuje kritéria, podle kterých jsou zděděny inicializátory supertřídy, pak sada zděděných inicializátorů zahrnuje ty, které jsou označeny jako povinné. Podtřída proto implicitně splňuje smlouvu uloženou požadovaným modifikátorem. Implementuje požadované inicializátory,prostě je nevidíte ve zdrojovém kódu.
Pokud podtřídy poskytuje explicitní (tj. nedědí) provádění požadované inicializátor, pak je také přepsání nadtřídy provádění. Požadovaný modifikátor znamená přepsání, takže modifikátor přepsání není použit. Můžete jej zahrnout, pokud chcete, ale bylo by to nadbytečné a XCode vás o tom bude otravovat.
jazyková příručka Swift neříká mnoho o požadovaném modifikátoru, proto jsem připravil ukázku kódu (viz níže) s komentáři, abych vysvětlil jeho účel a popsal, jak to funguje. Pro více informací, viz tento článek Anthony Levings.
Zvláštní Případ: Rozšíření UIView
Jedna z věcí, která mě přiměla sáhnout hluboko do Swift inicializátory byl můj pokus se přijít na způsob, jak vytvořit sadu určených inicializátory bez duplikování inicializační logiku. Například jsem pracoval přes tento výukový program UIView Ray Wenderlich, převod jeho Objective-C kód do Swift, když jsem šel (můžete se podívat na můj Swift verze zde). Pokud se podíváte na tento tutoriál, uvidíte, že jeho podtřída RateView UIView má metodu baseInit, kterou oba určené inicializátory používají k provádění běžných inicializačních úkolů. To se mi zdá jako dobrý přístup – nechcete duplikovat tyto věci v každém z těchto inicializátorů. Chtěl jsem tuto techniku znovu vytvořit ve své rychlé verzi Ratview. Ale našel jsem to těžké, protože určeného inicializátor nemůže delegovat na jiný inicializátor ve stejné třídě a nelze volat metody vlastní třídy až po to delegáti inicializátor nadtřídy. V tu chvíli je příliš pozdě na Nastavení konstantních vlastností. Toto omezení byste samozřejmě mohli obejít tím, že nepoužíváte konstanty, ale to není dobré řešení. Tak jsem si myslel, že to bylo nejlepší jen poskytnout výchozí hodnoty pro uložené vlastnosti, kde jsou deklarovány. To je stále nejlepší řešení, o kterém v současné době vím. Nicméně jsem přišel na alternativní techniku, která používá inicializátory.
podívejte se na následující příklad. Toto je úryvek z RateViewWithConvenienceInits.swift, což je alternativní verze mého Swift portu RateView. Být podtřídy UIView, který neposkytuje výchozí hodnoty pro všechny je to vlastní uložené vlastnosti v prohlášení, tento alternativní verze RateView musí alespoň stanovit explicitní provádění UIView je nutné init?(kodér aDecoder: NSCoder) inicializátor. Budeme také chtít poskytnout explicitní implementaci UIView init(frame: CGRect) inicializátor, aby se ujistil, že proces inicializace je konzistentní. Chceme, aby naše uložené vlastnosti byly nastaveny stejným způsobem bez ohledu na to, který inicializátor se používá.
Všimněte si, že jsem přidal modifikátor pohodlí K přepsaným verzím inicializátorů UIView. Do podtřídy jsem také přidal neúspěšný určený inicializátor, na který delegují oba přepsané (pohodlné) inicializátory. Tento jediný určený inicializátor se stará o nastavení všech uložených vlastností (včetně konstant-nemusel jsem se uchýlit k použití var pro všechno). Funguje to, ale myslím, že je to docela kludgy. Raději bych jen poskytnout výchozí hodnoty pro mé uložené vlastnosti, kde jsou deklarovány, ale je dobré vědět, že tato možnost existuje v případě potřeby.