Wysyłanie wiadomości do zera w Objective-C

107

Jako programista Java, który czyta dokumentację Apple Objective-C 2.0: zastanawiam się, co oznacza „ wysłanie wiadomości do zera ” - nie mówiąc już o tym, jak jest to w rzeczywistości przydatne. Wyciąg z dokumentacji:

Jest kilka wzorów w kakao, które wykorzystują ten fakt. Wartość zwrócona z wiadomości do nil może być również poprawna:

  • Jeśli metoda zwraca obiekt, dowolny typ wskaźnika, dowolny skalar całkowity o rozmiarze mniejszym lub równym sizeof (void *), float, a double, a long double lub long long, to wiadomość wysłana do zera zwraca 0 .
  • Jeśli metoda zwraca strukturę, zgodnie z definicją zawartą w przewodniku wywołań funkcji ABI systemu Mac OS X, która ma zostać zwrócona w rejestrach, wówczas komunikat wysłany do zera zwraca wartość 0,0 dla każdego pola w strukturze danych. Inne typy danych struktur nie będą wypełnione zerami.
  • Jeśli metoda zwraca cokolwiek innego niż wymienione powyżej typy wartości, wartość zwracana wiadomości wysłanej do nil jest niezdefiniowana.

Czy Java sprawiła, że ​​mój mózg nie był w stanie zrozumieć powyższego wyjaśnienia? A może jest coś, czego mi brakuje, co sprawiłoby, że byłby tak przejrzysty jak szkło?

Rozumiem wiadomości / odbiorniki w Objective-C, jestem po prostu zdezorientowany co do odbiornika, który tak się składa nil.

Ryan Delucchi
źródło
2
Miałem też podstawy w Javie i na początku byłem przerażony tą fajną funkcją, ale teraz uważam ją za absolutnie LOVELY !;
Valentin Radu
1
Dzięki, to świetne pytanie. Czy udało Ci się zobaczyć korzyści z tego? Wydaje mi się, że to „nie błąd, funkcja”. Ciągle dostaję błędy, w których Java po prostu uderzyłaby mnie z wyjątkiem, więc wiedziałem, gdzie jest problem. Nie jestem zadowolony z zamiany zerowego wyjątku wskaźnika, aby zapisać tu i tam wiersz lub dwa trywialne kod.
Maciej Trybiło

Odpowiedzi:

92

Cóż, myślę, że można to opisać na bardzo wymyślnym przykładzie. Załóżmy, że masz w Javie metodę, która wypisuje wszystkie elementy z ArrayList:

void foo(ArrayList list)
{
    for(int i = 0; i < list.size(); ++i){
        System.out.println(list.get(i).toString());
    }
}

Teraz, jeśli wywołasz tę metodę w ten sposób: someObject.foo (NULL); prawdopodobnie otrzymasz wyjątek NullPointerException podczas próby uzyskania dostępu do listy, w tym przypadku w wywołaniu list.size (); Teraz prawdopodobnie nigdy nie wywołałbyś obiektu someObject.foo (NULL) z taką wartością NULL. Jednak możesz pobrać swoją ArrayList z metody, która zwraca NULL, jeśli napotka jakiś błąd podczas generowania ArrayList, jak someObject.foo (otherObject.getArrayList ());

Oczywiście będziesz mieć również problemy, jeśli zrobisz coś takiego:

ArrayList list = NULL;
list.size();

Teraz w Objective-C mamy równoważną metodę:

- (void)foo:(NSArray*)anArray
{
    int i;
    for(i = 0; i < [anArray count]; ++i){
        NSLog(@"%@", [[anArray objectAtIndex:i] stringValue];
    }
}

Teraz, jeśli mamy następujący kod:

[someObject foo:nil];

mamy taką samą sytuację, w której Java wygeneruje wyjątek NullPointerException. Obiekt zerowy będzie dostępny jako pierwszy pod adresem [anArray count] Jednak zamiast rzucać wyjątek NullPointerException, Objective-C zwróci po prostu 0 zgodnie z powyższymi regułami, więc pętla nie zostanie uruchomiona. Jeśli jednak ustawimy pętlę tak, aby uruchamiała się określoną liczbę razy, najpierw wysyłamy wiadomość do tablicy anArray pod adresem [anArray objectAtIndex: i]; Zwróci to również 0, ale ponieważ objectAtIndex: zwraca wskaźnik, a wskaźnik do 0 ma wartość nil / NULL, NSLog zostanie przekazany do zera za każdym razem przez pętlę. (Chociaż NSLog jest funkcją, a nie metodą, wypisuje (null), jeśli przekazano nil NSString.

W niektórych przypadkach przyjemniej jest mieć wyjątek NullPointerException, ponieważ można od razu stwierdzić, że coś jest nie tak z programem, ale jeśli nie złapiesz wyjątku, program się zawiesi. (W C, próba wyłuskiwania NULL w ten sposób powoduje awarię programu.) W Objective-C, zamiast tego powoduje to prawdopodobnie nieprawidłowe zachowanie w czasie wykonywania. Jeśli jednak masz metodę, która nie psuje się, jeśli zwraca 0 / nil / NULL / zerowaną strukturę, to oszczędza ci to konieczności sprawdzania, czy obiekt lub parametry są zerowe.

Michael Buckley
źródło
33
Prawdopodobnie warto wspomnieć, że takie zachowanie było przedmiotem wielu debat w społeczności Objective-C w ciągu ostatnich kilku dekad. Kompromis między „bezpieczeństwem” a „wygodą” jest różnie oceniany przez różnych ludzi.
Mark Bessey
3
W praktyce istnieje duża symetria między przekazywaniem wiadomości do zera a sposobem działania Objective-C, szczególnie w nowej funkcji słabych wskaźników w ARC. Słabe wskaźniki są automatycznie zerowane. Zaprojektuj więc API tak, aby mogło odpowiadać na 0 / nil / NIL / NULL itp.
Cthutu
1
Myślę, że jeśli to zrobisz myObject->iVar, ulegnie awarii, niezależnie od tego, czy jest to C z obiektami, czy bez . (przepraszam za gravedig.)
11684
3
@ 11684 Zgadza się, ale ->nie jest to już operacja celu-C, ale w dużym stopniu ogólny C-izm.
bbum
1
Niedawny OSX korzeń wykorzystać / backdoor ukryty API jest dostępne dla wszystkich użytkowników (nie tylko administratorzy) z powodu obj-c za Nil wiadomości.
dcow
51

Komunikat na nilnic nie robi i powraca nil, Nil, NULL, 0, lub 0.0.

Peter Hosey
źródło
41

Wszystkie inne posty są poprawne, ale może to koncepcja jest tutaj ważna.

W wywołaniach metod Objective-C każde odwołanie do obiektu, które może zaakceptować selektor, jest prawidłowym celem dla tego selektora.

Oszczędza to DUŻO „czy obiekt docelowy jest typu X?” kod - o ile obiekt odbierający implementuje selektor, nie ma absolutnie żadnego znaczenia, jaka to klasa! niljest obiektem NSO, który akceptuje dowolny selektor - po prostu nic nie robi . Eliminuje to również wiele kodów typu „sprawdź zero, nie wysyłaj wiadomości, jeśli prawda”. (Koncepcja „jeśli to akceptuje, to implementuje” pozwala również na tworzenie protokołów , które są trochę podobne do interfejsów Java: deklaracja, że ​​jeśli klasa implementuje podane metody, to jest zgodna z protokołem.)

Powodem tego jest wyeliminowanie kodu małpy, który nie robi nic poza utrzymywaniem kompilatora w dobrym stanie. Tak, otrzymujesz dodatkowe wywołanie metody, ale oszczędzasz czas programisty , który jest znacznie droższym zasobem niż czas procesora. Ponadto eliminujesz z aplikacji więcej kodu i większą złożoność warunkową.

Wyjaśnienie dla zwolenników osłabienia: możesz pomyśleć, że to nie jest dobra droga, ale tak właśnie jest zaimplementowany język i jest to zalecany idiom programowania w Objective-C (zobacz wykłady z programowania w Stanford na iPhone'a).

Joe McMahon
źródło
17

Oznacza to, że środowisko wykonawcze nie generuje błędu, gdy wywoływana jest objc_msgSend na wskaźniku nil; zamiast tego zwraca pewną (często użyteczną) wartość. Wiadomości, które mogą mieć efekt uboczny, nic nie robią.

Jest to przydatne, ponieważ większość wartości domyślnych jest bardziej odpowiednia niż błąd. Na przykład:

[someNullNSArrayReference count] => 0

To znaczy, nil wydaje się być pustą tablicą. Ukrywanie zerowego odwołania NSView nic nie robi. Handy, co?

Bogaty
źródło
12

W cytacie z dokumentacji są dwie odrębne koncepcje - być może byłoby lepiej, gdyby dokumentacja to wyjaśniała:

Jest kilka wzorów w kakao, które wykorzystują ten fakt.

Wartość zwrócona z wiadomości do nil może być również poprawna:

To pierwsze jest prawdopodobnie bardziej istotne w tym przypadku: zazwyczaj możliwość wysyłania wiadomości w celu niluczynienia kodu prostszym - nie trzeba wszędzie sprawdzać wartości null. Przykładem kanonicznym jest prawdopodobnie metoda akcesora:

- (void)setValue:(MyClass *)newValue {
    if (value != newValue) { 
        [value release];
        value = [newValue retain];
    }
}

Gdyby wysyłanie wiadomości do nilnie było prawidłowe, ta metoda byłaby bardziej złożona - musiałbyś mieć dwie dodatkowe kontrole, aby się upewnić, valuea newValuenie nilprzed wysłaniem im wiadomości.

Drugi punkt (że wartości zwracane z wiadomości nilsą również zwykle prawidłowe), jednak dodaje efekt mnożnikowy do pierwszego. Na przykład:

if ([myArray count] > 0) {
    // do something...
}

Ten kod ponownie nie wymaga sprawdzania nilwartości i płynie naturalnie ...

Wszystko to powiedziawszy, dodatkowa elastyczność, do której można wysyłać wiadomości, nilwiąże się z pewnym kosztem. Istnieje możliwość, że na pewnym etapie napiszesz kod, który zawiedzie w szczególny sposób, ponieważ nie wziąłeś pod uwagę możliwości, że może to być wartość nil.

mmalc
źródło
12

Od Greg Parker „s stronie :

Jeśli jest uruchomiony LLVM Compiler 3.0 (Xcode 4.2) lub nowszy

Komunikaty do zera z typem zwracanym | powrót
Liczby całkowite do 64 bitów | 0
Zmiennoprzecinkowy aż do długiego podwójnego | 0.0
Wskaźniki | zero
Structs | {0}
Dowolny typ _Complex | {0, 0}
Heath Borders
źródło
9

Oznacza to często brak konieczności sprawdzania wszędzie pod kątem bezpieczeństwa żadnych obiektów - w szczególności:

[someVariable release];

lub, jak już wspomniano, wszystkie metody liczenia i długości zwracają 0, gdy masz wartość zerową, więc nie musisz dodawać dodatkowych sprawdzeń na zero:

if ( [myString length] > 0 )

albo to:

return [myArray count]; // say for number of rows in a table
Kendall Helmstetter Gelner
źródło
Pamiętaj, że druga strona medalu to potencjalne błędy, takie jak „if ([myString length] == 1)”
hatfinch
Jak to jest błąd? [długość myString] zwraca zero (nil), jeśli myString jest równe zero ... jedną rzeczą, którą myślę, że może być problem, jest [ramka myView], która, jak sądzę, może dać ci coś dziwnego, jeśli myView ma wartość zero.
Kendall Helmstetter Gelner
Jeśli projektujesz swoje klasy i metody wokół koncepcji, zgodnie z którą wartości domyślne (0, zero, NIE) oznaczają „nieprzydatne”, jest to potężne narzędzie. Nigdy nie muszę sprawdzać, czy sznurki nie mają nic przed sprawdzeniem długości. Dla mnie pusty ciąg jest tak samo bezużyteczny, jak ciąg zerowy, gdy przetwarzam tekst. Jestem też programistą Java i wiem, że puryści Java będą tego unikać, ale oszczędza to dużo kodowania.
Jason Fuerstenberg
6

Nie myśl o tym, że „odbiorca jest zerowy”; Zgadzam się, to dość dziwne. Jeśli wysyłasz wiadomość do zera, nie ma odbiorcy. Po prostu wysyłasz wiadomość do niczego.

Jak sobie z tym poradzić, to filozoficzna różnica między Javą a Objective-C: w Javie to błąd; w Objective-C nie jest to operacja.

benzado
źródło
Istnieje wyjątek od tego zachowania w java, jeśli wywołujesz funkcję statyczną o wartości null, jest to równoważne wywołaniu funkcji w klasie czasu kompilacji zmiennej (nie ma znaczenia, czy jest pusta).
Roman A. Taycher
6

Komunikaty ObjC, które są wysyłane do nil i których wartości zwracane mają rozmiar większy niż sizeof (void *), generują niezdefiniowane wartości na procesorach PowerPC. Ponadto komunikaty te powodują zwracanie niezdefiniowanych wartości w polach struktur, których rozmiar jest większy niż 8 bajtów również w procesorach Intel. Vincent Gable ładnie to opisał na swoim blogu

Nikita Zhuk
źródło
6

Nie sądzę, aby w żadnej z pozostałych odpowiedzi wyraźnie o tym wspomniało: jeśli jesteś przyzwyczajony do języka Java, należy pamiętać, że chociaż Objective-C w systemie Mac OS X obsługuje obsługę wyjątków, jest to opcjonalna funkcja języka, którą można włączony / wyłączony z flagą kompilatora. Domyślam się, że ten projekt „wysyłania wiadomości niljest bezpieczny” poprzedza włączenie obsługi wyjątków w języku i został wykonany z podobnym celem: metody mogą powrócić, nilaby wskazać błędy, a ponieważ wysyłanie wiadomości nilzwykle zwracanilto z kolei umożliwia propagację wskazania błędu w kodzie, dzięki czemu nie trzeba go sprawdzać w każdej wiadomości. Musisz to sprawdzić tylko w punktach, w których ma to znaczenie. Osobiście uważam, że propagowanie i obsługa wyjątków jest lepszym sposobem osiągnięcia tego celu, ale nie każdy może się z tym zgodzić. (Z drugiej strony, na przykład nie podoba mi się wymaganie Java, abyś musiał deklarować, jakie wyjątki może rzucać metoda, co często zmusza cię do syntaktycznego propagowania deklaracji wyjątków w całym kodzie; ale to inna dyskusja.)

Opublikowałem podobną, ale dłuższą odpowiedź na powiązane pytanie „Czy twierdzenie, że każde utworzenie obiektu zakończyło się sukcesem w celu C, jest konieczne?” jeśli chcesz więcej szczegółów.

Rinzwind
źródło
Nigdy nie myślałem o tym w ten sposób. Wydaje się, że jest to bardzo wygodna funkcja.
mk12
2
Dobre przypuszczenie, ale historycznie nieprecyzyjne, dlaczego podjęto taką decyzję. Obsługa wyjątków istniała w języku od początku, chociaż oryginalne procedury obsługi wyjątków były dość prymitywne w porównaniu z nowoczesnym idiomem. Komunikat Nil-eats był świadomym wyborem projektowym wynikającym z opcjonalnego zachowania obiektu Nil w Smalltalk. Kiedy projektowano oryginalne API NeXTSTEP, łączenie metod było dość powszechne, a nilpowrót często był używany do zwarcia łańcucha w NO-op.
bbum
2

C nie reprezentuje niczego jako 0 dla wartości pierwotnych i NULL dla wskaźników (co jest równoważne 0 w kontekście wskaźnika).

Cel-C opiera się na reprezentacji niczego w języku C, dodając zero. nil jest wskaźnikiem obiektu do niczego. Chociaż semantycznie różnią się od NULL, są one technicznie równoważne sobie.

Nowo przydzielone obiekty NSO zaczynają życie z zawartością ustawioną na 0. Oznacza to, że wszystkie wskaźniki, które ten obiekt ma do innych obiektów, zaczynają się od zera, więc nie jest konieczne, na przykład, ustawianie self. (Asocjacja) = nil w metodach init.

Najbardziej godne uwagi zachowanie nil polega jednak na tym, że może mieć wysyłane do niego wiadomości.

W innych językach, takich jak C ++ (lub Java), spowodowałoby to awarię programu, ale w Objective-C wywołanie metody na nil zwraca wartość zero. To znacznie upraszcza wyrażenia, ponieważ eliminuje potrzebę sprawdzania nil przed zrobieniem czegokolwiek:

// For example, this expression...
if (name != nil && [name isEqualToString:@"Steve"]) { ... }

// ...can be simplified to:
if ([name isEqualToString:@"Steve"]) { ... }

Świadomość, jak nil działa w Objective-C, sprawia, że ​​ta wygoda jest funkcją, a nie czającym się błędem w aplikacji. Upewnij się, że chronisz się przed przypadkami, w których wartości nil są niepożądane, sprawdzając i zwracając wcześnie, aby po cichu zakończyć się niepowodzeniem, lub dodając NSParameterAssert, aby zgłosić wyjątek.

Źródło: http://nshipster.com/nil/ https://developer.apple.com/library/ios/#documentation/cocoa/conceptual/objectivec/Chapters/ocObjectsClasses.html (wysyłanie wiadomości do nil).

Zee
źródło