как запретить базовый метод инициализации в NSObject

Я хочу заставить пользователя использовать мой собственный метод инициализации (например, -(id)initWithString:(NSString*)foo;), а не базовый [[myObject alloc]init];.

Как мне это сделать?


person Anthony    schedule 18.01.2012    source источник
comment
что мешает вам написать свой собственный конструктор?   -  person Shanti K    schedule 18.01.2012
comment
Это большой вопрос. Как показывают приведенные ниже ответы, вы не можете этого сделать. Как я это вижу, поскольку предполагается, что все классы являются подклассами NSObject, который определяет метод -init, и поскольку подклассы всегда должны поддерживать интерфейс, который они наследуют, Apple заставляя вас сделать -init действительным инициализатором или изменить этот основной закон ООП (выбрасывая исключение или что-то в этом роде).   -  person Danra    schedule 13.02.2012
comment
К вашему сведению: мне несколько раз понадобился этот метод при написании сложных библиотек для других разработчиков, когда небезопасно предполагать, что они внимательно прочитали документацию или случайно заполнили не тот класс. Но я согласен, что это ПОЧТИ ВСЕГДА неправильный подход при написании простых приложений...   -  person Adam    schedule 11.11.2012
comment
Многое изменилось с тех пор, как вы задали вопрос. Я удалил свой оригинальный (немного смущающий) ответ и написал новый о том, как это сделать правильно, используя директивы компилятора LLVM. Пожалуйста, взгляните.   -  person fzwo    schedule 11.11.2014


Ответы (6)


Принятый ответ неверен - вы МОЖЕТЕ сделать это, и это очень просто, вам просто нужно быть немного явным. Вот пример:

У вас есть класс с именем «DontAllowInit», который вы хотите запретить людям:

@implementation DontAllowInit

- (id)init
{
    if( [self class] == [DontAllowInit class])
    {
        NSAssert(false, @"You cannot init this class directly. Instead, use a subclass e.g. AcceptableSubclass");

        self = nil; // as per @uranusjr's answer, should assign to self before returning
    }
    else
        self = [super init];

    return nil;
}

Объяснение:

  1. Когда вы вызываете [super init], класс, который был выделен, был ПОДКЛАССОМ.
  2. "self" - это экземпляр, то есть то, что было инициализировано
  3. "[self class]" – это класс, который был создан – это будет ПОДКЛАСС, когда ПОДКЛАСС вызывает [super init], или будет СУПЕРКЛАСС, когда СУПЕРКЛАСС вызывается с простым [[SuperClass выделить] инициировать]
  4. Итак, когда суперкласс получает вызов "init", ему просто нужно проверить, совпадает ли класс alloc'd с его собственным классом.

Работает отлично. NB: я не рекомендую эту технику для «обычных приложений», потому что обычно вы ВМЕСТО хотите использовать протокол.

ОДНАКО... при написании библиотек... этот метод ОЧЕНЬ ценен: вы часто хотите "спасти (других разработчиков) от самих себя", и это легко сделать NSAssert и сказать им: "Ой! вы пытались выделить/инициализировать неправильный класс ! Вместо этого попробуйте класс X...".

person Adam    schedule 11.11.2012
comment
Этот ответ устарел. Это не неправильно, но неполно и все же опасно, потому что его нельзя проверить статически. См. мой новый ответ ниже для решения, которое приводит к ошибкам времени компиляции. - person fzwo; 11.01.2016

Все остальные ответы здесь устарели. Теперь есть способ сделать это правильно!

Несмотря на то, что легко просто выйти из строя во время выполнения, когда кто-то вызывает ваш метод, проверка во время компиляции была бы гораздо предпочтительнее.

К счастью, некоторое время это было возможно в Objective-C.

Используя LLVM, вы можете объявить любой метод недоступным в классе, например

- (void)aMethod __attribute__((unavailable("This method is not available")));

Это заставит компилятор жаловаться при попытке вызвать aMethod. Большой!

Поскольку - (id)init — это обычный метод, вы можете таким образом запретить вызов стандартного (или любого другого) инициализатора.

Обратите внимание, однако, что это не застрахует от вызова метода с использованием динамических аспектов языка, например, через [object performSelector:@selector(aMethod)] и т. д. В случае init вы даже не получите предупреждение, потому что метод init определен в других классах, и компилятор не знает достаточно, чтобы выдать вам предупреждение о необъявленном селекторе.

Итак, чтобы предотвратить это, убедитесь, что метод init дает сбой при вызове (см. ответ Адама).

Если вы хотите запретить - (id)init в фреймворке, не забудьте также запретить + (id)new, так как это просто перенаправит на инициализацию.

Хави Сото написал небольшой макрос, чтобы быстрее и проще запретить использование назначенного инициализатора и выдавать более приятные сообщения. Вы можете найти его здесь.

person fzwo    schedule 11.11.2014
comment
Остерегайтесь, пользователи GCC - это не будет работать, а заставить GNUStep работать с clang под не-дарвиновскими ящиками очень сложно. - person Qix - MONICA WAS MISTREATED; 20.01.2015
comment
Глядя на некоторые недавние проекты с открытым исходным кодом, я увидел использование NS ... init disallowed ... macro - я предположил, что это официальный макрос Apple, возможно, представленный в iOS 9? - person Adam; 12.01.2016
comment
Вам также нужно поместить это в заголовок AFICT. Кажется, игнорируется в расширении класса файла .m или в @implmentation. Не уверен насчет приватных заголовков. - person uchuugaka; 04.01.2017
comment
По крайней мере, в Xcode 8 он игнорируется и в частном заголовке. :( - person uchuugaka; 04.01.2017
comment
Если это действительно так, отправьте отчет об ошибке в Apple. Если вы также скопируете его на openradar, почему бы не поделиться ссылкой здесь, в комментариях, чтобы люди могли продублировать? - person fzwo; 04.01.2017
comment
Этот новый ответ выглядит намного чище; есть ли преимущества у вашего решения? - person Raphael; 14.07.2017
comment
@Raphael Этот другой ответ, по сути, такой же, с немного более приятным синтаксисом, которого не было, когда я писал свой, и с отсутствием дополнительной информации о новой проверке и проверке во время выполнения. Я бы использовал этот новый синтаксис сейчас и, чтобы быть действительно уверенным, по-прежнему использую описанные здесь проверки, особенно при создании фреймворков для использования другими. - person fzwo; 14.07.2017

tl; dr

Свифт:

private init() {}

Поскольку все классы Swift по умолчанию включают внутреннюю инициализацию, вы можете изменить ее на приватную, чтобы другие классы не вызывали ее.

Цель C:

Поместите это в файл .h вашего класса.

- (instancetype)init NS_UNAVAILABLE;

Это зависит от определения ОС, которое предотвращает вызов указанного метода.

person CodeBender    schedule 04.02.2016

-(id) init
{
    @throw [NSException exceptionWithName: @"MyExceptionName" 
                                   reason: @"-init is not allowed, use -initWithString: instead"
                                 userInfo: nil];
}

-(id) initWithString: (NSString*) foo
{
    self = [super init];  // OK because it calls NSObject's init, not yours
    // etc

Генерация исключения оправдана, если вы документируете, что -init не разрешено и, следовательно, его использование является ошибкой программиста. Однако лучшим ответом было бы заставить -init вызывать -initWtihString: с некоторым подходящим значением по умолчанию, т.е.

-(id) init
{
    return [self initWithString: @""];
}
person JeremyP    schedule 18.01.2012
comment
И когда инициализатор суперкласса вызовет его голый метод -init, все рухнет? :) - person Eiko; 18.01.2012
comment
@Eiko: Нет, потому что мы вызываем только -init суперкласса через [super init] из нашего назначенного инициализатора. Любые другие инициализаторы суперкласса, которые считаются допустимыми в подклассе, должны быть переопределены, чтобы вместо этого вызывать наш назначенный инициализатор. - person JeremyP; 18.01.2012
comment
Создание исключения в init, похоже, прерывает модульное тестирование с помощью OCUnit. Я получаю это исключение при создании тестов. Завершение работы приложения из-за необработанного исключения «NSInternalInconsistencyException», причина: «objc_getClassList вернул больше классов, чем должен был». Я думаю о том, чтобы init возвращал nil, так как в любом случае это то, что не удалось сделать. - person cesarislaw; 07.03.2012

Краткий ответ: вы не можете.

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

Другой вариант — «утвердить (0)» или сбой другим способом внутри «инициализации», но это не очень хорошее решение.

person Vladimir Gritsenko    schedule 18.01.2012
comment
На самом деле, вы можете. Сгенерировать исключение в -init. - person JeremyP; 18.01.2012
comment
@JeremyP Таким образом, вы ломаете интерфейс вашего суперкласса? Если класс B наследует A, то экземпляры B также должны быть действительными экземплярами A и, таким образом, соответствовать интерфейсу A. - person Danra; 13.02.2012
comment
@Danra: Теоретически ты прав. Гораздо лучше переопределить -init, чтобы вызвать назначенный вами инициализатор. На практике это не имеет большого значения, потому что метод -initWhatever: вызывается только один раз сразу после выделения. - person JeremyP; 20.02.2012
comment
Смотрите мой ответ ниже: вы МОЖЕТЕ сделать это, и это БЕЗОПАСНО, если вы делаете это осторожно. - person Adam; 11.11.2012
comment
Ответ, который я опубликовал, показывает, как вы можете безопасно сделать это в одной строке, используя оператор определения, предоставленный Apple. Работает также с любым методом. - person CodeBender; 05.02.2016

На самом деле я проголосовал за ответ Адама, но хотел бы добавить к нему кое-что.

Во-первых, настоятельно рекомендуется (как видно из автоматически сгенерированных методов init в подклассах NSObject) сверять self с nil в inits. Кроме того, я не думаю, что объекты класса гарантированно будут «равны», как в ==. я делаю это больше похоже на

- (id)init
{
    NSAssert(NO, @"You are doing it wrong.");
    self = [super init];
    if ([self isKindOfClass:[InitNotAllowedClass class]])
        self = nil;
    return self;
}

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

Но что более важно, вне зависимости от того, используете вы описанный выше подход или нет, у вас всегда должна быть соответствующая документация. Вы всегда должны четко указывать, какой метод является вашим назначенным инициализатором, стараться изо всех сил напоминать другим, чтобы они не использовали неподходящие инициализаторы в документации, и доверять другим пользователям/разработчикам, вместо того, чтобы пытаться «спасти чужие задницы» с помощью хитрые коды.

person uranusjr    schedule 26.11.2012
comment
Хороший момент о необходимости присвоить себе перед возвратом - я обновил свой ответ выше. - person Adam; 06.01.2013