Dlaczego w Objective-C powinienem sprawdzać, czy self = [super init] nie jest zerowe?

165

Mam ogólne pytanie dotyczące pisania metod init w Objective-C.

Widzę wszędzie (kod Apple, książki, kod open source itp.), Że metoda init powinna sprawdzać, czy self = [super init] nie jest zerowe przed kontynuacją inicjalizacji.

Domyślny szablon Apple dla metody init to:

- (id) init
{
    self = [super init];

    if (self != nil)
    {
        // your code here
    }

    return self;
}

Czemu?

Mam na myśli, kiedy init kiedykolwiek zwróci zero? Jeśli wywołałem init na NSObject i otrzymałem zero, to coś musi być naprawdę spieprzone, prawda? W takim przypadku równie dobrze możesz nawet nie napisać programu ...

Czy to naprawdę takie częste, że metoda init klasy może zwracać zero? Jeśli tak, w jakim przypadku i dlaczego?

Jasarien
źródło
1
Wil Shipley opublikował jakiś artykuł na ten temat. [self = [głupi init];] ( wilshipley.com/blog/2005/07/self-stupid-init.html ) Przeczytaj również komentarze, kilka dobrych rzeczy.
Ryan Townshend
6
Można zapytać Wil Shipley lub Mike Ash lub Matt Gallagher . Tak czy inaczej, jest to temat będący przedmiotem dyskusji. Ale zwykle dobrze jest trzymać się idiomów Apple ... w końcu to ich Framework.
jbrennan
1
Wygląda na to, że Wil bardziej argumentował za tym, aby nie ślepo zmieniać przypisania self podczas inicjalizacji, wiedząc, że [super init] może nie zwrócić odbiornika.
Jasarien
3
Wil zmienił zdanie od czasu opublikowania tego postu.
bbum
5
Widziałem to pytanie jakiś czas temu i po prostu znalazłem je ponownie. Idealny. +1
Dan Rosenstark,

Odpowiedzi:

53

Na przykład:

[[NSData alloc] initWithContentsOfFile:@"this/path/doesn't/exist/"];
[[NSImage alloc] initWithContentsOfFile:@"unsupportedFormat.sjt"];
[NSImage imageNamed:@"AnImageThatIsntInTheImageCache"];

... i tak dalej. (Uwaga: NSData może zgłosić wyjątek, jeśli plik nie istnieje). Istnieje wiele obszarów, w których zwracanie zera jest oczekiwanym zachowaniem, gdy pojawia się problem, dlatego standardową praktyką jest sprawdzanie zera prawie przez cały czas, dla zachowania spójności.

iKenndac
źródło
10
Tak, ale to nie jest WEWNĄTRZ metody init odpowiedniej klasy. NSData dziedziczy po NSObject. Czy NSData sprawdza, czy [super init] zwraca nil? O to tutaj proszę. Przepraszam, jeśli nie było jasne ...
Jasarien
9
Gdybyś miał podklasę tych klas, byłaby całkiem realna szansa, że ​​[super init] zwróci zero. Nie wszystkie klasy są bezpośrednimi podklasami NSObject. Nigdy nie ma żadnej gwarancji, że nie zwróci zera. Jest to tylko defensywna praktyka kodowania, która jest ogólnie zalecana.
Chuck
7
Wątpię, czy inicjalizacja NSObject może zwrócić nil w każdym przypadku, kiedykolwiek. Jeśli zabraknie pamięci, alokacja się nie powiedzie, ale jeśli się powiedzie, wątpię, czy init może się nie powieść - NSObject nie ma nawet żadnych zmiennych instancji poza Class. W GNUStep jest zaimplementowany jako „powrót do siebie”, a deasemblacja na Macu wygląda tak samo. Wszystko to jest oczywiście bez znaczenia - wystarczy postępować zgodnie ze standardowym idiomem i nie musisz się martwić, czy może, czy nie.
Peter N Lewis
9
Nie jestem do diabła nastawiony na nieprzestrzeganie najlepszych praktyk. Chciałbym jednak przede wszystkim wiedzieć, dlaczego są to najlepsze praktyki. To tak, jakby ktoś kazał mu zeskoczyć z wieży. Nie idziesz dalej i nie robisz tego, chyba że wiesz dlaczego. Czy na dole jest duża, miękka poduszka, na której można wylądować, z ogromną nagrodą pieniężną? Gdybym wiedział, że skoczyłbym. Jeśli nie, nie zrobiłbym tego. Nie chcę po prostu ślepo podążać za praktyką, nie wiedząc, dlaczego ją wykonuję ...
Jasarien
4
Jeśli alert zwraca nil, init jest wysyłany do nil, co zawsze skutkuje nil, co kończy się na self równym nil.
T.
50

Ten konkretny idiom jest standardowy, ponieważ działa we wszystkich przypadkach.

Chociaż jest to rzadkie, będą przypadki, w których ...

[super init];

... zwraca inną instancję, wymagając w ten sposób przypisania do siebie.

Będą przypadki, w których zwróci wartość nil, wymagając w ten sposób sprawdzenia nil, aby kod nie próbował zainicjować gniazda zmiennej instancji, która już nie istnieje.

Najważniejsze jest to, że jest to udokumentowany prawidłowy wzorzec do użycia, a jeśli go nie używasz, robisz to źle.

bbum
źródło
3
Czy jest to nadal prawdą w świetle specyfikatorów zerowej wartości? Jeśli inicjalizator mojej superklasy nie jest zerowy, czy naprawdę warto sprawdzić dodatkowy bałagan? (Chociaż sam NSObject nie wydaje się mieć żadnych -initproblemów…)
natevw
Czy jest kiedykolwiek przypadek, w którym [super init]zwraca zero, gdy bezpośrednia nadklasa jest NSObject? Czy nie jest to przypadek, w którym „wszystko jest zepsute”?
Dan Rosenstark
1
@DanRosenstark Nie, jeśli NSObjectjest bezpośrednią nadklasą. Ale ... nawet jeśli zadeklarujesz NSObjectjako bezpośrednią nadklasę, coś mogło zostać zmodyfikowane w czasie wykonywania w taki sposób, że NSObjectimplementacja initnie jest tym, co faktycznie się nazywa.
bbum
1
Wielkie dzięki @bbum, to naprawdę pomogło mi zorientować się w naprawianiu błędów. Dobrze jest wykluczyć pewne rzeczy!
Dan Rosenstark
26

Myślę, że w większości klas, jeśli wartość zwracana przez [super init] jest równa zero i sprawdzasz to, zgodnie ze standardowymi praktykami, a następnie zwracasz przedwcześnie, jeśli zero, w zasadzie Twoja aplikacja nadal nie będzie działać poprawnie. Jeśli myślisz o tym, mimo że jeśli (self! = Nil) sprawdzenie czy istnieje, do prawidłowego działania swojej klasie, 99,99% czasu faktycznie zrobić potrzebujemy siebie być non-zero. Teraz załóżmy, że, niezależnie od przyczyny, [SUPER startowych] zrobił zwrot nil, w zasadzie czek na zero jest w zasadzie przekazując złotówki do rozmówcy swojej klasie, gdzie byłoby to prawdopodobnie nie tak czy inaczej, ponieważ będzie on oczywiście założyć, że wezwanie było odnoszący sukcesy.

Zasadniczo rozumiem to, że w 99,99% przypadków if (self! = Nil) nie kupuje niczego w kategoriach większej odporności, ponieważ po prostu przekazujesz pieniądze inwokatorowi. Aby naprawdę móc sobie z tym poradzić, należałoby wprowadzić kontrole w całej hierarchii wywołań. I nawet wtedy jedyne, co by ci kupiło, to to, że twoja aplikacja zawiedzie trochę bardziej czysto / solidnie. Ale i tak by się nie udało.

Jeśli klasa biblioteki arbitralnie zdecydowała się zwrócić nil w wyniku [superinita], to i tak jesteś prawie spieprzony, a to bardziej wskazuje na to, że autor klasy biblioteki popełnił błąd implementacji.

Myślę, że jest to bardziej sugestia dotycząca starszego kodowania, gdy aplikacje działały w znacznie bardziej ograniczonej pamięci.

Ale w przypadku kodu poziomu C nadal zazwyczaj sprawdzałbym wartość zwracaną przez malloc () względem wskaźnika NULL. Natomiast w przypadku Objective-C, dopóki nie znajdę dowodów przeciwnych, myślę, że generalnie pomijam sprawdzanie if (self! = Nil). Skąd ta rozbieżność?

Ponieważ na poziomach C i Malloc w niektórych przypadkach można częściowo odzyskać zdrowie. Podczas gdy myślę, że w Objective-C, w 99,99% przypadków, jeśli [super init] zwraca zero, w zasadzie jesteś spieprzony, nawet jeśli spróbujesz sobie z tym poradzić. Równie dobrze możesz po prostu pozwolić aplikacji się zawiesić i poradzić sobie z następstwami.

Gino
źródło
6
Dobrze powiedziane. Popieram to.
Roger CS Wernersson,
3
+1 Całkowicie się zgadzam. Tylko drobna uwaga: nie wierzę, że wzorzec jest wynikiem czasów, w których alokacja częściej zawodziła. Alokacja jest zwykle wykonywana w momencie wywołania init. Jeśli alokacja się nie powiedzie, init nie zostanie nawet wywołany.
Nikolai Ruhe
8

To jest swego rodzaju podsumowanie powyższych uwag.

Powiedzmy, że powraca superklasa nil. Co się stanie?

Jeśli nie przestrzegasz konwencji

Twój kod ulegnie awarii w środku initmetody. (chyba że initnie robi nic istotnego)

Jeśli zastosujesz się do konwencji, nie wiedząc, że superklasa może zwrócić zero (większość ludzi kończy tutaj)

Twój kod prawdopodobnie ulegnie awarii w pewnym momencie później, ponieważ Twoja instancja jest niltam, gdzie spodziewałeś się czegoś innego. Albo twój program będzie zachowywał się nieoczekiwanie bez awarii. O jej! Chcesz tego? Nie wiem...

Jeśli będziesz postępować zgodnie z konwencjami, dobrowolnie pozwalając swojej podklasie zwrócić zero

Twoja dokumentacja kodu (!) Powinna jasno określać: „return ... or nil”, a reszta kodu musi być przygotowana do obsługi tego. Teraz to ma sens.

user123444555621
źródło
5
Myślę, że interesującą kwestią jest to, że opcja nr 1 jest zdecydowanie lepsza niż opcja nr 2. Jeśli rzeczywiście istnieją okoliczności, w których mógłbyś chcieć, aby init Twojej podklasy zwrócił nil, to preferowany jest # 3. Jeśli jedynym powodem, który kiedykolwiek miałby się zdarzyć, jest błąd w kodzie, użyj # 1. Korzystanie z opcji nr 2 to po prostu opóźnianie eksplozji aplikacji do późniejszego punktu w czasie, a tym samym znacznie utrudniając pracę podczas debugowania błędu. To tak, jakby po cichu łapać wyjątki i kontynuować bez ich obsługi.
Mark Amery,
Lub przełączysz się na Swift i po prostu użyj opcji
AndrewSB
7

Zazwyczaj, jeśli twoja klasa pochodzi bezpośrednio z NSObject, nie musisz tego robić. Jest to jednak dobry nawyk, ponieważ jeśli twoja klasa pochodzi od innych klas, ich inicjatory mogą powrócić nil, a jeśli tak, twój inicjator może następnie przechwycić to i zachować się poprawnie.

I tak, dla przypomnienia, postępuję zgodnie z najlepszymi praktykami i piszę je na wszystkich moich zajęciach, nawet tych pochodzących bezpośrednio z NSObject.

John Rudy
źródło
1
Mając to na uwadze, czy dobrą praktyką byłoby sprawdzenie nil po zainicjowaniu zmiennej i przed wywołaniem jej funkcji? np.Foo *bar = [[Foo alloc] init]; if (bar) {[bar doStuff];}
dev_does_software
Dziedziczenie z NSObjectnie gwarantuje również, że -initda ci to NSObject, jeśli liczysz egzotyczne środowiska wykonawcze, takie jak starsze wersje GNUstep w (który zwraca GSObject), więc bez względu na wszystko, czek i przypisanie.
Maxthon Chan,
3

Masz rację, często mógłbyś po prostu napisać [super init], ale to nie zadziała dla podklasy czegokolwiek. Ludzie wolą po prostu zapamiętać jedną standardową linię kodu i używać jej cały czas, nawet wtedy, gdy jest to tylko czasami konieczne, i tak otrzymujemy standard if (self = [super init]), który zakłada zarówno możliwość zwrócenia zera, jak i możliwość zwrócenia obiektu innego niż selfzwrócenie na konto.

andyvn22
źródło
3

Częstym błędem jest pisanie

self = [[super alloc] init];

która zwraca instancję nadklasy, która NIE jest tym, czego chcesz w konstruktorze / inicie podklasy. Otrzymujesz obiekt, który nie reaguje na metody podklasy, co może być mylące i generuje mylące błędy dotyczące braku odpowiedzi na metody lub identyfikatory, które nie zostały znalezione itp.

self = [super init]; 

jest potrzebne, jeśli superklasa ma elementy składowe (zmienne lub inne obiekty) do zainicjowania najpierw przed skonfigurowaniem elementów składowych podklas. W przeciwnym razie środowisko uruchomieniowe objc inicjuje je wszystkie na 0 lub na zero . (w przeciwieństwie do ANSI C, który często przydziela fragmenty pamięci bez ich czyszczenia )

I tak, inicjalizacja klasy bazowej może się nie powieść z powodu błędów braku pamięci, brakujących komponentów, błędów pozyskiwania zasobów itp., Więc sprawdzenie zeru jest rozsądne i zajmuje mniej niż kilka milisekund.

Chris Reid
źródło
2

Ma to na celu sprawdzenie, czy intialazacja zadziałała, instrukcja if zwraca prawdę, jeśli metoda init nie zwróciła nil, więc jest to sposób na sprawdzenie, czy tworzenie obiektu działa poprawnie. Niewiele powodów, dla których mogę pomyśleć, że init może się nie powieść, być może jest to nadpisana metoda init, o której superklasa nie wie, lub coś w tym rodzaju, chociaż nie sądziłbym, że jest to tak powszechne. Ale jeśli tak się stanie, lepiej, żeby nic się nie wydarzyło, niż przypuszczam awarię, więc zawsze jest sprawdzane ...

Daniel
źródło
tak jest, ale htey są zwoływane razem, co się stanie, jeśli przydzielimy zasoby?
Daniel
Wyobrażam sobie, że jeśli przydział się nie powiedzie, to init zostanie wysłany do zera, a nie do instancji dowolnej klasy, do której wywołujesz. W takim przypadku nic się nie stanie i żaden kod nie zostanie wykonany, aby sprawdzić, czy [super init] zwrócił nil.
Jasarien
2
Alokacja pamięci nie zawsze jest wykonywana w + alokacji. Rozważmy przypadek klastrów klas; NSString nie wie, której konkretnej podklasy użyć, dopóki nie zostanie wywołany inicjator.
bbum
1

W OS X nie jest tak prawdopodobne, -[NSObject init]że zawiedzie z powodu pamięci. Tego samego nie można powiedzieć o iOS.

Ponadto dobrą praktyką jest pisanie podczas tworzenia podklas klasy, która może powrócić nilz dowolnego powodu.

MaddTheSane
źródło
2
Zarówno w systemie iOS, jak i Mac OS -[NSObject init]jest bardzo mało prawdopodobne, aby zakończyło się niepowodzeniem ze względu na pamięć, ponieważ nie przydziela żadnej pamięci.
Nikolai Ruhe
Chyba miał na myśli alokację, a nie init :)
Thomas Tempelmann