Czy można ustawić metodę -init jako prywatną w Objective-C?

147

Muszę ukryć (uczynić prywatną) -initmetodę mojej klasy w Objective-C.

Jak mogę to zrobić?

lajos
źródło
3
Istnieje teraz konkretna, przejrzysta i opisowa możliwość osiągnięcia tego celu, jak pokazano w poniższej odpowiedzi . W szczególności: NS_UNAVAILABLE. Generalnie zachęcałbym do skorzystania z tego podejścia. Czy PO rozważyłby zmianę przyjętej odpowiedzi? Pozostałe odpowiedzi zawierają wiele przydatnych szczegółów, ale nie są preferowaną metodą osiągnięcia tego.
Benjohn,
Jak zauważyli inni poniżej, NS_UNAVAILABLEnadal umożliwia wywołującemu wywołanie initpośrednie przez new. Zwykłe zastąpienie initpowrotu nilbędzie obsługiwać oba przypadki.
Greg Brown,

Odpowiedzi:

88

Objective-C, podobnie jak Smalltalk, nie ma koncepcji metod „prywatnych” i „publicznych”. W dowolnym momencie można wysłać dowolną wiadomość do dowolnego obiektu.

Co możesz zrobić, to NSInternalInconsistencyExceptionzgłosić, jeśli twoja -initmetoda jest wywoływana:

- (id)init {
    [self release];
    @throw [NSException exceptionWithName:NSInternalInconsistencyException
                                   reason:@"-init is not a valid initializer for the class Foo"
                                 userInfo:nil];
    return nil;
}

Inną alternatywą - która prawdopodobnie jest znacznie lepsza w praktyce - jest -initzrobienie czegoś rozsądnego dla klasy, jeśli to w ogóle możliwe.

Jeśli próbujesz to zrobić, ponieważ próbujesz „upewnić się”, że używany jest obiekt pojedynczy, nie przejmuj się. W szczególności, nie przejmuj się z „ręcznym +allocWithZone:, -init, -retain, -release” metoda tworzenia pojedynczych. Jest to praktycznie zawsze niepotrzebne i po prostu powoduje komplikacje bez żadnej znaczącej korzyści.

Zamiast tego po prostu napisz swój kod w taki sposób, aby Twoja +sharedWhatevermetoda była sposobem uzyskiwania dostępu do singletona i udokumentuj to jako sposób na uzyskanie pojedynczej instancji w nagłówku. To powinno wystarczyć w większości przypadków.

Chris Hanson
źródło
2
Czy zwrot jest tu rzeczywiście konieczny?
philsquared
5
Tak, aby kompilator był zadowolony. W przeciwnym razie kompilator może narzekać, że nie ma powrotu z metody z zwrotem nieważnym.
Chris Hanson,
Zabawne, nie dla mnie. Może inna wersja kompilatora lub przełączniki? (Używam tylko domyślnych przełączników gcc z XCode 3.1)
philsquared
3
Liczenie na to, że deweloper będzie podążał za wzorem, nie jest dobrym pomysłem. Lepiej jest zgłosić wyjątek, aby programiści z innego zespołu wiedzieli, że tego nie robią. Moja prywatna koncepcja byłaby lepsza.
Nick Turner
4
„Bez znaczącej korzyści” . Całkowicie nieprawdziwe. Istotną zaletą jest to, że chcesz wymusić wzorzec singletona. Jeśli pozwolisz nowe instancje mają być tworzone, to deweloper, który nie jest obeznany z API może używać alloci initi mają funkcję kodu nieprawidłowo, ponieważ mają oni prawo klasy, ale źle instancji. Na tym polega istota zasady hermetyzacji w OO. Ukrywasz w swoich interfejsach API rzeczy, których inne klasy nie powinny potrzebować ani uzyskiwać do nich dostępu. Nie chcesz po prostu upubliczniać wszystkiego i oczekujesz, że ludzie będą to wszystko śledzić.
Nate
345

NS_UNAVAILABLE

- (instancetype)init NS_UNAVAILABLE;

To jest krótka wersja niedostępnego atrybutu. Po raz pierwszy pojawił się w macOS 10.7 i iOS 5 . Jest zdefiniowany w NSObjCRuntime.h jako #define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTE.

Istnieje wersja, która wyłącza tę metodę tylko dla klientów Swift , a nie dla kodu ObjC:

- (instancetype)init NS_SWIFT_UNAVAILABLE;

unavailable

Dodaj unavailableatrybut do nagłówka, aby wygenerować błąd kompilatora przy każdym wywołaniu init.

-(instancetype) init __attribute__((unavailable("init not available")));  

błąd czasu kompilacji

Jeśli nie masz powodu, po prostu wpisz __attribute__((unavailable)), a nawet __unavailable:

-(instancetype) __unavailable init;  

doesNotRecognizeSelector:

Służy doesNotRecognizeSelector:do zgłaszania wyjątku NSInvalidArgumentException. „System wykonawczy wywołuje tę metodę za każdym razem, gdy obiekt otrzyma wiadomość aSelector, na którą nie może odpowiedzieć ani przesłać dalej”.

- (instancetype) init {
    [self release];
    [super doesNotRecognizeSelector:_cmd];
    return nil;
}

NSAssert

Służy NSAssertdo zgłaszania wyjątku NSInternalInconsistencyException i wyświetlania komunikatu:

- (instancetype) init {
    [self release];
    NSAssert(false,@"unavailable, use initWithBlah: instead");
    return nil;
}

raise:format:

Użyj, raise:format:aby zgłosić własny wyjątek:

- (instancetype) init {
    [self release];
    [NSException raise:NSGenericException 
                format:@"Disabled. Use +[[%@ alloc] %@] instead",
                       NSStringFromClass([self class]),
                       NSStringFromSelector(@selector(initWithStateDictionary:))];
    return nil;
}

[self release]jest potrzebne, ponieważ obiekt był już alloczajęty. Podczas korzystania z ARC kompilator zadzwoni za Ciebie. W każdym razie nie ma się czym martwić, gdy zamierzasz celowo przerwać wykonanie.

objc_designated_initializer

W przypadku, gdy zamierzasz wyłączyć, initaby wymusić użycie wyznaczonego inicjatora, istnieje odpowiedni atrybut:

-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

To generuje ostrzeżenie, chyba że jakakolwiek inna metoda inicjowania wywoła myOwnInitwewnętrznie. Szczegóły zostaną opublikowane w Adopting Modern Objective-C po następnym wydaniu Xcode (tak sądzę).

Jano
źródło
Może to być dobre w przypadku metod innych niż init. Skoro jeśli ta metoda jest nieprawidłowa, to dlaczego inicjalizujesz obiekt? Dodatkowo, rzucając wyjątek, będziesz mógł określić niestandardową wiadomość przekazującą programistę poprawną init*metodę, podczas gdy nie masz takiej opcji w przypadku doesNotRecognizeSelector.
Aleks N.
Nie mam pojęcia, Aleks, tego nie powinno tam być :) Poprawiłem odpowiedź.
Jano
To dziwne, ponieważ spowodowało awarię systemu. Chyba lepiej nie dopuścić do tego, żeby coś się stało, ale zastanawiam się, czy jest lepszy sposób. Chcę, aby inni programiści nie mogli go wywołać i pozwolić kompilatorowi na złapanie go podczas
``
1
Próbowałem tego i nie działa: - (id) init __attribute __ ((niedostępny ("init niedostępny"))) {NSAssert (false, @ "Use initWithType"); return nil; }
okysabeni
1
@Miraaj Wygląda na to, że nie jest obsługiwany przez Twój kompilator. Jest obsługiwany w Xcode 6. Powinieneś otrzymać komunikat „Wygodny inicjator brakuje wywołania 'self' do innego inicjatora”, jeśli inicjator nie wywołuje wyznaczonego.
Jano
101

Firma Apple zaczęła używać następujących plików nagłówkowych w celu wyłączenia konstruktora inicjującego:

- (instancetype)init NS_UNAVAILABLE;

To poprawnie wyświetla się jako błąd kompilatora w Xcode. W szczególności jest to ustawione w kilku ich plikach nagłówkowych HealthKit (jednym z nich jest HKUnit).

lehn0058
źródło
3
Zauważ jednak, że nadal możesz utworzyć instancję obiektu za pomocą [MyObject new];
José
11
Możesz także zrobić + (typ instancj) new NS_UNAVAILABLE;
sonicfly
@sonicfly Próbowałem to zrobić, ale projekt nadal się kompiluje
CyberMew
3

Jeśli mówisz o domyślnej metodzie -init, nie możesz. Jest dziedziczony z NSObject i każda klasa odpowie na to bez żadnych ostrzeżeń.

Możesz utworzyć nową metodę, powiedz -initMyClass i umieścić ją w prywatnej kategorii, jak sugeruje Matt. Następnie zdefiniuj domyślną metodę -init, aby zgłosić wyjątek, jeśli jest wywoływana lub (lepiej) wywołać prywatną -initMyClass z pewnymi wartościami domyślnymi.

Jednym z głównych powodów, dla których ludzie wydają się chcieć ukryć init, są pojedyncze obiekty . W takim przypadku nie musisz ukrywać -init, po prostu zwróć obiekt singleton (lub utwórz go, jeśli jeszcze nie istnieje).

Nathan Kinsinger
źródło
Wydaje się, że jest to lepsze podejście niż po prostu pozostawienie „init” samego w swoim singletonie i poleganie na dokumentacji, aby przekazać użytkownikowi, do którego powinien uzyskać dostęp za pośrednictwem „sharedWhthing”. Ludzie zazwyczaj nie czytają dokumentów, dopóki nie zmarnowali już wielu minut, próbując rozwiązać problem.
Greg Maletic
3

Umieść to w pliku nagłówkowym

- (id)init UNAVAILABLE_ATTRIBUTE;
Jerry Juang
źródło
Nie jest to zalecane. Nowoczesne dokumenty firmy Apple z celem-c stwierdzają, że init powinien zwracać typ instancji, a nie identyfikator. developer.apple.com/library/ios/releasenotes/ObjectiveC/…
lehn0058
5
i to jest: - (instancetype) init NS_UNAVAILABLE;
bandejapaisa
3

Możesz zadeklarować niedostępność dowolnej metody za pomocą NS_UNAVAILABLE.

Możesz więc umieścić te linie poniżej swojego @interfejsu

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;

Jeszcze lepiej zdefiniuj makro w nagłówku prefiksu

#define NO_INIT \
- (instancetype)init NS_UNAVAILABLE; \
+ (instancetype)new NS_UNAVAILABLE;

i

@interface YourClass : NSObject
NO_INIT

// Your properties and messages

@end
Kaunteya
źródło
2

To zależy od tego, co masz na myśli, mówiąc „ustaw jako prywatny”. W Objective-C wywołanie metody na obiekcie można lepiej opisać jako wysłanie wiadomości do tego obiektu. W języku nie ma nic, co zabrania klientowi wywoływania jakiejkolwiek metody na obiekcie; najlepsze co możesz zrobić to nie zadeklarować metody w pliku nagłówkowym. Jeśli klient mimo wszystko wywoła metodę „prywatną” z odpowiednim podpisem, będzie ona nadal wykonywana w czasie wykonywania.

To powiedziawszy, najczęstszym sposobem tworzenia prywatnej metody w Objective-C jest utworzenie Kategorii w pliku implementacji i zadeklarowanie wszystkich „ukrytych” tam metod. Pamiętaj, że to naprawdę nie zapobiegnie inituruchomieniu wywołań do , ale kompilator wypluje ostrzeżenia, jeśli ktoś spróbuje to zrobić.

MyClass.m

@interface MyClass (PrivateMethods)
- (NSString*) init;
@end

@implementation MyClass

- (NSString*) init
{
    // code...
}

@end

Na MacRumors.com jest przyzwoity wątek na ten temat.

Matt Dillard
źródło
3
Niestety w tym przypadku podejście do kategorii nie pomoże. Zwykle kupuje ostrzeżenia w czasie kompilacji, że metoda może nie być zdefiniowana w klasie. Jednak ponieważ MyClass musi dziedziczyć z jednej z klas głównych i definiują init, nie będzie żadnego ostrzeżenia.
Barry Wark,
2

Cóż, problem dlaczego nie możesz uczynić go "prywatnym / niewidocznym" jest taki, że metoda init zostaje wysłana na id (alokacja zwraca id) a nie do YourClass

Zauważ, że z punktu kompilatora (kontrolera) identyfikator może potencjalnie odpowiedzieć na wszystko, co kiedykolwiek zostanie wpisane (nie może sprawdzić, co naprawdę trafia do identyfikatora w czasie wykonywania), więc możesz ukryć init tylko wtedy, gdy nic nigdzie tego nie zrobi (publicly = in header) używaj metody init, niż kompilator wiedziałby, że nie ma sposobu, aby id odpowiedział na init, ponieważ nigdzie nie ma init (w twoim źródle, we wszystkich bibliotekach itp.)

więc nie możesz zabronić użytkownikowi przekazywania init i zostania zniszczonym przez kompilator ... ale co możesz zrobić, to uniemożliwić użytkownikowi uzyskanie prawdziwej instancji przez wywołanie init

po prostu implementując init, który zwraca nil i ma (prywatny / niewidoczny) inicjator, którego nazwy ktoś inny nie otrzyma (jak initOnce, initWithSpecial ...)

static SomeClass * SInstance = nil;

- (id)init
{
    // possibly throw smth. here
    return nil;
}

- (id)initOnce
{
    self = [super init];
    if (self) {
        return self;
    }
    return nil;
}

+ (SomeClass *) shared 
{
    if (nil == SInstance) {
        SInstance = [[SomeClass alloc] initOnce];
    }
    return SInstance;
}

Uwaga: ktoś mógłby to zrobić

SomeClass * c = [[SomeClass alloc] initOnce];

i faktycznie zwróciłoby nową instancję, ale gdyby initOnce nigdzie w naszym projekcie nie byłby publicznie zadeklarowany (w nagłówku), wygenerowałoby ostrzeżenie (id może nie odpowiedzieć ...) i tak czy inaczej osoba używająca tego potrzebowałaby wiedzieć dokładnie, że prawdziwym inicjatorem jest initOnce

moglibyśmy temu zapobiec, ale nie ma takiej potrzeby

Peter Lapisu
źródło
0

Muszę wspomnieć, że umieszczanie asercji i wywoływanie wyjątków w celu ukrycia metod w podklasie ma paskudną pułapkę na dobrze zamierzone.

Poleciłbym użyć tego, __unavailableco wyjaśnił Jano w swoim pierwszym przykładzie .

Metody można przesłonić w podklasach. Oznacza to, że jeśli metoda w nadklasie używa metody, która po prostu zgłasza wyjątek w podklasie, prawdopodobnie nie będzie działać zgodnie z przeznaczeniem. Innymi słowy, właśnie zepsułeś to, co kiedyś działało. Dotyczy to również metod inicjalizacji. Oto przykład takiej dość powszechnej realizacji:

- (SuperClass *)initWithParameters:(Type1 *)arg1 optional:(Type2 *)arg2
{
    ...bla bla...
    return self;
}

- (SuperClass *)initWithLessParameters:(Type1 *)arg1
{
    self = [self initWithParameters:arg1 optional:DEFAULT_ARG2];
    return self;
}

Wyobraź sobie, co się stanie z -initWithLessParameters, jeśli zrobię to w podklasie:

- (SubClass *)initWithParameters:(Type1 *)arg1 optional:(Type2 *)arg2
{
    [self release];
    [super doesNotRecognizeSelector:_cmd];
    return nil;
}

Oznacza to, że powinieneś mieć tendencję do używania metod prywatnych (ukrytych), szczególnie w metodach inicjalizacji, chyba że planujesz nadpisać metody. Ale to jest inny temat, ponieważ nie zawsze masz pełną kontrolę nad implementacją superklasy. (To sprawia, że ​​kwestionuję użycie __attribute ((objc_designated_initializer)) jako złej praktyki, chociaż nie używałem go dogłębnie.)

Oznacza to również, że można używać potwierdzeń i wyjątków w metodach, które muszą zostać przesłonięte w podklasach. (Metody „abstrakcyjne” jak w Tworzenie klasy abstrakcyjnej w Objective-C )

I nie zapomnij o + nowej metodzie klasy.

techniao
źródło