0,0
рейтинг
20 августа 2015 в 15:31

Разработка → Атрибуты свойств в Objective-C. Инструкция для начинающих

image

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

Краткое вступление


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

Непосредственно, инструкция


Для начала поделим все атрибуты, которые есть у свойства, на группы:

  1. атрибуты доступности (readonly/readwrite),
  2. атрибуты владения (retain/strong/copy/assign/unsafe_unretained/weak),
  3. атрибут атомарности (atomic/nonatomic).
  4. nullability атрибут (null_unspecified/null_resettable/nullable/nonnull) — появился в xcode 6.3

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

Атрибуты доступности

  • readwrite — указывает, что свойство доступно и на чтение, и на запись, то есть будут сгенерированы и сеттер, и геттер. Это значение задается всем свойствам по умолчанию, если не задано другое значение.

  • readonly — указывает, что свойство доступно только для чтения. Это значение стоит применять в случаях, когда изменение свойства «снаружи» во время выполнения объектом своей задачи нежелательно, либо когда значение свойства не хранится ни в какой переменной, а генерируется исходя из значений других свойств. Например, есть у объекта User свойства firstName и lastName, и для удобства заданное readonly свойство fullName, значение которого генерируется исходя из значений первых двух свойств.

В случае, когда нежелательно изменение свойства «снаружи», оно, как правило, объявляется в интерфейсе класса как readonly, а потом переопределяется как readwrite в расширении класса (class extension), чтобы внутри класса также изменять значение не напрямую в переменной, а через сеттер.

Атрибуты владения

Это самый обширный тип атрибутов, во многом из-за сосуществования ручного и автоматического управления памятью.
При включенном ARC у переменных, как и у свойств, есть атрибут владения, только в этом случае набор значений меньше, чем у свойств: __strong/__weak/__unsafe_unretained, и касается это только тех типов данных, которые подпадают под действие ARC (особенности для типов данных, не попадающих под действие ARC здесь рассматривать не будем, чтобы не усложнять то, что призвано быть простой шпаргалкой). Поэтому при описании значений этого атрибута для свойств будем еще указывать, какое значение атрибута владения должно быть у соответствующей переменной экземпляра при включенном ARC (если переменная создается автоматически — она сразу создается с нужным значением этого атрибута. Если же вы определяете переменную сами — нужно вручную задать ей правильное значение атрибута владения).

  • retain (соответствующая переменная должна быть с атрибутом __strong) — это значение показывает, что в сгенерированном сеттере счетчик ссылок на присваиваемый объект будет увеличен, а у объекта, на который свойство ссылалось до этого, — счетчик ссылок будет уменьшен. Это значение применимо при выключенном ARC для всех Objective-C классов во всех случаях, когда никакие другие значения не подходят. Это значение сохранилось со времен, когда ARC еще не было и, хотя ARC и не запрещает его использование, при включенном автоматическом подсчете ссылок лучше вместо него использовать strong.

//примерно вот такой сеттер будет сгенерирован для свойства 
//с таким значением атрибута владения с отключенным ARC
-(void)setFoo(Foo *)foo {
  if (_foo != foo) {
    Foo *oldValue = _foo;

    //увеличивается счетчик ссылок на новый объект и укзатель на него сохраняется в ivar
    _foo = [foo retain];

    //уменьшается счетчик ссылок на объект, на который раньше указывало свойство
    [oldValue release]; 
  }
}

  • strong (соответствующая переменная должна быть с атрибутом __strong) — это значение аналогично retain, но применяется только при включенном автоматическом подсчете ссылок. При использовании ARC это значение используется по умолчанию. Используйте strong во всех случаях, не подходящих для weak и copy, и все будет хорошо.

  • copy (соответствующая переменная должна быть с атрибутом __strong) — при таком значении атрибута владения в сгенерированном сеттере соответствующей переменной экземпляра присваивается значение, возвращаемое сообщением copy, отправленным присваиваемому объекту. Использование этого значения атрибута владения накладывает некоторые ограничения на класс объекта:

  1. класс должен поддерживать протокол NSCopying,
  2. класс не должен быть изменяемым (mutable). У некоторых классов есть mutable-подкласс, например, NSString-NSMutableString. Если ваше свойство — экземпляр «мутабельного» класса, использование copy приведет к нежелательным последствиям, так как метод copy вернет экземпляр его «немутабельного» сородича. Например, вызов copy у экземпляра NSMutableString вернет экземпляр NSString.

Пример:

@property (copy, nonatomic) NSMutableString *foo;
	...
	//в сгенерированном сеттере будет примерно следующее
- (void)setFoo:(NSMutableString)foo {
    _foo = [foo copy]; //метод copy класса NSMutableString вернет объект типа NSString, так что после присвоения значения этому свойству, оно будет указывать на неизменяемую строку, и вызов методов, изменяющих строку, у этого объекта приведет к крэшу
}

Из второго ограничения вытекает самая главная причина использования значения copy: все публичные свойства, являющиеся экземплярами класса, у которого есть «мутабельный» подкласс, лучше всего создавать именно с этим значением атрибута владения. У «немутабельных» классов метод copy работает как retain — никакого копирования не произойдет, лишняя память и время израсходованы не будут, но при этом свойство будет защищено от задания экземпляра «мутабельного» подкласса. Например, в свойство типа NSArray нельзя будет задать объект класса NSMutableArray, а значит и изменить свойство «снаружи», минуя сеттер, будет нельзя.

Пример:

@interface Foo : NSObject
…
@property (copy, nonatomic) NSArray *bar;
@property (strong, nonatomic) NSArray *barNotProtected;
…
@end

…
NSMutableArray *mutableArray = [@[@1, @2, @3] mutableCopy];
Foo *foo = [Foo new];
foo.bar = mutableArray; //в bar будет указатель на неизменяемую копию массива
foo.barNotProtected = mutableArray; //а в barNotProtected будет записан указатель на изменяемый массив
[mutableArray removeObjectAtIndex:0]; //теперь foo.barNotProtected указывает на измененный массив (@[@2, @3]), а массив, на который указывает foo.bar никак не изменился (@[@1, @2, @3]).

  • weak (соответствующая переменная должна быть с атрибутом __weak) — это значение аналогично assign и unsafe_unretained. Разница в том, что особая уличная магия позволяет переменным с таким значением атрибута владения менять свое значение на nil, когда объект, на который указывала переменная, уничтожается, что очень хорошо сказывается на устойчивости работы приложения (ибо, как известно, nil отвечает на любые сообщения, а значит никаких вам EXC_BAD_ACCESS при обращении к уже удаленному объекту). Это значение атрибута владения стоит использовать при включенном ARC для исключения retain cycle’ов для свойств, в которых хранится указатель на делегат объекта и им подобных. Это единственное значение атрибута владения, которое не поддерживается при выключенном ARC (как и при включенном ARC на iOS до версии 5.0).

  • unsafe_unretained (соответствующая переменная должна быть с атрибутом __unsafe_unretained) — свойство с таким типом владения просто сохраняет адрес присвоенного ему объекта. Методы доступа к такому свойству никак не влияют на счетчик ссылок объекта. Он может удалиться, и тогда обращение к такому свойству приведет к крэшу (потому и unsafe). Это значение использовалось вместо weak, когда уже появился ARC, но нужно было еще поддерживать iOS 4.3. Сейчас его использование можно оправдать разве что скоростью работы (есть сведения, что магия weak свойств требует значительного времени, хотя, конечно, невооруженным глазом при нынешней производительности этого не заметишь), поэтому, особенно на первых порах, использовать его не стоит.

  • assign (соответствующая переменная должна быть с атрибутом __unsafe_unretained, но так как атрибуты владения есть только у типов попадающих под ARC, с которыми лучше использовать strong или weak, это значение вам вряд ли понадобится) — просто присвоение адреса. Без ARC является дефолтным значением атрибута владения. Его стоит применять к свойствам типов, не попадающих под действие ARC (к ним относятся примитивные типы и так называемые необъектные типы (non-object types) вроде CFTypeRef). Без ARC он также используется вместо weak для исключения retain cycle’ов для свойств, в которых хранится указатель на делегат объекта и им подобных.

Атрибут атомарности

  • atomic — это дефолтное значение для данного атрибута. Оно означает, что акцессор и мутатор будут сгенерированы таким образом, что при обращении к ним одновременно из разных потоков, они не будут выполняться одновременно (то есть все равно сперва один поток сделает свое дело — задаст или получит значение, и только после этого другой поток сможет заняться тем же). Из-за особенностей реализации у свойств с таким значением атрибута атомарности нельзя переопределять только один из методов доступа (уж если переопределяете, то переопределяйте оба, и сами заморочьтесь с защитой от одновременного выполнения в разных потоках). Не стоит путать атомарность свойства с потокобезопасностью объекта. К примеру, если вы в одном потоке получаете значение имени и фамилии из свойств объекта, а в другом — изменяете эти же значения, вполне может получиться так, что значение имени вы получите старое, а фамилии — уже измененное. Соответственно, применять стоит для свойств объектов, доступ к которым может осуществляться из многих потоков одновременно, и нужно спастись от получения каких-нибудь невалидных значений, но при необходимости обеспечить полноценную потокобезопасность, одного этого может оказаться недостаточно. Кроме прочего стоит помнить, что методы доступа таких свойств работают медленнее, чем nonatomic, что, конечно, мелочи в масштабах вселенной, но все же копейка рубль бережет, поэтому там, где нет необходимости, лучше использовать nonatomic.

  • nonatomic — значение противоположное atomic — у свойств с таким значением атрибута атомарности методы доступа не обременены защитой от одновременного выполнения в разных потоках, поэтому выполняются быстрее. Это значение пригодно для большинства свойств, так как большинство объектов все-таки используются только в одном потоке, и нет смысла «обвешивать» их лишними фичами. В общем, для всех свойств, для которых не сможете объяснить, почему оно должно быть atomic, используйте nonatomic, и будет вам fast and easy and smart и просто праздник какой-то.

Nullability атрибут

Этот атрибут никак не влияет на генерируемые методы доступа. Он предназначен для того, чтобы обозначить, может ли данное свойство принимать значение nil или NULL. Xcode использует эту информацию при взаимодействии Swift-кода с вашим классом. Кроме того, это значение используется для вывода предупреждений в случае, если ваш код делает что-то, противоречащее заявленному поведению. Например, если вы пытаетесь задать значение nil свойству, которое объявлено как nonnull. А самое главное, эта информация будет полезна тому, кто будет читать ваш код. На использование этого атрибута есть пара ограничений:
  1. Его нельзя использовать для примитивных типов (им и не нужно, так как они в любом случае не принимают значений nil и NULL)
  2. его нельзя использовать для многоуровневых указателей (например, id* или NSError**)

  • null_unspecified — используется по умолчанию и ничего не говорит о том, может ли свойство принимать значение nil/NULL или нет. До появления этого атрибута именно так мы и воспринимали абсолютно все свойства. Это плохо в плане содержательности заголовков, поэтому использование этого значения не рекомендуется.
  • null_resettable — это значение свидетельствует о том, что геттер такого свойства никогда не вернет nil/NULL в связи с тем, что при задании такого значения, на самом деле свойству будет присвоено некое дефолтное. А так как генерируемые методы доступа от значения этого атрибута не зависят, вы сами должны будете либо переопределить сеттер так, чтобы при получении на вход nil/NULL он сохранял в ivar значение по умолчанию, либо переопределить геттер так, чтобы он возвращал дефолтное значение в случае, если соответствующая ivar == nil/NULL. Соответственно, если у вашего свойства есть дефолтное значение — объявляйте его как null_resettable.
  • nonnull — это значение свидетельствует о том, что свойство, помеченное таким атрибутом, не будет принимать значение nil/NULL. На самом деле, вы все еще можете получить nil, если, к примеру, попробуете получить значение этого свойства у nil’а и просто потому, что Xcode не очень жестко следит за этим. Но это уже будет ваша ошибка и Xcode будет по мере сил указывать вам на нее в своих предупреждениях. Использовать стоит, если вы уверены, что значение данного свойства никогда не будет nil/NULL и хотите, чтобы IDE помогала вам следить за этим.
  • nullable — это значение свидетельствует о том, что свойство может иметь значение nil/NULL. Оно так же, как и null_unspecified, ни к чему не обязывает, но все же ввиду его бОльшей определенности, среди этих двух правильнее использовать именно nullable. Таким образом, если вам не подходит ни null_resettable, ни nonnull — используйте nullable.

Для большего удобства вы можете изменить значение по умолчанию с null_unspecified на nonnull для определенного блока кода. Для этого нужно поставить NS_ASSUME_NONNULL_BEGIN перед таким блоком и NS_ASSUME_NONNULL_END — после него.
//например вот такое объявление
@property (copy, nonatomic, nonnull) NSString *foo;
@property (copy, nonatomic, nonnull) NSString *str;
@property (strong, nonatomic, nullable) NSNumber *bar;
@property (copy, nonatomic, null_resettable) NSString *baz;

//аналогично следующему блоку
NS_ASSUME_NONNULL_BEGIN
@property (copy, nonatomic) NSString *foo;
@property (copy, nonatomic) NSString *str;
@property (strong, nonatomic, nullable) NSNumber *bar;
@property (copy, nonatomic, null_resettable) NSString *baz;
NS_ASSUME_NONNULL_END


На этом повествование спешу закончить. Кому есть что добавить или с чем поспорить — милости просим в комменты.

Upd: Благодаря storoj подправлен пример сеттера в пункте про retain, и немного уточнен пункт про atomic. Спасибо за комментарии.
Upd2: Благодаря убедительности fsmorygo добавлен раздел про nullability атрибут
Центр Высоких Технологий @htc-cs
карма
10,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (20)

  • +1
    Вот такие базовые вещи порой ускользают так как ты считаешь их обыденными и используешь по наитию. А если спросят на собеседовании вопрос вроде: «почему переменные типа id объявляются без *?» не знаешь как ответить и уже после такого вопроса начинаешь разбираться в вещах которые ты вроде бы знал.

    Продолжайте.
  • +1
    имхо чудовищный вариант сеттера: isEqual, release до retain
    • –1
      и про atomic тоже неправда
      • +2
        не надо ничего выдумывать, надо просто раскурить ман
        • 0
          Воистину так, но злоба дня требует импортозамещения. В смысле, кириллицы.
      • +1
        Спасибо за комментарии. Действительно, сеттер был не примером для подражания. Поправил. С atomic тоже немного уточнил формулировку, но в целом, не совсем понятно, что именно, по Вашему мнению, там неправда. Добавьте и сюда конструктива, пожалуйста, — укажите, с чем именно Вы не согласны.
        • 0
          Про atomic по-моему было написано совсем не так как сейчас, потому что к нынешнему варианту мне уже сложно придраться. А может я вчера плохо прочитал.
          Я вообще не очень хорошо представляю себе реальный случай необходимости atomic, разве что при использовании больших структур, типа
          struct MyStruct {
          int integers[100];
          double doubles[100]
          };
          Как я понимаю, здесь atomic обеспечит полную запись и полное чтение, т.е. не будет ситуации когда один поток прочитал или записал одну половину, а второй-вторую. В случаях с указателями atomic имхо не актуален.

          Призываю знающих людей, кто действительно хорошо понимает что это такое и может объяснить на простом примере, а то я тут сейчас нарассказываю :)
    • 0
      На вкус и цвет. Пол Хэгарти, например, считает это очень хорошо.
      • +2
        а Алексей Сторожев на своей заднице прочувствовал креши из-за того, что _foo и foo это один и тот же объект
        • 0
          справедливости ради, там сначала проверка ![_foo isEqual:foo] идет. Но всё равно code smell.
          • 0
            А разве не бывает такого, что [x isEqual:x] == false? И вообще здесь не место isEqual по-умолчанию, логика бывает завязана не только на объект как значение, но и на объект как указатель. Можно придумать кучу примеров, демонстрирующих почему isEqual тут не первичен.
            • 0
              Я считаю что это нарушение контракта -isEqual:, но так как для Cocoa он явно нигде не прописан (в отличие от Java), вопрос открытый.
        • 0
          Всё же это вопрос восприятия и привычки. Т.е., я акцентирую на этом внимание только, когда акцентируют на этом моё внимание) В остальных случаях всё понятно.
  • +1
    Уже давно вышел Xcode 6, не за горами уже Xcode 7, а в Вашей статье про Nullability атрибуты — ни слова. Ну как же так?
    • 0
      Не упомянуты nullability annotations здесь потому, что imho к свойствам они относятся скорее «заодно», чем «в первую очередь». Да еще, рассказывая про них, надо бы и Swift приплетать. Думаю, хорошенько покопавшись, так можно к этой статье еще много чего привязать. Но я уверен, что не стоит все собирать в одном месте, как не стоит писать классы на 70к строк
      • 0
        Я уважаю Ваше мнение, но, как Вы написали, статья для новичка. Новичок, прочитав ее, даже не будет знать что такое @property(..., nullable), не говоря уже о null_resettable. Также, цитируя Вас:
        Для начала поделим все атрибуты, которые есть у свойства, на группы:

        Данная фраза подразумевает, что вы перечислили все варианты свойств, что уже вводит в заблуждение. А знание об этих атрибутах поможет защитить себя от ошибок в коде, а также поможет выработать хороший стиль программирования. И, как мне кажется, вполне можно обойтись и без упоминания Swift — он тут совсем не обязателен.
        • +1
          Очень убедительно. Добавил раздел про nullability атрибут. Спасибо.
  • 0
    Эх, где вы были с данной статьей года 2 назад =)
    Objective-C в принципе дался мне достаточно тяжело, прежде чем написать первый готовый проект я раза 2 бросал это занятие, отчасти из-за таких непонятных мелочей.
  • 0
    Почему, стоит лишь у одно свойства прописать nonnull, как Xcode тут же выписывает предупреждения
    Pointer is missing a nullability type specifier (__nonnull or __nullable)

    по всем остальным свойствам класса? Почему ему недостаточно «null_unspecified — используется по умолчанию»?
    • 0
      Отключите warning, если так не хочется везде прописывать атрибут или использовать NS_ASSUME_NONNULL_BEGIN/END.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.