Чи можливо зробити метод -init приватним в Objective-C?


147

Мені потрібно сховати (зробити приватним) -initметод мого класу в Objective-C.

Як я можу це зробити?


3
Зараз існує конкретний, чистий та описовий засіб для досягнення цього, як показано у цій відповіді нижче . В зокрема: NS_UNAVAILABLE. Я б закликав вас використовувати цей підхід. Чи розглядає ОП перегляд їх прийнятої відповіді? Інші відповіді тут містять багато корисних деталей, але не є кращим методом досягнення цього.
Бенджон

Як зазначали інші, NS_UNAVAILABLEвсе ще дозволяє абоненту посилатись initопосередковано через new. Просто initповернення до повернення nilбуде вирішувати обидва випадки.
Грег Браун

Відповіді:


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метод був таким, як ви отримуєте доступ до синглтона, і документуйте це як спосіб отримати одиночний екземпляр у своєму заголовку. Це повинно бути все, що вам потрібно в переважній більшості випадків.


2
Чи потрібне повернення насправді?
фільтрований

5
Так, для задоволення компілятора. В іншому випадку компілятор може поскаржитися, що немає повернення з методу з недійсним поверненням.
Кріс Хансон

Смішно, це не для мене. Можливо, інша версія компілятора чи комутатори? (Я просто використовую gcc-комутатори за замовчуванням з XCode 3.1)
фільтрований

3
Розраховувати на розробника для слідування шаблону не є хорошою ідеєю. Краще кинути виняток, тому розробники в іншій команді знають, що не робити. Я приватна концепція буде краще.
Нік Тернер

4
"Без жодної реальної суттєвої переваги" . Повністю неправдиві. Важливою перевагою є те, що ви хочете застосувати єдину модель. Якщо ви дозволяєте створювати нові екземпляри, то розробник, який не знайомий з API, може використовувати allocта використовувати initфункцію коду неправильно, оскільки вони мають правильний клас, але неправильний примірник. У цьому суть принципу інкапсуляції в ОО. Ви ховаєте речі в своїх API, до яких інші класи не повинні потребувати або отримувати доступ. Ви не просто тримаєте все загальнодоступне і очікуєте, що люди все це слідкують.
Нейт

345

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.
Алекс Н.

Ніякої ідеї Алекс, цього не повинно бути :) Я відповів редакцією.
Яно

Це дивно, тому що в кінцевому підсумку вийшло з ладу ваша система. Я думаю, що краще не допустити, щоб щось сталося, але мені цікаво, чи є кращий спосіб. Що я хочу, щоб інші розробники не змогли його зателефонувати, і хай його перехопить компілятор під час "запуску" чи "будівництва"
okysabeni

1
Я спробував це, і він не працює: - (id) init __attribute __ ((недоступний ("init недоступний")) {NSAssert (false, @ "Використовувати initWithType"); повернути нуль; }
okysabeni

1
@Miraaj Здається, що ваш компілятор не підтримується. Він підтримується в Xcode 6. Ви повинні отримати "У ініціалізаторі зручностей відсутній виклик" self "на інший ініціалізатор", якщо ініціалізатор не викликає призначений.
Яно

101

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

- (instancetype)init NS_UNAVAILABLE;

Це правильно відображається як помилка компілятора в Xcode. Зокрема, це встановлено в декількох файлах заголовків HealthKit (HKUnit є одним з них).


3
Зауважте, що ви все ще можете інстанціювати об'єкт за допомогою [MyObject new];
Хосе

11
Ви також можете зробити + (typetype) новий NS_UNAVAILABLE;
sonicfly

@sonicfly Пробував це робити, але проект все ще
збирається

3

Якщо ви говорите про метод -init за замовчуванням, ви не можете. Він успадкований від NSObject, і кожен клас відповість на нього без попереджень.

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

Однією з головних причин, які люди, схоже, хочуть сховати ініт, є об'єкти однотонних . Якщо це так, то вам не потрібно ховати -init, просто поверніть замість нього об'єкт singleton (або створіть його, якщо він ще не існує).


Це здається кращим підходом, ніж просто залишити "init" в самоті у вашому синглі та покладатися на документацію, щоб повідомити користувачеві, що він повинен отримати доступ через "sharedWever". Люди, як правило, не читають документи, поки вони вже не витратили багато хвилин, намагаючись з'ясувати проблему.
Грег Малетич


3

Ви можете оголосити будь-який метод недоступним за допомогою NS_UNAVAILABLE.

Таким чином, ви можете розмістити ці рядки під своїм інтерфейсом @

- (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, попередження не буде.
Barry Wark

2

ну проблема, чому ви не можете зробити його "приватним / невидимим", полягає в тому, що метод init отримує пересилання в id (як аллок повертає ідентифікатор) не в YourClass

Зауважте, що з точки зору компілятора (перевірки) ідентифікатор може потенційно відповідати на все, що коли-небудь вводиться (він не може перевірити, що насправді йде в ідентифікатор під час виконання), тож ви можете ховати init лише тоді, коли нічого нікуди не буде (public = in заголовок) використовуйте метод init, ніж компілятор знав би, що ідентифікатор не може реагувати на init, оскільки ніде не існує init (у вашому джерелі, всі libs тощо ...)

тож ви не можете заборонити користувачеві передавати init і розбиватися компілятором ... але те, що ви можете зробити, це запобігти користувачеві отримати реальний екземпляр, зателефонувавши до init

просто реалізуючи init, який повертає нуль і має (приватний / невидимий) ініціалізатор, якого імені хтось інший не отримає (наприклад, 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 ніде в нашому проекті не буде публічно оголошено (у заголовку), воно створить попередження (ідентифікатор може не відповісти ...) і все одно людині, яка користується цим, знадобиться точно знати, що справжній ініціалізатор - це 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_designized_initializer)) як погану практику, хоча я не використовував її глибоко.)

Це також означає, що ви можете використовувати твердження та винятки в методах, які необхідно перекрити в підкласах. ("Абстрактні" методи, як у створенні абстрактного класу в Objective-C )

І не забувайте про метод + новий клас.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.