¿Es posible hacer que el método -init sea privado en Objective-C?

147

Necesito ocultar (hacer privado) el -initmétodo de mi clase en Objective-C.

¿Cómo puedo hacer eso?

lajos
fuente
3
Ahora hay una instalación específica, limpia y descriptiva para lograr esto, como se muestra en esta respuesta a continuación . Específicamente: NS_UNAVAILABLE. En general, le recomiendo que utilice este enfoque. ¿El OP consideraría revisar su respuesta aceptada? Las otras respuestas aquí proporcionan muchos detalles útiles, pero no son el método preferido para lograrlo.
Benjohn
Como otros han señalado a continuación, NS_UNAVAILABLEaún permite que una persona que llama invoque initindirectamente a través de new. Simplemente anular el initretorno nilmanejará ambos casos.
Greg Brown

Respuestas:

88

Objective-C, como Smalltalk, no tiene concepto de métodos "privados" versus métodos "públicos". Cualquier mensaje puede enviarse a cualquier objeto en cualquier momento.

Lo que puede hacer es arrojar un NSInternalInconsistencyExceptionsi -initse invoca su método:

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

La otra alternativa, que probablemente sea mucho mejor en la práctica, es hacer -initalgo sensato para su clase si es posible.

Si está tratando de hacer esto porque está tratando de "asegurarse" de que se use un objeto singleton, no se moleste. Específicamente, no se moleste con el "override +allocWithZone:, -init, -retain, -release" método de crear singletons. Prácticamente siempre es innecesario y solo agrega complicaciones para no tener una ventaja real significativa.

En su lugar, simplemente escriba su código de manera que su +sharedWhatevermétodo sea cómo accede a un singleton y documente eso como la forma de obtener la instancia de singleton en su encabezado. Eso debería ser todo lo que necesita en la gran mayoría de los casos.

Chris Hanson
fuente
2
¿Es realmente necesario el retorno aquí?
philsquared
55
Sí, para mantener feliz al compilador. De lo contrario, el compilador puede quejarse de que no hay devolución de un método con devolución no nula.
Chris Hanson
Gracioso, no es para mí. ¿Quizás una versión o un compilador diferente? (Solo estoy usando los interruptores gcc predeterminados con XCode 3.1)
philsquared
3
Contar con que el desarrollador siga un patrón no es una buena idea. Es mejor lanzar una excepción, por lo que los desarrolladores de un equipo diferente saben que no deben hacerlo. Yo concepto privado sería mejor.
Nick Turner
44
"Por ninguna ventaja significativa real" . Completamente falso. La ventaja significativa es que desea aplicar el patrón singleton. Si permite que los nuevos casos que se creen, a continuación, un desarrollador que no está familiarizado con la API puede utilizar allocy inity tienen su función de código de forma incorrecta, ya que tienen la clase correcta, pero la instancia equivocada. Esta es la esencia del principio de encapsulación en OO. Esconde cosas en sus API a las que otras clases no deberían necesitar u obtener acceso. No solo mantienes todo público y esperas que los humanos hagan un seguimiento de todo.
Nate
345

NS_UNAVAILABLE

- (instancetype)init NS_UNAVAILABLE;

Esta es una versión corta del atributo no disponible. Apareció por primera vez en macOS 10.7 y iOS 5 . Se define en NSObjCRuntime.h como #define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTE.

Hay una versión que deshabilita el método solo para clientes Swift , no para el código ObjC:

- (instancetype)init NS_SWIFT_UNAVAILABLE;

unavailable

Agregue el unavailableatributo al encabezado para generar un error del compilador en cualquier llamada a init.

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

error de tiempo de compilación

Si no tiene una razón, simplemente escriba __attribute__((unavailable)), o incluso __unavailable:

-(instancetype) __unavailable init;  

doesNotRecognizeSelector:

Se usa doesNotRecognizeSelector:para generar una NSInvalidArgumentException. "El sistema de tiempo de ejecución invoca este método cada vez que un objeto recibe un mensaje aSelector al que no puede responder o reenviar".

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

NSAssert

Use NSAssertpara lanzar NSInternalInconsistencyException y mostrar un mensaje:

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

raise:format:

Use raise:format:para lanzar su propia excepción:

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

[self release]es necesario porque el objeto ya estaba allocalterado. Cuando use ARC, el compilador lo llamará por usted. En cualquier caso, no es algo de lo que preocuparse cuando está a punto de detener intencionalmente la ejecución.

objc_designated_initializer

En caso de que tenga la intención de deshabilitar initpara forzar el uso de un inicializador designado, hay un atributo para eso:

-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

Esto genera una advertencia a menos que cualquier otro método inicializador llame myOwnInitinternamente. Los detalles se publicarán en Adopting Modern Objective-C después del próximo lanzamiento de Xcode (supongo).

Jano
fuente
Esto puede ser bueno para otros métodos que no sean init. Dado que, si este método no es válido, ¿por qué inicializa un objeto? Además, al lanzar una excepción, podrá especificar algún mensaje personalizado que comunique el init*método correcto al desarrollador, mientras que no tiene esa opción en caso de doesNotRecognizeSelector.
Aleks
No tengo idea Aleks, eso no debería estar allí :) Edité la respuesta.
Jano
Esto es extraño porque terminó bloqueando su sistema. Supongo que es mejor no dejar que pase algo, pero me pregunto si hay una mejor manera. Lo que quiero es que otros desarrolladores no puedan llamarlo y dejar que el compilador lo
atrape
1
Intenté esto y no funciona: - (id) init __attribute __ ((no disponible ("init no disponible"))) {NSAssert (false, @ "Use initWithType"); volver nulo }
okysabeni
1
@Miraaj Parece que no es compatible con tu compilador. Es compatible con Xcode 6. Debería obtener “Inicializador de conveniencia que pierde una llamada 'propia' a otro inicializador” si un inicializador no llama al designado.
Jano
101

Apple ha comenzado a usar lo siguiente en sus archivos de encabezado para deshabilitar el constructor init:

- (instancetype)init NS_UNAVAILABLE;

Esto se muestra correctamente como un error del compilador en Xcode. Específicamente, esto se establece en varios de sus archivos de encabezado HealthKit (HKUnit es uno de ellos).

lehn0058
fuente
3
Sin embargo, tenga en cuenta que aún puede crear una instancia del objeto con [MyObject new];
José
11
También puede hacer + (tipo de instancia) nuevo NS_UNAVAILABLE;
sonicfly
@sonicfly Intenté hacer eso, pero el proyecto aún se compila
CyberMew
3

Si está hablando del método predeterminado de inicio, entonces no puede. Se hereda de NSObject y todas las clases responderán sin advertencias.

Puede crear un nuevo método, digamos -initMyClass, y ponerlo en una categoría privada como Matt sugiere. Luego defina el método predeterminado -init para generar una excepción si se llama o (mejor) llame a su privado -initMyClass con algunos valores predeterminados.

Una de las razones principales por las que las personas parecen querer ocultar init es por los objetos singleton . Si ese es el caso, entonces no necesita ocultar -init, solo devuelva el objeto singleton en su lugar (o créelo si aún no existe).

Nathan Kinsinger
fuente
Este parece ser un mejor enfoque que simplemente dejar 'init' solo en su singleton y confiar en la documentación para comunicar al usuario que se supone que debe acceder a través de 'sharedWhatever'. Por lo general, las personas no leen documentos hasta que ya han perdido muchos minutos tratando de resolver un problema.
Greg Maletic
3

Pon esto en el archivo de encabezado

- (id)init UNAVAILABLE_ATTRIBUTE;
Jerry Juang
fuente
Esto no es recomendable. Los documentos modernos del objetivo c de Apple establecen que init debería devolver el tipo de instancia, no el id. developer.apple.com/library/ios/releasenotes/ObjectiveC/…
lehn0058
55
y es: - (tipo de instancia) init NS_UNAVAILABLE;
bandejapaisa
3

Puede declarar que cualquier método no está disponible usando NS_UNAVAILABLE.

Entonces puedes poner estas líneas debajo de tu @interface

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

Aún mejor defina una macro en su encabezado de prefijo

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

y

@interface YourClass : NSObject
NO_INIT

// Your properties and messages

@end
Kaunteya
fuente
2

Eso depende de lo que quieras decir con "hacer privado". En Objective-C, llamar a un método en un objeto podría describirse mejor como enviar un mensaje a ese objeto. No hay nada en el lenguaje que prohíba a un cliente llamar a un método dado en un objeto; Lo mejor que puede hacer es no declarar el método en el archivo de encabezado. Sin embargo, si un cliente llama al método "privado" con la firma correcta, aún se ejecutará en tiempo de ejecución.

Dicho esto, la forma más común de crear un método privado en Objective-C es crear una Categoría en el archivo de implementación y declarar todos los métodos "ocultos" allí. Recuerde que esto realmente no impedirá que se initejecuten llamadas a , pero el compilador emitirá advertencias si alguien intenta hacer esto.

MyClass.m

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

@implementation MyClass

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

@end

Hay un hilo decente en MacRumors.com sobre este tema.

Matt Dillard
fuente
3
Desafortunadamente, en este caso, el enfoque de categoría realmente no ayudará. Normalmente le compra advertencias en tiempo de compilación de que el método puede no estar definido en la clase. Sin embargo, dado que MyClass debe heredar de una de las clases raíz y definen init, no habrá advertencia.
Barry Wark
2

bueno, el problema por el que no puedes hacerlo "privado / invisible" es porque el método init se envía a id (ya que alloc devuelve un id) no a YourClass

Tenga en cuenta que desde el punto del compilador (verificador) una identificación podría responder de manera potente a cualquier cosa que se haya escrito (no puede verificar lo que realmente entra en la identificación en tiempo de ejecución), por lo que puede ocultar init solo cuando nada en ninguna parte lo haría (publicly = in encabezado) use un método init, de lo que la compilación sabría, que no hay forma de que la identificación responda a init, ya que no hay init en ningún lugar (en su fuente, todas las bibliotecas, etc.)

por lo que no puede prohibir al usuario que pase init y que el compilador lo rompa ... pero lo que puede hacer es evitar que el usuario obtenga una instancia real llamando a un init

simplemente implementando init, que devuelve nil y tiene un inicializador (privado / invisible) cuyo nombre alguien más no obtendrá (como 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: que alguien podría hacer esto

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

y de hecho devolvería una nueva instancia, pero si initOnce no se declararía públicamente en ninguna parte de nuestro proyecto (en el encabezado), generaría una advertencia (la identificación podría no responder ...) y de todos modos la persona que usa esto, necesitaría saber exactamente que el inicializador real es initOnce

podríamos evitar esto aún más, pero no hay necesidad

Peter Lapisu
fuente
0

Tengo que mencionar que colocar afirmaciones y plantear excepciones para ocultar métodos en la subclase tiene una trampa desagradable para los bien intencionados.

Recomendaría usar __unavailablecomo Jano explicó para su primer ejemplo .

Los métodos pueden anularse en subclases. Esto significa que si un método en la superclase usa un método que solo genera una excepción en la subclase, probablemente no funcionará como se esperaba. En otras palabras, acabas de romper lo que solía funcionar. Esto también es cierto con los métodos de inicialización. Aquí hay un ejemplo de una implementación tan común:

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

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

Imagina lo que sucede con -initWithLessParameters, si hago esto en la subclase:

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

Esto implica que debe tender a utilizar métodos privados (ocultos), especialmente en los métodos de inicialización, a menos que planee anularlos. Pero, este es otro tema, ya que no siempre tiene el control total en la implementación de la superclase. (Esto me hace cuestionar el uso de __attribute ((objc_designated_initializer)) como mala práctica, aunque no lo he usado en profundidad).

También implica que puede usar aserciones y excepciones en métodos que deben ser anulados en subclases. (Los métodos "abstractos" como en Crear una clase abstracta en Objective-C )

Y no te olvides del método + nueva clase.

techniao
fuente