Eksploracja inicjatorów Swift / partnerów obiektu

w tym artykule omówię niektóre z drobiazgowych szczegółów inicjatorów Swift. Po co podejmować tak ekscytujący temat? Głównie dlatego, że czasami byłem zdezorientowany błędami kompilacji, gdy próbowałem utworzyć podklasę UIView, lub gdy próbowałem stworzyć szybką wersję starej klasy Objective-C, na przykład. Postanowiłem więc kopać głębiej, aby lepiej zrozumieć niuanse szybkich inicjatorów. Ponieważ moje doświadczenie/doświadczenie dotyczy głównie pracy z Groovy, Java i Javascript, porównam niektóre mechanizmy inicjalizacji Swift do tych z Java & Groovy. Zachęcam do odpalenia placu zabaw w XCode i samodzielnej zabawy przykładami kodu. Zachęcam również do zapoznania się z sekcją inicjalizacji przewodnika językowego Swift 2.1, która jest podstawowym źródłem informacji dla tego artykułu. Na koniec skupię się na typach referencyjnych (klasach), a nie na typach wartości (strukturach).

Co To Jest Inicjalizator?

inicjalizator jest specjalnym typem metody w Swift struct, class lub enum, która jest odpowiedzialna za upewnienie się, że nowo utworzona instancja struct, class lub enum jest w pełni zainicjalizowana, zanim będą gotowe do użycia. Odgrywają one taką samą rolę jak” konstruktor ” w Javie i Groovy. Jeśli znasz Objective-C, powinieneś zauważyć, że inicjalizatory Swift różnią się od inicjalizatorów Objective-C tym, że nie zwracają wartości.

podklasy generalnie nie dziedziczą Inicjalizatorów

jedną z pierwszych rzeczy, o których należy pamiętać, jest to, że „podklasy Swift domyślnie nie dziedziczą swoich inicjalizatorów superklasy”, zgodnie z przewodnikiem po języku. (Przewodnik wyjaśnia, że istnieją scenariusze, w których inicjalizatory klasy superklasowej są automatycznie dziedziczone. Te wyjątkowe scenariusze omówimy później). Jest to zgodne z tym, jak działa Java (a przez to Groovy). Rozważ następujące kwestie:

podobnie jak w przypadku Javy & Groovy, ma sens, że nie jest to dozwolone (chociaż, jak w przypadku większości rzeczy, może być jakiś argument na ten temat. Zobacz ten post StackOverflow). Gdyby było to dozwolone, inicjalizacja właściwości” instrument ” muzyka zostałaby pominięta, potencjalnie pozostawiając instancję muzyka w stanie nieważnym. Jednak z Groovy normalnie nie zawracałbym sobie głowy pisaniem inicjalizatorów (tj. konstruktorów). Zamiast tego użyłbym konstruktora map, który zapewnia GROOVY pośrednio, co pozwala na swobodne wybieranie i wybieranie właściwości, które chcesz ustawić podczas budowy. Na przykład, poniższy kod Groovy jest całkowicie poprawny:

zauważ, że możesz dołączyć dowolną właściwość, w tym te dostarczane przez superklasy, ale nie musisz określać wszystkich (ANI ŻADNYCH) z nich i nie muszą być określone w określonej kolejności. Ten rodzaj ultra-elastycznego inicjatora nie jest dostarczany przez Swift. Najbliższą rzeczą w języku Swift są automatycznie dostarczane inicjalizatory memberwise dla struktur. Ale kolejność argumentów w inicjalizatorze memberwise jest znacząca, mimo że są nazwane i zależy od kolejności, w jakiej są zdefiniowane:
wracając do klas – Filozofia Groovy dotycząca ważności obiektów po budowie wyraźnie różni się od Swifta. jest to tylko jeden z wielu sposobów, w jaki Groovy różni się od Swifta.

wyznaczeni a wygodni inicjatorzy

zanim zejdziemy zbyt daleko w dół króliczej nory, powinniśmy wyjaśnić ważną koncepcję: W języku Swift inicjator jest klasyfikowany jako wyznaczony lub wygodny inicjator. Postaram się wyjaśnić różnicę zarówno koncepcyjnie, jak i składniowo. Każda klasa musi mieć co najmniej jeden wyznaczony inicjator, ale może mieć kilka („wyznaczony „nie oznacza”pojedynczy”). Wyznaczony inicjalizator jest uważany za inicjalizator główny. To główni honchos. Są one ostatecznie odpowiedzialne za upewnienie się, że wszystkie właściwości są inicjalizowane. Z powodu tej odpowiedzialności, używanie przez cały czas wyznaczonego inicjatora może okazać się uciążliwe, ponieważ mogą one wymagać kilku argumentów. Wyobraź sobie pracę z klasą, która ma kilka właściwości i musisz utworzyć kilka instancji tej klasy, które są prawie identyczne, z wyjątkiem jednej lub dwóch właściwości. (Ze względu na argument, Załóżmy również, że nie ma sensownych wartości domyślnych, które mogłyby zostać przypisane do właściwości, gdy są zadeklarowane). Na przykład, powiedzmy, że nasza klasa Person również miała jedzenie i przyjemność właściwości muzyczne. Oczywiście, te dwie rzeczy byłyby ustawione na prawdziwe przez większość czasu, ale nigdy nie wiadomo. Spójrz:

teraz Nasza klasa Person ma cztery właściwości, które muszą być ustawione, i mamy wyznaczony inicjalizator, który może wykonać zadanie. Wskazanym inicjalizatorem jest ten pierwszy, który przyjmuje 4 argumenty. Przez większość czasu te dwa ostatnie argumenty będą miały wartość „true”. To byłoby bolesne, musieć je określać za każdym razem, gdy chcemy stworzyć typową osobę. Tam wchodzą dwa ostatnie inicjalizatory, te oznaczone modyfikatorem wygody. Ten wzorzec powinien wyglądać znajomo dla programisty Java. Jeśli masz konstruktora, który pobiera więcej argumentów,niż naprawdę potrzebujesz przez cały czas, możesz napisać uproszczone konstruktory, które biorą podzbiór tych argumentów i dostarczają rozsądnych wartości domyślnych dla pozostałych. Inicjatory convenience muszą delegować do innego, być może mniej wygodnego, inicjatora convenience lub do wyznaczonego inicjatora. Ostatecznie wyznaczony inicjator musi się zaangażować. Ponadto, jeśli jest to podklasa, wyznaczony inicjator musi wywołać wyznaczony inicjator z jego natychmiastowej superklasy.

jeden rzeczywisty przykład użycia modyfikatora wygody pochodzi z klasy UIKit UIBezierPath. Jestem pewien, że można sobie wyobrazić, że istnieje kilka sposobów określenia ścieżki. W związku z tym UIBezierPath zapewnia kilka wygodnych inicjalizacji:

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, endAngle: CGFloat, clockwise: Bool)
Public convenience INIT(CGPath: CGPath)

wcześniej powiedziałem, że klasa może mieć wiele wyznaczonych inicjalizatorów. Więc jak to wygląda? Jedną z istotnych różnic, wymuszonych przez kompilator, pomiędzy wyznaczonymi inicjatorami a wygodnymi inicjatorami jest to, że wyznaczone inicjatory nie mogą delegować do innego inicjatora w tej samej klasie (ale muszą delegować do wyznaczonego inicjatora w jego natychmiastowej klasie nadrzędnej). Spójrz na drugi inicjator osoby, ten, który przyjmuje pojedynczy argument o nazwie „nieumarli”. Ponieważ ten inicjalizator nie jest oznaczony modyfikatorem wygody, Swift traktuje go jako wyznaczony inicjalizator. W związku z tym nie może delegować osobiście do innego inicjatora. Spróbuj skomentować pierwsze cztery wiersze i skomentować ostatnią linię. Kompilator będzie narzekał, a XCode powinien spróbować Ci pomóc, sugerując, że naprawisz to, dodając modyfikator wygody.

teraz rozważ podklasę Muzyka osoby. Ma jeden inicjalizator, a zatem musi być wyznaczonym inicjalizatorem. Jako taki, musi wywołać wyznaczony inicjator natychmiastowej superklasy, Person. Pamiętaj: podczas gdy wyznaczone inicjalizatory nie mogą delegować do innego inicjalizatora tej samej klasy, inicjalizatory wygody muszą to zrobić. Ponadto wyznaczony inicjator musi wywołać wyznaczony inicjator jego natychmiastowej superklasy. Zobacz Przewodnik po języku, aby uzyskać więcej szczegółów (i ładną grafikę).

fazy inicjalizacji

jak wyjaśnia Przewodnik po języku Swift, istnieją dwie fazy inicjalizacji. Fazy są rozgraniczane przez wywołanie do nadklasy wyznaczonego inicjatora. Faza 1 jest przed wywołaniem do nadklasy wyznaczonego inicjatora, Faza 2 jest po. Podklasa musi inicjalizować wszystkie własne właściwości w fazie 1 i nie może ustawiać żadnych właściwości zdefiniowanych przez superklasę aż do fazy 2.
oto próbka kodu, zaadaptowana z próbki dostarczonej w Przewodniku po języku, pokazująca, że musisz zainicjować własne właściwości podklasy przed wywołaniem nadklasy wyznaczonego inicjatora. Nie można uzyskać dostępu do właściwości dostarczanych przez klasę nadrzędną, dopóki nie zostanie wywołany wyznaczony inicjator klasy nadrzędnej. Wreszcie, nie można modyfikować stałych właściwości przechowywanych po wywołaniu nadrzędnej klasy wyznaczonego inicjalizatora.

nadpisywanie inicjatora

teraz, gdy jesteśmy przekonani, że podklasy generalnie nie dziedziczą inicjatorów, i mamy jasne znaczenie i rozróżnienie między wyznaczonymi i wygodnymi inicjatorami, zastanówmy się, co się dzieje, gdy chcesz, aby podklasa nadpisała inicjalizator z jego natychmiastowej superklasy. Są cztery scenariusze, które chciałbym omówić, biorąc pod uwagę, że istnieją dwa rodzaje inicjalizacji. Weźmy je jeden po drugim, z prostymi przykładami kodu dla każdego przypadku:

wyznaczony inicjator, który pasuje do nadklasy oznaczonej inicjatorem
jest to typowy scenariusz. Gdy to zrobisz, musisz zastosować modyfikator override. Zauważ, że ten scenariusz działa nawet wtedy, gdy „nadpisujesz” automatycznie dostarczany domyślny inicjalizator (tj. gdy Klasa nadrzędna nie definiuje żadnego jawnego inicjalizatora. W tym przypadku Swift podaje je w sposób niejawny. Programiści Java powinni być zaznajomieni z tym zachowaniem). Ten automatycznie dostarczany domyślny inicjator jest zawsze wyznaczonym inicjatorem.
wyznaczony initializer, który pasuje do superklasy initializer wygody
Załóżmy teraz, że chcesz dodać wyznaczony initializer do swojej podklasy, który tak się składa, że pasuje do initializera wygody w rodzicu. Zgodnie z zasadami delegowania inicjatora określonymi w przewodniku językowym, podklasa wyznaczona przez inicjatora musi zostać delegowana do wyznaczonego inicjatora natychmiastowej klasy nadrzędnej. Oznacza to, że nie możesz delegować do inicjalizatora wygody dopasowania rodzica. Dla dobra argumentu, Załóżmy również, że podklasa nie kwalifikuje się do dziedziczenia inicjalizatorów superklasy. Następnie, ponieważ nigdy nie można utworzyć instancji swojej podklasy poprzez bezpośrednie wywołanie superklasy initializer convenience, ten pasujący initializer convenience i tak nie jest i nigdy nie może być zaangażowany w proces inicjalizacji. Dlatego tak naprawdę nie nadpisujesz go, a modyfikator nadpisania nie ma zastosowania.

wygodny inicjalizator, który pasuje do nadrzędnej klasy oznaczonej inicjalizatorem
w tym scenariuszu wyobraź sobie, że masz podklasę, która dodaje własne właściwości, których wartość domyślna może być (ale nie musi być) obliczona z wartości przypisanych do jednej lub więcej właściwości klasy nadrzędnej. Załóżmy, że chcesz mieć tylko jeden wyznaczony inicjator dla swojej podklasy. Możesz dodać wygodny inicjator do swojej podklasy, którego podpis pasuje do podpisu wyznaczonego inicjatora klasy nadrzędnej. W takim przypadku Nowy inicjalizator będzie wymagał zarówno modyfikatorów wygody, jak i nadpisywania. Oto poprawna próbka kodu, aby zilustrować ten przypadek:
inicjalizator wygody, który pasuje do inicjalizatora wygody superklasy
jeśli chcesz dodać inicjalizator wygody do swojej podklasy, który pasuje do podpisu inicjalizatora wygody Twojej superklasy, po prostu śmiało. Jak wyjaśniłem powyżej, tak naprawdę nie można zastąpić inicjalizatorów wygody. Dodasz więc modyfikator wygody, ale pominiesz modyfikator nadpisania i potraktujesz go jak każdy inny inicjalizator wygody.

jednym z kluczowych elementów tej sekcji jest to, że modyfikator override jest używany tylko i musi być użyty, jeśli nadpisujesz superklasę oznaczoną inicjalizatorem. (Drobne Wyjaśnienie: jeśli nadpisujesz wymagany inicjalizator, wtedy użyjesz wymaganego modyfikatora zamiast modyfikatora nadpisania. Wymagany modyfikator implikuje modyfikator override. Patrz sekcja wymagane Inicjalizatory poniżej).

gdy Inicjalizatory są dziedziczone

teraz dla wyżej wymienionych scenariuszy, w których dziedziczone są inicjalizatory klasy nadrzędnej. Jak wyjaśnia Przewodnik po języku Swift, jeśli twoja podklasa podaje wartości domyślne dla wszystkich własnych właściwości w deklaracji i nie definiuje żadnego z własnych wyznaczonych inicjalizatorów, to automatycznie dziedziczy wszystkie wyznaczone inicjalizatory swojej nadrzędnej klasy. Jeśli podklasa zawiera implementację wszystkich wskazanych inicjalizatorów klasy nadrzędnej, automatycznie dziedziczy wszystkie inicjalizatory wygody klasy nadrzędnej. Jest to zgodne z zasadą w języku Swift, że inicjalizacja klas (i struktur) nie może pozostawiać przechowywanych właściwości w nieokreślonym stanie.

natknąłem się na pewne „interesujące” zachowanie podczas eksperymentowania z wygodnymi inicjatorami, wyznaczonymi inicjatorami i regułami dziedziczenia. Odkryłem, że możliwe jest nieumyślne ustawienie błędnego koła. Rozważmy następujący przykład:

Klasa RecipeIngredient nadpisuje wszystkie inicjalizatory klasy Food, a zatem automatycznie dziedziczy wszystkie inicjalizatory superclass convenience. Ale food convenience initializer rozsądnie deleguje do własnego wyznaczonego initializera, który został zastąpiony przez podklasę Recipe ingredient. Tak więc nie jest wywoływana wersja Food tego inicjatora init(name: String), ale wersja nadpisana w Recipe. Wersja nadpisana wykorzystuje fakt, że podklasa odziedziczyła wygodę initializer Food, i oto jest – masz cykl. Nie wiem, czy można by to uznać za błąd programisty, czy błąd kompilatora (zgłosiłem to jako błąd: https://bugs.swift.org/browse/SR-512 ). Wyobraź sobie, że Food jest klasą pochodzącą od osób trzecich, a ty nie masz dostępu do kodu źródłowego, więc nie wiesz, w jaki sposób jest on faktycznie zaimplementowany. Nie wiesz (do czasu wykonania), że użycie go w sposób pokazany w tym przykładzie spowoduje uwięzienie w cyklu. Więc myślę, że byłoby lepiej, gdyby kompilator nam pomógł.

Failable Initializers

wyobraź sobie, że zaprojektowałeś klasę, która ma pewne niezmienniki i chciałbyś je wymusić od momentu utworzenia instancji klasy. Na przykład, być może modelujesz fakturę i chcesz mieć pewność, że kwota jest zawsze nieujemna. Jeśli dodałeś initializer, który pobiera argument amount typu Double, jak możesz się upewnić,że nie naruszasz swojego niezmiennika? Jedną ze strategii jest po prostu sprawdzenie, czy argument jest nieujemny. Jeśli Tak, użyj go. W przeciwnym razie domyślnie 0. Na przykład:

to zadziała i może być dopuszczalne, jeśli udokumentujesz, co robi inicjalizator (zwłaszcza jeśli planujesz udostępnić swoją klasę innym programistom). Ale możesz mieć trudności z obroną tej strategii, ponieważ zamieci problem pod dywan. Innym podejściem wspieranym przez Swift byłoby pozwolić na niepowodzenie inicjalizacji. Oznacza to, że Twój inicjalizator nie będzie działał.

jak opisano w tym artykule, do Swift dodano failable initializers jako sposób na wyeliminowanie lub zmniejszenie zapotrzebowania na metody fabryczne, „które wcześniej były jedynym sposobem zgłaszania awarii” podczas budowy obiektu. Aby inicjalizator nie działał poprawnie, wystarczy dołączyć ? albo ! znak po słowie kluczowym init(np. albo init! ). Następnie, po ustawieniu wszystkich właściwości i spełnieniu wszystkich innych reguł dotyczących delegowania, dodasz trochę logiki, aby zweryfikować, czy argumenty są poprawne. Jeśli nie są poprawne, wywołujesz błąd inicjalizacji zwracając nil. Zauważ, że nie oznacza to, że inicjalizator kiedykolwiek zwraca cokolwiek. Oto jak nasza klasa faktury może wyglądać z nieudanym inicjalizatorem:
zauważyłeś coś innego w tym, jak wykorzystujemy wynik tworzenia obiektu? Traktujemy to jako opcjonalne, prawda? Właśnie to robimy! Gdy użyjemy failable initializer, otrzymamy nil (jeśli błąd inicjalizacji został wywołany) lub opcjonalny (Faktura). Oznacza to, że jeśli inicjalizacja się powiodła, otrzymamy opcję, która zawija interesującą nas instancję faktury, więc musimy ją rozpakować. (Na marginesie, należy zauważyć, że Java ma również opcje od Java 8).

Failable initializers są takie same jak inne typy initializerów, które omówiliśmy w odniesieniu do nadpisywania i delegowania, wyznaczonych vs wygody, itp… w rzeczywistości, można nawet nadpisać failable initializer z nonfailable initializer. Nie można jednak nadpisać niedostępnego inicjalizatora błędnym.

być może zauważyłeś failable initializers z czynienia z UIView lub UIViewController, które oba zapewniają INIT failable INIT INIT?(koder aDecoder: nscoder). Ten inicjalizator jest wywoływany, gdy Widok lub kontroler ViewController jest ładowany z końcówki. Ważne jest, aby zrozumieć, jak działają nieudane inicjalizatory. Zdecydowanie polecam przeczytanie sekcji Failable Initializers w Przewodniku języka Swift w celu dokładnego wyjaśnienia.

wymagane Inicjalizatory

wymagany modyfikator jest używany do wskazania, że wszystkie podklasy muszą zaimplementować odpowiedni inicjalizator. Na pierwszy rzut oka brzmi to dość prosto i prosto. Czasami może to być nieco mylące, jeśli nie rozumiesz, w jaki sposób wchodzą w grę zasady dotyczące dziedziczenia inicjalizatora omówione powyżej. Jeśli podklasa spełnia kryteria, według których inicjalizatory klasy nadrzędnej są dziedziczone, to zestaw odziedziczonych inicjalizatorów zawiera te oznaczone jako wymagane. W związku z tym podklasa w sposób dorozumiany spełnia umowę nałożoną przez wymagany modyfikator. Implementuje wymagany inicjator(y), po prostu nie widzisz go w kodzie źródłowym.

jeśli podklasa zapewnia jawną (tzn. nie dziedziczoną) implementację wymaganego inicjalizatora, to również nadpisuje implementację klasy nadrzędnej. Wymagany modyfikator implikuje override, więc modyfikator override nie jest używany. Możesz to uwzględnić, jeśli chcesz, ale zrobienie tego byłoby zbędne, a XCode będzie Cię o to dręczyć.

Przewodnik po języku Swift nie mówi zbyt wiele o wymaganym modyfikatorze, więc przygotowałem próbkę kodu (patrz poniżej) z komentarzami, aby wyjaśnić jego cel i opisać, jak działa. Aby uzyskać więcej informacji, zobacz ten artykuł autorstwa Anthony ’ ego Levingsa.

szczególny przypadek: rozszerzenie UIView

jedną z rzeczy, która skłoniła mnie do zagłębienia się w szybkie inicjalizatory, była moja próba znalezienia sposobu na stworzenie zestawu wyznaczonych inicjalizatorów bez powielania logiki inicjalizacji. Na przykład, pracowałem przez ten samouczek UIView Raya Wenderlicha, konwertując jego kod Objective-C na Swift, gdy poszedłem (możesz spojrzeć na moją wersję Swift tutaj). Jeśli spojrzysz na ten samouczek, zobaczysz, że jego podklasa RateView UIView ma metodę baseInit, której oba wyznaczone inicjalizatory używają do wykonywania typowych zadań inicjalizacyjnych. Wydaje mi się to dobrym podejściem – nie chcesz powielać tego w każdym z tych inicjalizatorów. Chciałem odtworzyć tę technikę w mojej szybkiej wersji RateView. Ale okazało się to trudne, ponieważ wyznaczony inicjator nie może delegować do innego inicjatora w tej samej klasie i nie może wywoływać metod swojej własnej klasy, dopóki nie deleguje do inicjatora superklasy. W tym momencie jest już za późno, aby ustawić stałe właściwości. Oczywiście można obejść to ograniczenie, nie używając stałych, ale to nie jest dobre rozwiązanie. Więc pomyślałem, że najlepiej jest po prostu podać domyślne wartości dla przechowywanych właściwości, w których są zadeklarowane. To nadal najlepsze rozwiązanie, jakie obecnie znam. Jednak wymyśliłem alternatywną technikę, która wykorzystuje inicjalizatory.

spójrz na poniższy przykład. Jest to fragment z Rateviewwithcomponenceinits.swift, który jest alternatywną wersją mojego Swift port of RateView. Będąc podklasą UIView, która nie zapewnia domyślnych wartości dla wszystkich własnych właściwości przechowywanych w deklaracji, ta alternatywna wersja RateView musi przynajmniej zapewniać jawną implementację wymaganego init UIView?(coder aDecoder: nscoder) initializer. Będziemy również chcieli zapewnić wyraźną implementację INIT UIView (frame: Cgrect) initializer, aby upewnić się, że proces inicjalizacji jest spójny. Chcemy, aby nasze przechowywane właściwości były ustawiane w ten sam sposób, niezależnie od tego, który inicjalizator jest używany.
zauważ, że dodałem modyfikator wygody do nadpisanych wersji inicjalizatorów UIView. Dodałem również do podklasy failable desygnowany inicjalizator, do którego delegują oba nadpisane (wygodne) inicjalizatory. Ten pojedynczy wyznaczony initializer zajmuje się ustawianiem wszystkich przechowywanych właściwości (w tym stałych – nie musiałem uciekać się do używania var do wszystkiego). Działa, ale wydaje mi się, że jest całkiem niezdarny. Wolałbym po prostu podać domyślne wartości dla moich przechowywanych właściwości, w których są zadeklarowane, ale dobrze jest wiedzieć, że ta opcja istnieje w razie potrzeby.

You might also like

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.