Est-il possible de rendre la méthode -init privée en Objective-C?

147

J'ai besoin de cacher (rendre privée) la -initméthode de ma classe en Objective-C.

Comment puis je faire ça?

lajos
la source
3
Il existe maintenant une fonction spécifique, propre et descriptive pour y parvenir, comme indiqué dans cette réponse ci-dessous . Plus précisément: NS_UNAVAILABLE. Je vous exhorte généralement à utiliser cette approche. Le PO envisagerait-il de réviser sa réponse acceptée? Les autres réponses ici fournissent beaucoup de détails utiles, mais ne sont pas la méthode préférée pour y parvenir.
Benjohn
Comme d'autres l'ont noté ci-dessous, NS_UNAVAILABLEpermet toujours à un appelant d'appeler initindirectement via new. Le simple fait de remplacer initpar return niltraitera les deux cas.
Greg Brown
vous pouvez bien sûr créer «new» NS_UNAVAILABLE ainsi que «init» - ce qui est aujourd'hui une pratique courante.
Motti Shneor

Réponses:

88

Objective-C, comme Smalltalk, n'a aucun concept de méthodes «privées» ou «publiques». Tout message peut être envoyé à n'importe quel objet à tout moment.

Ce que vous pouvez faire est de lancer un NSInternalInconsistencyExceptionsi votre -initméthode est appelée:

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

L'autre alternative - qui est probablement bien meilleure en pratique - est de faire -initquelque chose de sensé pour votre classe si possible.

Si vous essayez de faire cela parce que vous essayez de "vous assurer" qu'un objet singleton est utilisé, ne vous inquiétez pas. Plus précisément, ne vous embêtez pas avec la « dérogation +allocWithZone:, -init, -retain, -release» méthode de création singletons. C'est pratiquement toujours inutile et ne fait qu'ajouter des complications sans véritable avantage significatif.

Au lieu de cela, écrivez simplement votre code de manière à ce que votre +sharedWhateverméthode soit la façon dont vous accédez à un singleton, et documentez-le comme le moyen d'obtenir l'instance de singleton dans votre en-tête. Cela devrait être tout ce dont vous avez besoin dans la grande majorité des cas.

Chris Hanson
la source
2
Le retour est-il réellement nécessaire ici?
philsquared
5
Oui, pour satisfaire le compilateur. Sinon, le compilateur peut se plaindre de l'absence de retour d'une méthode avec un retour non void.
Chris Hanson
C'est drôle, ce n'est pas le cas pour moi. Peut-être une version de compilateur ou des commutateurs différents? (J'utilise juste les commutateurs gcc par défaut avec XCode 3.1)
philsquared
3
Compter sur le développeur pour suivre un modèle n'est pas une bonne idée. Il est préférable de lancer une exception, afin que les développeurs d'une autre équipe ne le sachent pas. Je concept privé serait mieux.
Nick Turner
4
"Sans réel avantage significatif" . Complètement faux. L'avantage significatif est que vous souhaitez appliquer le modèle singleton. Si vous permettez à de nouvelles instances à créer, puis un développeur qui est pas familier avec l'API peut utiliser allocet initet ont leur fonction de code mal parce qu'ils ont la bonne classe, mais le mauvais exemple. C'est l'essence du principe de l' encapsulation en OO. Vous cachez dans vos API des éléments auxquels les autres classes ne devraient pas avoir besoin, ou auxquelles ils n'ont pas accès. Vous ne gardez pas seulement tout public et vous attendez des humains à garder une trace de tout cela.
Nate
346

NS_UNAVAILABLE

- (instancetype)init NS_UNAVAILABLE;

Il s'agit d'une version courte de l'attribut indisponible. Il est apparu pour la première fois dans macOS 10.7 et iOS 5 . Il est défini dans NSObjCRuntime.h comme #define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTE.

Il existe une version qui désactive la méthode uniquement pour les clients Swift , pas pour le code ObjC:

- (instancetype)init NS_SWIFT_UNAVAILABLE;

unavailable

Ajoutez l' unavailableattribut à l'en-tête pour générer une erreur du compilateur sur tout appel à init.

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

erreur de compilation

Si vous n'avez pas de raison, tapez simplement __attribute__((unavailable)), ou même __unavailable:

-(instancetype) __unavailable init;  

doesNotRecognizeSelector:

Utilisez doesNotRecognizeSelector:pour déclencher une NSInvalidArgumentException. "Le système d'exécution appelle cette méthode chaque fois qu'un objet reçoit un message aSelector auquel il ne peut pas répondre ou le transférer."

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

NSAssert

Utilisez NSAssertpour lancer NSInternalInconsistencyException et afficher un message:

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

raise:format:

Utilisez raise:format:pour lancer votre propre exception:

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

[self release]est nécessaire car l'objet a déjà été allocaffecté. Lors de l'utilisation d'ARC, le compilateur l'appellera pour vous. Dans tous les cas, pas de quoi s'inquiéter lorsque vous êtes sur le point d'arrêter intentionnellement l'exécution.

objc_designated_initializer

Dans le cas où vous avez l'intention de désactiver initpour forcer l'utilisation d'un initialiseur désigné, il existe un attribut pour cela:

-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

Cela génère un avertissement à moins qu'une autre méthode d'initialisation n'appelle en myOwnInitinterne. Les détails seront publiés dans Adopting Modern Objective-C après la prochaine version de Xcode (je suppose).

Jano
la source
Cela peut être bon pour des méthodes autres que init. Depuis, si cette méthode n'est pas valide, pourquoi initialisez-vous un objet? De plus, lors de la levée d'une exception, vous pourrez spécifier un message personnalisé communiquant la bonne init*méthode au développeur, alors que vous n'avez pas cette option dans le cas de doesNotRecognizeSelector.
Aleks N.
Aucune idée Aleks, ça ne devrait pas être là :) J'ai édité la réponse.
Jano
C'est bizarre car cela a fini par planter votre système. Je suppose qu'il vaut mieux ne pas laisser quelque chose se produire, mais je me demande s'il existe une meilleure façon. Ce que je veux, c'est que les autres développeurs ne puissent pas l'appeler et le laisser rattraper par le compilateur lors de `` l'exécution '' ou de la `` construction ''
okysabeni
1
J'ai essayé cela et cela ne fonctionne pas: - (id) init __attribute __ ((indisponible ("init not available"))) {NSAssert (false, @ "Use initWithType"); retour nul; }
okysabeni
1
@Miraaj On dirait que ce n'est pas pris en charge sur votre compilateur. Il est pris en charge dans Xcode 6. Vous devriez obtenir «L'initialiseur de commodité manque un appel« auto »à un autre initialiseur» si un initialiseur n'appelle pas celui désigné.
Jano
101

Apple a commencé à utiliser ce qui suit dans ses fichiers d'en-tête pour désactiver le constructeur init:

- (instancetype)init NS_UNAVAILABLE;

Cela s'affiche correctement comme une erreur de compilation dans Xcode. Plus précisément, cela est défini dans plusieurs de leurs fichiers d'en-tête HealthKit (HKUnit en fait partie).

lehn0058
la source
3
Notez cependant que vous pouvez toujours instancier l'objet avec [MyObject new];
José
11
Vous pouvez également faire + (instancetype) new NS_UNAVAILABLE;
sonicfly
@sonicfly J'ai essayé de le faire mais le projet se compile toujours
CyberMew
3

Si vous parlez de la méthode par défaut -init, vous ne pouvez pas. Il est hérité de NSObject et chaque classe y répondra sans avertissement.

Vous pouvez créer une nouvelle méthode, par exemple -initMyClass, et la mettre dans une catégorie privée comme le suggère Matt. Définissez ensuite la méthode par défaut -init pour lever une exception si elle est appelée ou (mieux) appeler votre -initMyClass privé avec des valeurs par défaut.

L'une des principales raisons pour lesquelles les gens semblent vouloir cacher init est pour les objets singleton . Si tel est le cas, vous n'avez pas besoin de masquer -init, retournez simplement l'objet singleton à la place (ou créez-le s'il n'existe pas encore).

Nathan Kinsinger
la source
Cela semble être une meilleure approche que de laisser simplement «init» seul dans votre singleton et de compter sur la documentation pour communiquer à l'utilisateur auquel il est censé accéder via «sharedWwhat». Les gens ne lisent généralement pas les documents avant d'avoir déjà perdu plusieurs minutes à essayer de résoudre un problème.
Greg Maletic
3

Mettez ceci dans le fichier d'en-tête

- (id)init UNAVAILABLE_ATTRIBUTE;
Jerry Juang
la source
Ceci n'est pas recommandé. Les documents object-c modernes d'Apple indiquent que init doit renvoyer le type d'instance, pas id. developer.apple.com/library/ios/releasenotes/ObjectiveC/…
lehn0058
5
et c'est: - (instancetype) init NS_UNAVAILABLE;
bandejapaisa
3

Vous pouvez déclarer toute méthode indisponible en utilisant NS_UNAVAILABLE.

Vous pouvez donc mettre ces lignes sous votre @interface

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

Encore mieux définir une macro dans votre en-tête de préfixe

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

et

@interface YourClass : NSObject
NO_INIT

// Your properties and messages

@end
Kaunteya
la source
2

Cela dépend de ce que vous entendez par «rendre privé». En Objective-C, l'appel d'une méthode sur un objet peut être mieux décrit comme l'envoi d'un message à cet objet. Il n'y a rien dans le langage qui empêche un client d'appeler une méthode donnée sur un objet; le mieux que vous puissiez faire est de ne pas déclarer la méthode dans le fichier d'en-tête. Si un client appelle néanmoins la méthode "privée" avec la bonne signature, elle s'exécutera toujours à l'exécution.

Cela dit, la manière la plus courante de créer une méthode privée en Objective-C est de créer une catégorie dans le fichier d'implémentation et de déclarer toutes les méthodes "cachées" qui s'y trouvent. Souvenez-vous que cela n'empêchera pas vraiment l' initexécution des appels à , mais le compilateur crachera des avertissements si quelqu'un essaie de le faire.

MyClass.m

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

@implementation MyClass

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

@end

Il y a un fil décent sur MacRumors.com à ce sujet.

Matt Dillard
la source
3
Malheureusement, dans ce cas, l'approche par catégorie n'aidera pas vraiment. Normalement, il vous achète des avertissements au moment de la compilation indiquant que la méthode peut ne pas être définie sur la classe. Cependant, comme MyClass doit hériter de l'une des classes racine et qu'elles définissent init, il n'y aura pas d'avertissement.
Barry Wark
2

Eh bien, le problème pour lequel vous ne pouvez pas le rendre "privé / invisible" est que la méthode init est envoyée à id (car alloc renvoie un id) et non à YourClass

Notez qu'à partir du point du compilateur (vérificateur), un identifiant pourrait potentiellement répondre à tout ce qui a été tapé (il ne peut pas vérifier ce qui entre vraiment dans l'identifiant au moment de l'exécution), donc vous ne pouvez masquer init que lorsque rien ne le ferait nulle part (publiquement = in header) utilisez une méthode init, que la compilation sait, qu'il n'y a aucun moyen pour id de répondre à init, car il n'y a aucun init nulle part (dans votre source, toutes les libs etc ...)

donc vous ne pouvez pas interdire à l'utilisateur de passer init et d'être écrasé par le compilateur ... mais ce que vous pouvez faire, c'est empêcher l'utilisateur d'obtenir une instance réelle en appelant un init

simplement en implémentant init, qui renvoie nil et dispose d'un initialiseur (privé / invisible) dont le nom que quelqu'un d'autre ne recevra pas (comme 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;
}

Remarque: que quelqu'un pourrait le faire

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

et il renverrait en fait une nouvelle instance, mais si l'initOnce n'était nulle part dans notre projet déclaré publiquement (dans l'en-tête), il générerait un avertissement (id pourrait ne pas répondre ...) et de toute façon la personne qui l'utilise aurait besoin pour savoir exactement que le vrai initialiseur est l'initOnce

nous pourrions empêcher cela encore plus, mais il n'est pas nécessaire

Peter Lapisu
la source
0

Je dois mentionner que placer des assertions et lever des exceptions pour masquer des méthodes dans la sous-classe a un piège méchant pour les bien intentionnés.

Je recommanderais d'utiliser __unavailablecomme Jano l'a expliqué pour son premier exemple .

Les méthodes peuvent être remplacées dans les sous-classes. Cela signifie que si une méthode de la superclasse utilise une méthode qui lève simplement une exception dans la sous-classe, elle ne fonctionnera probablement pas comme prévu. En d'autres termes, vous venez de casser ce qui fonctionnait auparavant. Cela est également vrai avec les méthodes d'initialisation. Voici un exemple d'une telle implémentation assez courante:

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

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

Imaginez ce qui arrive à -initWithLessParameters, si je fais cela dans la sous-classe:

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

Cela implique que vous devriez avoir tendance à utiliser des méthodes privées (masquées), en particulier dans les méthodes d'initialisation, à moins que vous ne prévoyiez de remplacer les méthodes. Mais c'est un autre sujet, car vous n'avez pas toujours un contrôle total sur l'implémentation de la superclasse. (Cela me fait remettre en question l'utilisation de __attribute ((objc_designated_initializer)) comme une mauvaise pratique, bien que je ne l'ai pas utilisé en profondeur.)

Cela implique également que vous pouvez utiliser des assertions et des exceptions dans les méthodes qui doivent être remplacées dans les sous-classes. (Les méthodes "abstraites" comme dans Créer une classe abstraite en Objective-C )

Et n'oubliez pas la + nouvelle méthode de classe.

techniao
la source