È possibile rendere privato il metodo -init in Objective-C?


147

Devo nascondere (rendere privato) il -initmetodo della mia classe in Objective-C.

Come lo posso fare?


3
Ora esiste una struttura specifica, pulita e descrittiva per raggiungere questo obiettivo, come mostrato nella seguente risposta . In particolare: NS_UNAVAILABLE. In generale, ti esorto a utilizzare questo approccio. L'OP prenderebbe in considerazione la revisione della risposta accettata? Le altre risposte qui forniscono molti dettagli utili, ma non sono il metodo preferito per raggiungere questo obiettivo.
Benjohn,

Come altri hanno notato di seguito, NS_UNAVAILABLEconsente comunque a un chiamante di invocare initindirettamente tramite new. La semplice initsostituzione di return nilgestirà entrambi i casi.
Greg Brown,

puoi ovviamente rendere "nuovo" NS_UNAVAILABLE e "init", che è oggi una pratica comune.
Motti Shneor

Risposte:


88

Objective-C, come Smalltalk, non ha il concetto di metodi "privati" contro "pubblici". Qualsiasi messaggio può essere inviato a qualsiasi oggetto in qualsiasi momento.

Quello che puoi fare è lanciare un NSInternalInconsistencyExceptionse il tuo -initmetodo è invocato:

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

L'altra alternativa - che probabilmente è molto meglio nella pratica - è -initfare qualcosa di sensato per la tua classe, se possibile.

Se stai cercando di farlo perché stai cercando di "assicurare" che venga utilizzato un oggetto singleton, non preoccuparti. Specificamente, non sprecate con "Override +allocWithZone:, -init, -retain, -release" metodo per creare singletons. È praticamente sempre inutile e sta solo aggiungendo complicazioni per nessun reale vantaggio significativo.

Invece, basta scrivere il codice in modo tale che il +sharedWhatevermetodo sia come accedere a un singleton e documentarlo come modo per ottenere l'istanza singleton nell'intestazione. Questo dovrebbe essere tutto ciò di cui hai bisogno nella stragrande maggioranza dei casi.


2
Il reso è effettivamente necessario qui?
philsquared,

5
Sì, per rendere felice il compilatore. Altrimenti il ​​compilatore potrebbe lamentarsi che non esiste un ritorno da un metodo con ritorno non nullo.
Chris Hanson,

Divertente, non per me. Forse una versione del compilatore o switch diversi? (Sto solo usando gli switch gcc predefiniti con XCode 3.1)
philsquared il

3
Contare sullo sviluppatore di seguire uno schema non è una buona idea. È meglio generare un'eccezione, quindi gli sviluppatori di un team diverso non sanno. Il concetto privato sarebbe migliore.
Nick Turner,

4
"Per nessun reale vantaggio significativo" . Completamente falso. Il vantaggio significativo è che si desidera applicare il modello singleton. Se si consente nuove istanze da creare, quindi uno sviluppatore che non ha familiarità con l'API può utilizzare alloce inite hanno la loro funzione di codice errato perché hanno la classe giusta, ma l'istanza sbagliata. Questa è l'essenza del principio dell'incapsulamento in OO. Nascondi cose nelle tue API alle quali altre classi non dovrebbero avere bisogno o cui ottenere l'accesso. Non tieni tutto pubblico e ti aspetti che gli umani ne tengano traccia.
Nate,

346

NS_UNAVAILABLE

- (instancetype)init NS_UNAVAILABLE;

Questa è la versione breve dell'attributo non disponibile. È apparso per la prima volta in macOS 10.7 e iOS 5 . È definito in NSObjCRuntime.h come #define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTE.

Esiste una versione che disabilita il metodo solo per i client Swift , non per il codice ObjC:

- (instancetype)init NS_SWIFT_UNAVAILABLE;

unavailable

Aggiungi l' unavailableattributo all'intestazione per generare un errore del compilatore su qualsiasi chiamata a init.

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

errore di compilazione

Se non hai un motivo, digita __attribute__((unavailable))o addirittura __unavailable:

-(instancetype) __unavailable init;  

doesNotRecognizeSelector:

Utilizzare doesNotRecognizeSelector:per generare un NSInvalidArgumentException. "Il sistema di runtime invoca questo metodo ogni volta che un oggetto riceve un messaggio aSelector al quale non può rispondere o inoltrare."

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

NSAssert

Utilizzare NSAssertper generare NSInternalInconsistencyException e mostrare un messaggio:

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

raise:format:

Usa raise:format:per lanciare la tua eccezione:

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

[self release]è necessario perché l'oggetto era già allocatato. Quando si utilizza ARC, il compilatore lo chiamerà per te. In ogni caso, non c'è nulla di cui preoccuparsi quando si sta per interrompere intenzionalmente l'esecuzione.

objc_designated_initializer

Nel caso in cui si desideri disabilitare initper forzare l'uso di un inizializzatore designato, esiste un attributo per questo:

-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

Questo genera un avviso a meno che qualsiasi altro metodo di inizializzazione non venga chiamato myOwnInitinternamente. I dettagli saranno pubblicati in Adopting Modern Objective-C dopo la prossima versione di Xcode (credo).


Questo può essere utile per metodi diversi da init. Poiché, se questo metodo non è valido, perché si inizializza un oggetto? Inoltre, quando si genera un'eccezione, sarà possibile specificare un messaggio personalizzato che comunica il init*metodo corretto allo sviluppatore, mentre in questo caso non si dispone di tale opzione doesNotRecognizeSelector.
Aleks N.

Nessuna idea Aleks, che non dovrebbe essere lì :) Ho modificato la risposta.
Jano,

Questo è strano perché ha finito per arrestare il sistema. Immagino sia meglio non lasciare che accada qualcosa, ma mi chiedo se esiste un modo migliore. Quello che voglio è che altri sviluppatori non siano in grado di chiamarlo e lasciarlo catturare dal compilatore quando "esegue" o "costruisce"
okysabeni,

1
Ho provato questo e non funziona: - (id) init __attribute __ ((non disponibile ("init non disponibile"))) {NSAssert (false, @ "Usa initWithType"); restituire zero; }
okysabeni,

1
@Miraaj Sembra che non sia supportato nel tuo compilatore. È supportato in Xcode 6. Dovresti ottenere "Inizializzatore di convenienza che manca una chiamata" self "a un altro inizializzatore" se un inizializzatore non chiama quello designato.
Jano,

101

Apple ha iniziato a utilizzare quanto segue nei propri file di intestazione per disabilitare il costruttore init:

- (instancetype)init NS_UNAVAILABLE;

Questo viene visualizzato correttamente come errore del compilatore in Xcode. In particolare, questo è impostato in molti dei loro file di intestazione HealthKit (HKUnit è uno di questi).


3
Si noti tuttavia che è ancora possibile creare un'istanza dell'oggetto con [MyObject new];
José,

11
Puoi anche fare + (instancetype) nuovo NS_UNAVAILABLE;
sonicfly,

@sonicfly Ho provato a farlo ma il progetto si compila ancora
CyberMew il

3

Se stai parlando del metodo predefinito -init, non puoi. È ereditato da NSObject e ogni classe risponderà senza avvertimenti.

Potresti creare un nuovo metodo, dire -initMyClass, e inserirlo in una categoria privata come suggerisce Matt. Quindi definire il metodo predefinito -init per sollevare un'eccezione se viene chiamato o (meglio) chiamare il proprio -initMyClass privato con alcuni valori predefiniti.

Uno dei motivi principali per cui le persone sembrano voler nascondere init è per gli oggetti singleton . In tal caso, non è necessario nascondere -init, restituire invece l'oggetto singleton (o crearlo se non esiste ancora).


Questo sembra un approccio migliore rispetto al semplice lasciare 'init' nel tuo singleton e fare affidamento sulla documentazione per comunicare all'utente a cui dovrebbero accedere tramite 'sharedWhatever'. Le persone in genere non leggono i documenti fino a quando non hanno già perso molti minuti cercando di capire un problema.
Greg Maletic,


3

È possibile dichiarare qualsiasi metodo non disponibile tramite NS_UNAVAILABLE.

Quindi puoi mettere queste righe sotto la tua @interfaccia

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

Ancora meglio definire una macro nell'intestazione del prefisso

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

e

@interface YourClass : NSObject
NO_INIT

// Your properties and messages

@end

2

Dipende da cosa intendi per "rendi privato". In Objective-C, chiamare un metodo su un oggetto potrebbe essere meglio descritto come inviare un messaggio a quell'oggetto. Non c'è nulla nella lingua che vieti a un client di chiamare un determinato metodo su un oggetto; il meglio che puoi fare non è dichiarare il metodo nel file di intestazione. Se un client chiama tuttavia il metodo "privato" con la firma corretta, verrà comunque eseguito in fase di esecuzione.

Detto questo, il modo più comune per creare un metodo privato in Objective-C è creare una categoria nel file di implementazione e dichiarare tutti i metodi "nascosti". Ricorda che ciò non impedirà veramente l' initesecuzione delle chiamate , ma il compilatore emetterà avvisi se qualcuno tenta di farlo.

MyClass.m

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

@implementation MyClass

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

@end

C'è un thread decente su MacRumors.com su questo argomento.


3
Sfortunatamente, in questo caso, l'approccio di categoria non sarà di grande aiuto. Normalmente acquista avvisi in fase di compilazione che il metodo potrebbe non essere definito sulla classe. Tuttavia, poiché MyClass deve ereditare da uno dei clase di root e definiscono init, non ci saranno avvisi.
Barry Wark,

2

bene il problema per cui non è possibile renderlo "privato / invisibile" è perché il metodo init viene inviato a id (poiché alloc restituisce un id) non a YourClass

Si noti che dal punto di vista del compilatore (checker) un id potrebbe rispondere in modo potenziato a qualsiasi cosa mai digitata (non è in grado di verificare ciò che realmente entra nell'id in fase di esecuzione), quindi è possibile nascondere init solo quando nulla non farebbe (pubblicamente = in header) usa un metodo init, che la compilazione saprebbe, che non c'è modo per id di rispondere a init, dato che non c'è init da nessuna parte (nella tua fonte, tutte le librerie ecc ...)

quindi non puoi vietare all'utente di passare init e di essere distrutto dal compilatore ... ma quello che puoi fare è impedire all'utente di ottenere un'istanza reale chiamando un init

semplicemente implementando init, che restituisce zero e ha un inizializzatore (privato / invisibile) che il nome che qualcun altro non otterrà (come 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;
}

Nota: che qualcuno potrebbe farlo

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

e in effetti restituirebbe una nuova istanza, ma se initOnce non venisse dichiarato pubblicamente (nell'intestazione) da nessuna parte nel nostro progetto, genererebbe un avviso (l'id potrebbe non rispondere ...) e comunque la persona che lo utilizza, avrebbe bisogno sapere esattamente che il vero inizializzatore è initOnce

potremmo impedirlo ancora di più, ma non è necessario


0

Devo dire che porre asserzioni e sollevare eccezioni per nascondere i metodi nella sottoclasse ha una cattiva trappola per i ben intenzionati.

Consiglierei l'uso __unavailablecome spiegato da Jano per il suo primo esempio .

I metodi possono essere sovrascritti in sottoclassi. Ciò significa che se un metodo nella superclasse utilizza un metodo che genera un'eccezione nella sottoclasse, probabilmente non funzionerà come previsto. In altre parole, hai appena rotto ciò che funzionava. Questo vale anche con i metodi di inizializzazione. Ecco un esempio di tale implementazione piuttosto comune:

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

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

Immagina cosa succede a -initWithLessParameters, se lo faccio nella sottoclasse:

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

Ciò implica che dovresti tendere ad usare metodi privati ​​(nascosti), specialmente nei metodi di inizializzazione, a meno che tu non preveda di sovrascrivere i metodi. Ma questo è un altro argomento, dal momento che non si ha sempre il pieno controllo dell'implementazione della superclasse. (Questo mi fa mettere in dubbio l'uso di __attribute ((objc_designated_initializer)) come cattiva pratica, anche se non l'ho usato in profondità.)

Implica anche che è possibile utilizzare asserzioni ed eccezioni nei metodi che devono essere sovrascritti nelle sottoclassi. (I metodi "astratti" come in Creazione di una classe astratta in Objective-C )

E, non dimenticare il + nuovo metodo di classe.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.