É possível tornar o método -init privado no Objective-C?

147

Preciso ocultar (tornar privado) o -initmétodo da minha classe no Objective-C.

Como eu posso fazer isso?

lajos
fonte
3
Agora existe um recurso específico, limpo e descritivo para conseguir isso, conforme mostrado nesta resposta abaixo . Especificamente: NS_UNAVAILABLE. Geralmente, exorto você a usar essa abordagem. O OP consideraria revisar sua resposta aceita? As outras respostas aqui fornecem muitos detalhes úteis, mas não são o método preferido para conseguir isso.
Benjohn
Como outros observaram abaixo, NS_UNAVAILABLEainda permite que um chamador invoque initindiretamente via new. Simplesmente substituir initpara retornar niltratará dos dois casos.
Greg Brown

Respostas:

88

O Objetivo-C, como Smalltalk, não tem conceito de métodos "privados" versus "públicos". Qualquer mensagem pode ser enviada para qualquer objeto a qualquer momento.

O que você pode fazer é lançar um NSInternalInconsistencyExceptionse seu -initmétodo for chamado:

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

A outra alternativa - que provavelmente é muito melhor na prática - é -initfazer algo sensato para a sua classe, se possível.

Se você está tentando fazer isso porque está tentando "garantir" que um objeto singleton seja usado, não se preocupe. Especificamente, não se incomode com o "override +allocWithZone:, -init, -retain, -release" método de criação de singletons. É quase sempre desnecessário e está apenas adicionando complicações sem nenhuma vantagem significativa.

Em vez disso, basta escrever seu código de forma que seu +sharedWhatevermétodo seja como você acessa um singleton e documentar isso como forma de obter a instância singleton em seu cabeçalho. Isso deve ser tudo o que você precisa na grande maioria dos casos.

Chris Hanson
fonte
2
O retorno é realmente necessário aqui?
philsquared 16/02/09
5
Sim, para manter o compilador feliz. Caso contrário, o compilador pode reclamar que não há retorno de um método com retorno não nulo.
21320 Chris Hanson
Engraçado, não é para mim. Talvez uma versão ou opções diferentes do compilador? (Eu só estou usando o gcc padrão muda com o Xcode 3.1)
philsquared
3
Contar com o desenvolvedor para seguir um padrão não é uma boa ideia. É melhor lançar uma exceção, para que os desenvolvedores de uma equipe diferente saibam. Eu conceito privado seria melhor.
Nick Turner
4
"Para nenhuma vantagem realmente significativa" . Completamente falso. A vantagem significativa é que você deseja aplicar o padrão singleton. Se você permitir que novas instâncias a ser criado, em seguida, um desenvolvedor que não está familiarizado com a API pode usar alloce inite têm a sua função de código de forma incorrecta, porque eles têm a classe certa, mas a instância errada. Essa é a essência do princípio do encapsulamento no OO. Você oculta coisas em suas APIs às quais outras classes não precisam ou têm acesso. Você não apenas mantém tudo público e espera que os humanos acompanhem tudo.
Nate
345

NS_UNAVAILABLE

- (instancetype)init NS_UNAVAILABLE;

Esta é a versão curta do atributo indisponível. Ele apareceu pela primeira vez no macOS 10.7 e iOS 5 . É definido em NSObjCRuntime.h como #define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTE.

Existe uma versão que desativa o método apenas para clientes Swift , não para o código ObjC:

- (instancetype)init NS_SWIFT_UNAVAILABLE;

unavailable

Adicione o unavailableatributo ao cabeçalho para gerar um erro do compilador em qualquer chamada para init.

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

erro de tempo de compilação

Se você não tiver um motivo, basta digitar __attribute__((unavailable))ou até __unavailable:

-(instancetype) __unavailable init;  

doesNotRecognizeSelector:

Use doesNotRecognizeSelector:para gerar uma NSInvalidArgumentException. "O sistema de tempo de execução chama esse método sempre que um objeto recebe uma mensagem aSelector que não pode responder ou encaminhar."

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

NSAssert

Use NSAssertpara lançar NSInternalInconsistencyException e mostrar uma mensagem:

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

raise:format:

Use raise:format:para lançar sua própria exceção:

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

[self release]é necessário porque o objeto já foi allocatado. Ao usar o ARC, o compilador o chamará para você. De qualquer forma, não é algo para se preocupar quando você está prestes a parar intencionalmente a execução.

objc_designated_initializer

Caso você pretenda desativar initpara forçar o uso de um inicializador designado, há um atributo para isso:

-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

Isso gera um aviso, a menos que qualquer outro método inicializador chame myOwnInitinternamente. Os detalhes serão publicados em Adotting Modern Objective-C após o próximo lançamento do Xcode (eu acho).

Jano
fonte
Isso pode ser bom para outros métodos que não init. Como esse método é inválido, por que você inicializa um objeto? Além disso, ao lançar uma exceção, você poderá especificar alguma mensagem personalizada comunicando o init*método correto ao desenvolvedor, enquanto não tiver essa opção no caso de doesNotRecognizeSelector.
Aleks N.
Não faço ideia, Aleks, que não deveria estar lá :) Editei a resposta.
Jano
Isso é estranho, porque acabou travando seu sistema. Eu acho que é melhor não deixar que algo aconteça, mas eu estou querendo saber se existe uma maneira melhor. O que eu quero é que outros desenvolvedores para não ser capaz de chamá-lo e deixá-lo ser pego pelo compilador quando 'correndo' ou 'construir'
okysabeni
1
Eu tentei isso e não funciona: - (id) init __attribute __ ((indisponível ("init não disponível")))) {NSAssert (false, @ "Use initWithType"); retorno nulo; }
okysabeni
1
@Miraaj Parece que não há suporte para o seu compilador. É suportado no Xcode 6. Você deve obter “Inicializador de conveniência perdendo uma chamada 'auto' para outro inicializador” ”se um inicializador não chamar o designado.
Jano
101

A Apple começou a usar o seguinte em seus arquivos de cabeçalho para desativar o construtor init:

- (instancetype)init NS_UNAVAILABLE;

Isso é exibido corretamente como um erro de compilador no Xcode. Especificamente, isso é definido em vários de seus arquivos de cabeçalho HealthKit (o HKUnit é um deles).

lehn0058
fonte
3
Observe que você ainda pode instanciar o objeto com [MyObject new];
José José
11
Você também pode criar + (instancetype) new NS_UNAVAILABLE;
sonicfly 9/09/15
@sonicfly tentei fazer isso, mas o projeto ainda compila
CyberMew
3

Se você está falando sobre o método -init padrão, não pode. Ele é herdado do NSObject e todas as classes responderão a ele sem avisos.

Você pode criar um novo método, digamos -initMyClass, e colocá-lo em uma categoria particular, como sugere Matt. Em seguida, defina o método -init padrão para gerar uma exceção se for chamado ou (melhor) chamar seu -initMyClass privado com alguns valores padrão.

Uma das principais razões pelas quais as pessoas parecem querer ocultar o init é para objetos singleton . Se for esse o caso, não será necessário ocultar -init, basta retornar o objeto singleton (ou crie-o se ainda não existir).

Nathan Kinsinger
fonte
Essa parece ser uma abordagem melhor do que apenas deixar 'init' sozinho no seu singleton e confiar na documentação para se comunicar com o usuário que ele deveria acessar via 'sharedWhatever'. As pessoas normalmente não lêem documentos até que já tenham perdido muitos minutos tentando descobrir um problema.
Greg Maletic
3

Coloque isso no arquivo de cabeçalho

- (id)init UNAVAILABLE_ATTRIBUTE;
Jerry Juang
fonte
Isto não é recomendado. Os documentos modernos do objetivo-c da Apple afirmam que o init deve retornar o tipo de instância, não o id. developer.apple.com/library/ios/releasenotes/ObjectiveC/…
lehn0058 9/09/15
5
e é: - (tipo de instância) init NS_UNAVAILABLE;
bandeja paisa
3

Você pode declarar que qualquer método não está disponível usando NS_UNAVAILABLE.

Então você pode colocar essas linhas abaixo do seu @interface

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

Ainda melhor, defina uma macro no cabeçalho do prefixo

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

e

@interface YourClass : NSObject
NO_INIT

// Your properties and messages

@end
Kaunteya
fonte
2

Isso depende do que você quer dizer com "tornar privado". No Objective-C, chamar um método em um objeto pode ser melhor descrito como enviar uma mensagem para esse objeto. Não há nada na linguagem que proíba um cliente de chamar qualquer método em um objeto; o melhor que você pode fazer é não declarar o método no arquivo de cabeçalho. No entanto, se um cliente chamar o método "private" com a assinatura correta, ele ainda será executado em tempo de execução.

Dito isso, a maneira mais comum de criar um método privado no Objective-C é criar uma categoria no arquivo de implementação e declarar todos os métodos "ocultos" nele. Lembre-se de que isso realmente não impedirá a initexecução de chamadas , mas o compilador emitirá avisos se alguém tentar fazer isso.

MyClass.m

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

@implementation MyClass

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

@end

Há uma discussão decente no MacRumors.com sobre esse tópico.

Matt Dillard
fonte
3
Infelizmente, nesse caso, a abordagem por categoria não ajudará realmente. Normalmente, você recebe avisos em tempo de compilação de que o método pode não estar definido na classe. No entanto, como MyClass deve herdar de um dos clases raiz e eles definem init, não haverá aviso.
Barry Wark
2

Bem, o problema por que você não pode torná-lo "privado / invisível" é porque o método init é enviado para o ID (pois o atributo retorna um ID), não para o YourClass

Observe que, a partir do ponto do compilador (verificador), um ID pode responder potencialmente a qualquer coisa digitada (não pode verificar o que realmente entra no ID em tempo de execução); portanto, você pode ocultar o init apenas quando nada em lugar algum (publicamente = in cabeçalho) use um método init, do que a compilação saberia, que não há como o id responder ao init, pois não existe init em nenhum lugar (em sua fonte, todas as bibliotecas etc ...)

então você não pode proibir que o usuário passe init e seja esmagado pelo compilador ... mas o que você pode fazer é impedir que o usuário obtenha uma instância real chamando um init

simplesmente implementando init, que retorna nulo e possui um inicializador (privado / invisível) cujo nome alguém não receberá (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: alguém poderia fazer isso

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

e, de fato, retornaria uma nova instância, mas se o initOnce em nenhum lugar do nosso projeto fosse declarado publicamente (no cabeçalho), isso geraria um aviso (o id pode não responder ...) e, de qualquer maneira, a pessoa que o usa, precisará saber exatamente que o inicializador real é o initOnce

poderíamos evitar isso ainda mais, mas não há necessidade

Peter Lapisu
fonte
0

Devo mencionar que colocar afirmações e criar exceções para ocultar métodos na subclasse tem uma armadilha desagradável para os bem-intencionados.

Eu recomendaria usar __unavailablecomo Jano explicou em seu primeiro exemplo .

Os métodos podem ser substituídos nas subclasses. Isso significa que, se um método na superclasse usar um método que apenas gera uma exceção na subclasse, provavelmente não funcionará como pretendido. Em outras palavras, você acabou de quebrar o que costumava funcionar. Isso também ocorre com os métodos de inicialização. Aqui está um exemplo dessa implementação bastante comum:

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

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

Imagine o que acontece com -initWithLessParameters, se eu fizer isso na subclasse:

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

Isso implica que você deve usar métodos privados (ocultos), especialmente em métodos de inicialização, a menos que planeje substituir os métodos. Mas esse é outro tópico, pois nem sempre você tem controle total na implementação da superclasse. (Isso me faz questionar o uso de __attribute ((objc_designated_initializer)) como uma prática ruim, embora eu não o tenha usado em profundidade.

Isso também implica que você pode usar asserções e exceções em métodos que devem ser substituídos nas subclasses. (Os métodos "abstratos", como em Criando uma classe abstrata em Objective-C )

E não se esqueça do método + new class.

techniao
fonte