Объектная инициализация

Инициализация устанавливает переменные экземпляра объекта к разумным и полезным начальным значениям. Это может также выделить и подготовить другие глобальные ресурсы, необходимые объекту, загрузив их при необходимости из внешнего источника, такие как файл. Каждый объект, объявляющий переменные экземпляра, должен реализовать метод инициализации — если значение по умолчанию не установило все для обнуления инициализации, достаточно. Если объект не реализует инициализатор, Какао вызывает инициализатор самого близкого наследователя вместо этого.

Форма инициализаторов

NSObject объявляет init прототип для инициализаторов; это - метод экземпляра, введенный для возврата объекта типа id. Переопределение init хорошо для подклассов, требующих, чтобы никакие дополнительные данные не инициализировали их объекты. Но часто инициализация зависит от внешних данных для установки объекта в разумное начальное состояние. Например, скажите, что Вы имеете Account класс; инициализировать Account объект соответственно требует уникального номера счета, и это должно быть предоставлено инициализатору. Таким образом инициализаторы могут взять один или несколько параметров; единственное требование - то, что метод инициализации начинается с букв «init». (Стилистическое соглашение init... иногда используется для обращения к инициализаторам.)

Какао имеет много примеров инициализаторов с параметрами. Вот некоторые (с классом определения в круглых скобках):

Эти инициализаторы являются методами экземпляра, начинающимися с «init» и возвращающими объект динамического типа id. Кроме этого, они следуют соглашениям Какао для методов мультипараметра, часто с помощью WithВвести: или FromИсточник: перед первым и самым важным параметром.

Проблемы с инициализаторами

Несмотря на то, что init... методы требуются их сигнатурой метода возвратить объект, тот объект является не обязательно тем, последний раз выделенным — получатель init... сообщение. Другими словами, объект, который Вы возвращаете от инициализатора, не мог бы быть тем, который Вы думали, инициализировался.

Два условия запрашивают возврат чего-то другого, чем просто выделенный объект. Первое включает две связанных ситуации: когда атрибут определения объекта должен быть уникальным, когда должен быть одноэлементный экземпляр или. Некоторые классы Какао —NSWorkspace, например — позволяют только один экземпляр в программе; класс в таком случае должен гарантировать (в инициализаторе или, более вероятно, в методе фабрики классов), что только один экземпляр создается, возвращая этот экземпляр, если существует дальнейший запрос на новый.

Аналогичная ситуация возникает, когда объект требуется, чтобы иметь атрибут, делающий его уникальным. Вспомните гипотетическое Account класс, упомянутый ранее. Учетная запись любого вида должна иметь уникальный идентификатор. Если инициализатор для этого класса — говорит, initWithAccountID:— передается идентификатор, уже связанный с объектом, это должно сделать две вещи:

Путем выполнения этого инициализатор гарантирует уникальность идентификатора при обеспечении того, относительно чего попросили: Account экземпляр с требуемым идентификатором.

Иногда init... метод не может выполнить инициализацию, которую требуют. Например, initFromFile: метод ожидает инициализировать объект от содержания файла, пути, которому передается в качестве параметра. Но если никакой файл не существует в том расположении, объект не может быть инициализирован. Подобная проблема происходит если initWithArray: инициализатор передается NSDictionary объект вместо NSArray объект. Когда init... метод не может инициализировать объект, он должен:

Возврат nil от инициализатора указывает, что не может быть создан нужный объект. При создании объекта необходимо обычно проверять, является ли возвращенное значение nil перед продолжением:

id anObject = [[MyClass alloc] init];
if (anObject) {
    [anObject doSomething];
    // more messages...
} else {
    // handle error
}

Поскольку init... метод мог бы возвратиться nil или объект кроме того явно выделил, опасно использовать экземпляр, возвращенный alloc или allocWithZone: вместо того, возвращенного инициализатором. Рассмотрите следующий код:

id myObject = [MyClass alloc];
[myObject init];
[myObject doSomething];

init метод в этом примере, возможно, возвратился nil или, возможно, заменил различным объектом. Поскольку можно отправить сообщение в nil не повышая исключение, ничто не произошло бы в прежнем случае кроме (возможно), головной боли отладки. Но необходимо всегда полагаться на инициализированный экземпляр вместо «сырых данных» просто выделенный. Поэтому необходимо вложить сообщение выделения в сообщении инициализации и протестировать объект, возвращенный из инициализатора перед продолжением.

id myObject = [[MyClass alloc] init];
if ( myObject ) {
    [myObject doSomething];
} else {
    // error recovery...
}

Как только объект инициализируется, Вы не должны инициализировать его снова. При попытке реинициализации класс платформы инстанцированного объекта часто повышает исключение. Например, вторая инициализация в этом примере привела бы к NSInvalidArgumentException быть повышенным.

NSString *aStr = [[NSString alloc] initWithString:@"Foo"];
aStr = [aStr initWithString:@"Bar"];

Реализация инициализатора

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

- (id)initWithAccountID:(NSString *)identifier {
    if ( self = [super init] ) {
        Account *ac = [accountDictionary objectForKey:identifier];
        if (ac) { // object with that ID already exists
            [self release];
            return [ac retain];
        }
        if (identifier) {
            accountID = [identifier copy]; // accountID is instance variable
            [accountDictionary setObject:self forKey:identifier];
            return self;
        } else {
            [self release];
            return nil;
        }
    } else
        return nil;
}

Не необходимо инициализировать все переменные экземпляра объекта явно, просто те, которые необходимы сделать объект функциональным. Инициализация набора к нулю по умолчанию, выполняемая на переменной экземпляра во время выделения, часто достаточна. Удостоверьтесь, что Вы сохраняете или копируете переменные экземпляра, как требуется для управления памятью.

Требование для вызова инициализатора суперкласса как первого действия важно. Вспомните, что объект инкапсулирует не только переменные экземпляра, определенные его классом, но и переменные экземпляра, определенные всеми его классами наследователя. Путем вызова инициализатора super во-первых, Вы помогаете гарантировать, что переменные экземпляра, определенные классами цепочка наследования, инициализируются сначала. Непосредственный суперкласс, в его инициализаторе, вызывает инициализатор своего суперкласса, вызывающего основное init... метод его суперкласса, и т.д. (см. рисунок 6-1). Надлежащий порядок инициализации критически важен, потому что более поздние инициализации подклассов могут зависеть от определенных с помощью суперкласса переменных экземпляра, инициализируемых к рыночной стоимости.

  Инициализация рисунка 6-1 цепочка наследования
Initialization up the inheritance chain

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

Многократные инициализаторы и определяемый инициализатор

Класс может определить больше чем один инициализатор. Иногда многократные инициализаторы позволяют клиентам класса обеспечить ввод для той же инициализации в различных формах. NSSet класс, например, предлагает клиентам несколько инициализаторов, принимающих те же данные в различных формах; каждый берет NSArray объект, другой считаемый список элементов и другой a nil- завершенный список элементов:

- (id)initWithArray:(NSArray *)array;
- (id)initWithObjects:(id *)objects count:(unsigned)count;
- (id)initWithObjects:(id)firstObj, ...;

Некоторые подклассы обеспечивают инициализаторы удобства, предоставляющие значения по умолчанию к инициализатору, берущему полное дополнение параметров инициализации. Этот инициализатор обычно является определяемым инициализатором, самым важным инициализатором класса. Например, предположите, что существует a Task класс и это объявляют определяемый инициализатор с этой подписью:

- (id)initWithTitle:(NSString *)aTitle date:(NSDate *)aDate;

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

- (id)initWithTitle:(NSString *)aTitle {
    return [self initWithTitle:aTitle date:[NSDate date]];
}
 
- (id)init {
    return [self initWithTitle:@”Task”];
}

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

При определении подкласса необходимо быть в состоянии идентифицировать определяемый инициализатор суперкласса и вызвать его в определяемом инициализаторе подкласса через сообщение к super. Необходимо также удостовериться, что наследованные инициализаторы покрыты в некотором роде. И можно обеспечить столько инициализаторов удобства, сколько Вы считаете необходимыми. При разработке инициализаторов класса имейте в виду, что определяемые инициализаторы объединены в цепочку друг другу через сообщения к super; тогда как другие инициализаторы объединены в цепочку к определяемому инициализатору их класса через сообщения к self.

Пример сделает эту расчетную организацию. Скажем, существует три класса, A, B, и C; класс B наследовался от класса A, и класс C наследовался от класса B. Каждый подкласс добавляет атрибут как переменную экземпляра и реализует init... метод — определяемый инициализатор — для инициализации этой переменной экземпляра. Они также определяют вторичные инициализаторы и гарантируют, что наследованные инициализаторы переопределяются, при необходимости. Рисунок 6-2 иллюстрирует инициализаторы всех трех классов и их отношений.

  Взаимодействия рисунка 6-2 вторичных и определяемых инициализаторов
Interactions of secondary and designated initializers

Определяемый инициализатор для каждого класса является инициализатором с большей частью покрытия; это - метод, инициализирующий атрибут, добавленный подклассом. Определяемый инициализатор также init... метод, вызывающий определяемый инициализатор суперкласса в сообщении к super. В этом примере, определяемом инициализаторе класса C, initWithTitle:date:, вызывает определяемый инициализатор его суперкласса, initWithTitle:, который поочередно вызывает init метод класса A. При создании подкласса всегда важно знать определяемый инициализатор суперкласса.

Несмотря на то, что определяемые инициализаторы таким образом соединяются цепочка наследования через сообщения к super, вторичные инициализаторы подключены к определяемому инициализатору их класса через сообщения к self. Вторичные инициализаторы (как в этом примере) являются часто переопределяемыми версиями наследованных инициализаторов. Переопределения Класса C initWithTitle: вызвать его определяемый инициализатор, передавая его дата по умолчанию. Этот определяемый инициализатор, в свою очередь, вызывает определяемый инициализатор класса B, который является переопределенным методом, initWithTitle:. Если Вы отправили initWithTitle: обменивайтесь сообщениями к объектам класса B и класса C, Вы вызвали бы различные реализации метода. С другой стороны, если класс C не переопределял initWithTitle: и Вы отправили сообщение в экземпляр класса C, реализация класса B будет вызвана. Следовательно, экземпляр C был бы не полностью инициализирован (так как он испытает недостаток в дате). При создании подкласса важно удостовериться, что соответственно покрыты все наследованные инициализаторы.

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