Можно ли сделать метод -init закрытым в Objective-C?

147

Мне нужно скрыть (сделать личным) -initметод моего класса в Objective-C.

Как я могу это сделать?

Лайош
источник
3
Теперь для этого есть специальное, чистое и описательное средство, как показано в этом ответе ниже . В частности: NS_UNAVAILABLE. Как правило, я призываю вас использовать этот подход. Рассмотрит ли ФП пересмотр принятого ответа? Другие ответы здесь предоставляют много полезных деталей, но не являются предпочтительным методом достижения этого.
Бенджон
Как другие отметили ниже, NS_UNAVAILABLEвсе еще позволяет вызывающей стороне вызывать initкосвенно через new. Простое переопределение initвозврата nilбудет обрабатывать оба случая.
Грег Браун
Вы можете, конечно, сделать «новый» NS_UNAVAILABLE, а также «init» - что сегодня является обычной практикой.
Мотти Шнеор

Ответы:

88

Objective-C, как и Smalltalk, не имеет понятия «частные» и «публичные» методы. Любое сообщение может быть отправлено любому объекту в любое время.

Что вы можете сделать, это бросить, NSInternalInconsistencyExceptionесли ваш -initметод вызывается:

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

Другая альтернатива - которая, вероятно, намного лучше на практике - это сделать -initчто-то разумное для вашего класса, если это вообще возможно.

Если вы пытаетесь сделать это, потому что вы пытаетесь «гарантировать» использование одноэлементного объекта, не беспокойтесь. В частности, не заморачиваться с «переопределение +allocWithZone:, -init, -retain, -release» метод создания одиночек. Это практически всегда не нужно и просто добавляет усложнение без реального существенного преимущества.

Вместо этого просто напишите свой код так, чтобы ваш +sharedWhateverметод был таким, каким вы получаете доступ к singleton, и запишите это как способ получить экземпляр singleton в своем заголовке. Это должно быть все, что вам нужно в подавляющем большинстве случаев.

Крис Хансон
источник
2
Здесь действительно необходим возврат?
philsquared
5
Да, чтобы компилятор был доволен. В противном случае компилятор может жаловаться, что нет возврата от метода с не пустым возвратом.
Крис Хэнсон
Забавно, это не для меня. Возможно, другая версия компилятора или переключатели? (Я просто использую стандартные переключатели gcc с XCode 3.1)
philsquared
3
Не стоит рассчитывать на то, что разработчик будет следовать шаблону. Лучше бросить исключение, чтобы разработчики в другой команде не знали об этом. У меня частная концепция была бы лучше.
Ник Тернер
4
«Без реального существенного преимущества» . Совершенно не соответствует действительности. Существенным преимуществом является то, что вы хотите применить шаблон синглтона. Если вы разрешите создание новых экземпляров, то разработчик, не знакомый с API, может использовать allocи использовать initсвой код неправильно, потому что у него правильный класс, но неправильный экземпляр. Это суть принципа инкапсуляции в ОО. Вы скрываете в своих API то, что другим классам не нужно, или вам не нужен доступ. Вы не просто держите все публично и ожидаете, что люди будут следить за всем этим.
конец
346

NS_UNAVAILABLE

- (instancetype)init NS_UNAVAILABLE;

Это краткая версия недоступного атрибута. Впервые он появился в macOS 10.7 и iOS 5 . Это определяется в NSObjCRuntime.h как #define NS_UNAVAILABLE UNAVAILABLE_ATTRIBUTE.

Существует версия, которая отключает метод только для клиентов Swift , но не для кода ObjC:

- (instancetype)init NS_SWIFT_UNAVAILABLE;

unavailable

Добавьте unavailableатрибут в заголовок, чтобы сгенерировать ошибку компилятора при любом вызове init.

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

ошибка времени компиляции

Если у вас нет причины, просто введите __attribute__((unavailable))или даже __unavailable:

-(instancetype) __unavailable init;  

doesNotRecognizeSelector:

Используйте, doesNotRecognizeSelector:чтобы вызвать NSInvalidArgumentException. «Система времени выполнения вызывает этот метод всякий раз, когда объект получает сообщение aSelector, на которое он не может ответить или переслать».

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

NSAssert

Используйте, NSAssertчтобы бросить NSInternalInconsistencyException и показать сообщение:

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

raise:format:

Используйте, raise:format:чтобы создать собственное исключение:

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

[self release]необходимо, потому что объект уже allocсъел. При использовании ARC его вызовет компилятор. В любом случае не о чем беспокоиться, когда вы собираетесь намеренно прекратить исполнение.

objc_designated_initializer

Если вы намереваетесь отключить initпринудительное использование назначенного инициализатора, для этого есть атрибут:

-(instancetype)myOwnInit NS_DESIGNATED_INITIALIZER;

Это генерирует предупреждение, если какой-либо другой метод инициализатора не вызывает myOwnInitвнутренне. Подробности будут опубликованы в « Принятии Modern Objective-C» после следующего выпуска Xcode (я думаю).

Яно
источник
Это может быть полезно для других методов, чем init. Так как, если этот метод недопустим, почему вы инициализируете объект? Кроме того, при возникновении исключения вы сможете указать какое-либо пользовательское сообщение, сообщающее разработчику правильный init*метод, в то время как у вас нет такой опции в случае doesNotRecognizeSelector.
Алекс Н.
Понятия не имею, Алекс, этого быть не должно :) Я отредактировал ответ.
Яно
Это странно, потому что это привело к краху вашей системы. Я думаю, лучше не допустить, чтобы что-то произошло, но мне интересно, есть ли лучший способ. Я хочу, чтобы другие разработчики не могли вызывать его и позволять компилятору его
догонять
1
Я пробовал это, и это не работает: - (id) init __attribute __ ((недоступно ("init not available"))) {NSAssert (false, @ "Use initWithType"); вернуть ноль; }
okysabeni
1
@Miraaj Похоже, он не поддерживается вашим компилятором. Это поддерживается в Xcode 6. Вы должны получить «инициализатор удобства, пропускающий вызов« self »к другому инициализатору», если инициализатор не вызывает назначенный.
Яно
101

Apple начала использовать следующее в своих заголовочных файлах, чтобы отключить конструктор init:

- (instancetype)init NS_UNAVAILABLE;

Это правильно отображается как ошибка компилятора в XCode. В частности, это установлено в нескольких из их заголовочных файлов HealthKit (HKUnit является одним из них).

lehn0058
источник
3
Обратите внимание, что вы все еще можете создать экземпляр объекта с помощью [MyObject new];
Хосе
11
Вы также можете сделать + (instancetype) new NS_UNAVAILABLE;
sonicfly
@sonicfly пытался сделать это, но проект все еще компилируется
CyberMew
3

Если вы говорите о методе по умолчанию -init, то вы не можете. Он унаследован от NSObject, и каждый класс ответит на него без предупреждений.

Вы можете создать новый метод, скажем -initMyClass, и поместить его в закрытую категорию, как предлагает Мэтт. Затем определите метод -init по умолчанию, чтобы вызвать исключение, если оно вызывается, или (лучше) вызвать ваш закрытый -initMyClass с некоторыми значениями по умолчанию.

Одна из основных причин, по которой люди, похоже, хотят скрыть init, - это одноэлементные объекты . Если это так, то вам не нужно скрывать -init, просто верните вместо этого объект-одиночка (или создайте его, если он еще не существует).

Натан Кинсингер
источник
Это кажется более подходящим подходом, чем просто оставить init в вашем синглтоне в одиночку и полагаться на документацию, чтобы сообщить пользователю, к которому он должен получить доступ через sharedWhothing. Люди обычно не читают документы, пока не потратили впустую много минут, пытаясь выяснить проблему.
Грег Малетик
3

Поместите это в заголовочный файл

- (id)init UNAVAILABLE_ATTRIBUTE;
Джерри Хуанг
источник
Это не рекомендуется. В современных документах Apple, направленных на определение цели, говорится, что init должен возвращать тип экземпляра, а не идентификатор. developer.apple.com/library/ios/releasenotes/ObjectiveC/...
lehn0058
5
и это: - (instancetype) init NS_UNAVAILABLE;
Bandejapaisa
3

Вы можете объявить любой метод недоступным, используя NS_UNAVAILABLE.

Таким образом, вы можете поместить эти строки ниже вашего @interface

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

Еще лучше определить макрос в заголовке вашего префикса

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

и

@interface YourClass : NSObject
NO_INIT

// Your properties and messages

@end
Каунтея
источник
2

Это зависит от того, что вы подразумеваете под «сделать частным». В Objective-C вызов метода для объекта лучше описать как отправку сообщения этому объекту. В языке нет ничего, что запрещало бы клиенту вызывать какой-либо метод на объекте; лучшее, что вы можете сделать, это не объявлять метод в заголовочном файле. Если клиент, тем не менее, вызывает «закрытый» метод с правильной подписью, он все равно будет выполняться во время выполнения.

Тем не менее, наиболее распространенный способ создания закрытого метода в Objective-C - это создание категории в файле реализации и объявление всех «скрытых» методов в нем. Помните, что это не помешает initзапуску вызовов , но компилятор выдаст предупреждения, если кто-нибудь попытается это сделать.

MyClass.m

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

@implementation MyClass

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

@end

На MacRumors.com есть достойная ветка на эту тему.

Мэтт Диллард
источник
3
К сожалению, в этом случае подход категории действительно не поможет. Обычно он покупает вам предупреждения во время компиляции, что метод не может быть определен в классе. Однако, поскольку MyClass должен наследоваться от одного из корневых предложений и они определяют init, предупреждений не будет.
Барри Уорк
2

проблема в том, что вы не можете сделать его «приватным / невидимым», потому что метод init отправляется на id (так как alloc возвращает id), а не на YourClass

Обратите внимание, что с точки зрения компилятора (средства проверки) идентификатор мог бы потенциально реагировать на все, что когда-либо вводилось (он не может проверить, что действительно входит в идентификатор во время выполнения), поэтому вы можете скрыть init только тогда, когда ничего не будет (publicly = in используйте метод init, чтобы компилятор знал, что id не может ответить на init, поскольку нигде нет init (в вашем источнике, всех библиотеках и т. д.)

поэтому вы не можете запретить пользователю проходить инициализацию и разбиваться компилятором ... но вы можете запретить пользователю получить реальный экземпляр, вызвав init

просто путем реализации init, который возвращает nil и имеет (приватный / невидимый) инициализатор, имя которого кто-то другой не получит (как 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;
}

Примечание: что кто-то может сделать это

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

и он фактически вернет новый экземпляр, но если initOnce нигде в нашем проекте не будет публично объявлен (в заголовке), он выдаст предупреждение (id может не отвечать ...), и в любом случае человеку, использующему это, потребуется точно знать, что настоящим инициализатором является initOnce

мы могли бы предотвратить это еще дальше, но в этом нет необходимости

Питер Лапису
источник
0

Я должен упомянуть, что размещение утверждений и поднятие исключений для методов скрытия в подклассе имеет неприятную ловушку для благонамеренного.

Я бы порекомендовал использовать, __unavailableкак объяснил Яно для своего первого примера .

Методы могут быть переопределены в подклассах. Это означает, что если метод в суперклассе использует метод, который просто вызывает исключение в подклассе, он, вероятно, не будет работать как задумано. Другими словами, вы просто сломали то, что раньше работало. Это верно и для методов инициализации. Вот пример такой довольно распространенной реализации:

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

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

Представьте, что происходит с -initWithLessParameters, если я делаю это в подклассе:

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

Это подразумевает, что вы должны стремиться использовать закрытые (скрытые) методы, особенно в методах инициализации, если только вы не планируете переопределять методы. Но это другая тема, так как вы не всегда имеете полный контроль над реализацией суперкласса. (Это заставляет меня усомниться в использовании __attribute ((objc_designated_initializer)) как плохой практике, хотя я не использовал его подробно.)

Это также подразумевает, что вы можете использовать утверждения и исключения в методах, которые должны быть переопределены в подклассах. («Абстрактные» методы как в Создание абстрактного класса в Objective-C )

И не забывайте о + новом методе класса.

techniao
источник