Встреча с Олегом Хусаеновым: «Почему кризис не повод ставить бизнес на паузу». Как туда попасть? 👨🏻‍💻
Support us

Подводные камни свойств в Objective-C

Оставить комментарий
Подводные камни свойств в Objective-C
Давно мы ничего не публиковали о языках программирования, особенно о тех, которые сейчас «в тренде» и при этом не избалованы вниманием пишущих на русском. Сегодня в нашем эфире разработчик-блоггер Мэтт Мэссикот с рассказом о том, «…что мы узнали о свойствах Objective-C, работая с компилятором Clang?» Недавно мы в Crashlytics закончили разработку крупного обновления для Mac – и очень гордимся тем, что у нас получилось. Я как разработчик всегда стремлюсь, чтобы код выглядел не менее красиво, чем пользовательский интерфейс. На мой взгляд, хороший стиль сложно переоценить, особенно в заголовках. Поэтому неудивительно, что, когда дело доходит до Objective-C, я много работаю со свойствами. Свойства в Objective-C пленили меня с тех самых пор, как они появились в версии 2.0. До выхода этой версии, когда мы работали с «геттерами» и «сеттерами», нам приходилось писать «простыни» шаблонного кода, не говоря уже о различных хитрых тонкостях. И, когда вышла вторая версия, то краткие заголовки и возможность синтезировать (@synthesize) реализации показались настоящей маленькой революцией. Теперь мы и представить себе не можем, как же мы раньше без них обходились? Несмотря на то, что свойства Objective-на первый взгляд просты, на деле они довольно сложны как с синтаксической, так и с семантической точки зрения. Я в полной мере осознал это, когда открыл для себя флаг «-Weverything», используемый в компиляторе Clang (дело было на конференции WWDC). Не поймите меня неправильно: свойства – это хорошо, и работать с ними нужно. Вопрос только в том, вполне ли вы осознаете, что при этом происходит?

Стиль объявлений

Постепенно я выработал последовательный стиль объявления свойств. Практически все свойства, которые я создаю, выглядят примерно так: 1 @property (nonatomic, copy, readonly) NSString* name; Оператор @property может иметь не более пяти аргументов, и все они оказывают огромное влияние на семантику. Рассмотрим этот вопрос подробнее:

Возможность записи

Ключевые слова readonly и readwrite являются взаимоисключающими. С их помощью вы можете управлять доступностью метода-«сеттера». Свойство readwrite задается по умолчанию, поэтому мы всегда считали это ключевое слово чем-то избыточным. К тому же мне никогда не приходилось использовать его на практике. Но, если вы не стремитесь к лаконичности, оно вам, возможно, и не повредит. Кстати, свойство writeonly создать нельзя. А вот с readonly я работаю постоянно – отличный способ объявить один лишь метод-«геттер».

Имена методов доступа

Вы можете управлять именами методов, которые используете в качестве «геттеров» и «сеттеров». Желаемые имена задаются как getter=myGetterMethod и setter=mySetterMethod. Мне не приходилось на практике применять такие подходы с аргументами «установщиков» («сеттеров») но, если верить документации, getter полезен при добавлении «is» перед булевскими (логическими) свойствами.

Семантика установщиков

И вот здесь начинается самое интересное. Свойства позволяют задать пять различных семантических аспектов поведения вашего метода-установщика. Я использую copy с типами, содержащими единственное значение и способными изменяться. Рассмотрим следующий код: 1 NSMutableString* string; 2 string = [NSMutableString stringWithString:@"Dude"]; 3 [obj setName:string]; 4 [string setString:@"El Duderino"]; Retain здесь не подойдет, если только вы не собирались менять имя без дополнительного вызова -setName:. Assign очень удобно применять со значимыми типами (value-types), а также в случаях отсутствия жесткого связывания (non-owning relationships) например, при работе с делегатами. В таких случаях вы, возможно, решите использовать weak вместо assign, поскольку weak автоматически обнуляет указатель, как только высвобождается объект, на который проставлен этот указатель. Лично мне не нравится ссылочное поведение weak, так как сообщения, направленные в никуда, просто бессимптомно исчезают. Я мог бы написать целую книгу о том, что я думаю о «сообщениях в никуда» в Objective-C, но ограничусь лишь кратким выводом: чтобы смягчить побочные эффекты, не стесняйтесь использовать операторы подтверждения. Я бы предпочел, чтобы программа «падала» таким образом, чтобы я хотя бы знал, что где-то допустил ошибку. С появлением автоматического подсчета ссылок появилась альтернатива использованию retain. Речь идет о strong. Немного погуглив, легко убедиться, что существует серьезная путаница с использованием retain и strong. И это неудивительно: у этих ключевых слов одинаковая семантика и они выдают идентичный код. Тем не менее, strong логичнее связывается с типом отношений, и поэтому я рекомендую работать именно с этим словом.

Атомарность

Методы доступа Objective-C зачастую должны выполнять довольно большой объем работы, и они при этом не являются по определению потоко-безопасными. Скажем, @synthesize может автоматически генерировать «геттеры» и «сеттеры» для ваших переменных экземпляров. Пожалуй, это – наиболее тонкий и запутанный момент в работе со свойствами. Я бы никогда и никому не рекомендовал использовать свойства atomic и искренне не согласен с тем, что такое значение задается по умолчанию. Почему же? Во-первых, сама идея атомарности искушает нас тем, что якобы можно обойтись без других видов синхронизации. Часто ли вам приходилось считывать/записывать переменную экземпляра сразу в нескольких потоках? Случается, скажете вы. А ведь здесь вам с большой вероятностью придется иметь дело и с более сложным взаимодействием. Если начать работать без блокировки, то другие специалисты, которые будут читать ваш код, возможно, даже и не подумают о том, что он может быть завязан на скрытой блокировке. А вот если вы не позволите себе забыть о том, что свойства не являются потоко-безопасными, то, конечно же, более тщательно проработаете вопросы синхронизации. Еще один значительный недостаток атомарности заключается в том, что из соображений производительности свойства atomic не рекомендуется использовать с процессорами ARM. Тот факт, что задаваемое по умолчанию поведение неприемлемо при работе с платформой, которая чаще всего используется в устройствах Apple, является очень веской причиной с самого начала от такого поведения отказаться. Наконец, мне часто приходится писать специальные (пользовательские) «геттеры» и «сеттеры». Это бывает нужно для перехвата важной переменной экземпляра или просто для предоставления какой-то сущности, более сложной, чем обычное свойство. Неоднократно мне приходилось переопределять синтезированную версию, с которой я начинал, и изменение заголовков меня слегка раздражало, поскольку мои пользовательские реализации, как правило, не являются атомарными. И пускай компилятор в таких случаях вполне справляется с атомарностью, нелогично утверждать, будто задаваемое по умолчанию объявление atomic является корректным. К сожалению, у атомарности есть еще один аспект из разряда неприятных сюрпризов. Вспомним раздел из документации по свойствам в Objective-C. 1 [_internal lock]; // используем блокировку на уровне объекта 2 id result = [[value retain] autorelease]; 3 [_internal unlock]; 4 return result; Если указать здесь nonatomic, то синтезированный метод доступа к свойству объекта просто напрямую вернет значение. Существует масса ошибок, связанных с методами доступа, которые проявляются при возврате свойств без применения идиомы «удержание-автовысвобождение» (retain-autorelease). А nonatomic может коренным образом влиять на семантику метода-получателя, никак не связанную с атомарностью. Рассмотрим следующий пример: 1 NSString *name = [obj name]; 2 [obj setName:nil]; // это может привести к высвобождению имени 3 4 // а без применения retain-autorelease здесь возникли бы проблемы 5 NSLog(@”My name is %@, so that’s what you call me!”, [self name]); Не факт, что все эти сложности с методами-получателями известны достаточно широко. Более того, в файле NSObjCRuntime.h даже есть специальный макрос NS_NONATOMIC_IOSONLY, позволяющий задавать значение по умолчанию только в Mac OS X, но не в iOS. Я прямо содрогаюсь при мысли о том, какие сложные для обнаружения ошибки, характерные только для iOS, может провоцировать этот макрос. Итак, атомарные свойства позволяют реализовать желаемое поведение методов-получателей. Но очень жаль, что мы не можем управлять семантикой получателей независимо от атомарности. Не в последнюю очередь это обусловлено парадоксальной ситуацией с iOS.

Документация по Objective-C: знакомьтесь с Clang

Поддержание актуальности документации, описывающей такие громадные операционные системы, как Mac OS X и iOS, – сложнейшая задача. Я никого не хочу оскорбить, но я обнаружил в документации некоторые неувязки, которые сами по себе – занимательное чтиво. Кроме того, так уж получилось, что я люблю читать предупреждения, выдаваемые компилятором (а также проводить их статический анализ) – вот я и решил привести здесь несколько моих находок. Работая с флагом -Weverything компилятора Clang, я обнаружил несколько мест, где компилятор и документация противоречат друг другу. Кое-где такие нестыковки довольно забавны. 1 clang-warnings.m:19:1: warning: property is assumed atomic by default [-Wimplicit-atomic-properties] 2 @synthesize name = _name; 3 ^ 4 clang-warnings.m:7:28: note: property declared here 5 @property (copy) NSString* name; 6 ^ 7 1 warning generated. Выходит, что здесь подразумевается атомарность? Но ведь, согласно документации и многочисленным постам на сайте StackOverflow, для обозначения атомарности нет даже специального ключевого слова. На момент написания этой статьи предполагается, что «Ключевое слово для обозначения атомарности отсутствует». ОК, расскажите об этом Clang. Мало того, что код нормально компилируется – так при этом компилятор еще и убирает предупреждение! 1 @property (atomic, copy) NSString* name; Давайте рассмотрим следующий пример кода. Выбор свойств, как мне кажется, самый что ни есть типичный: readonly в заголовке, а в расширении класса – приватный метод-установщик. 1 // clang -Weverything -o myclass.o -c clang-warnings.m 2 #import 3 4 @interface MyClass : NSObject 5 6 @property (nonatomic, copy, readonly) NSString* name; 7 8 @end 9 10 @interface MyClass () { 11 NSString* _name; 12 } 13 14 @property (nonatomic, copy) NSString* name; 15 16 @end 17 18 @implementation MyClass 19 20 @synthesize name = _name; 21 22 @end А вот таких вещей Clang не любит: 1 clang-warnings.m:7:1: warning: property attributes ‘readonly’ and ‘copy’ are mutually exclusive [-Wreadonly-setter-attrs] 2 @property (nonatomic, copy, readonly) NSString* name; 3 ^ 4 1 warning generated Нигде не говорится о том, что readonly и семантика метода-установщика являются взаимоисключающими. На самом деле в документации приведена именно такая идиома. Но вы можете убедить себя, что, возможно, это предупреждение и оправданно. В конце концов, слово readonly предполагает отсутствие метода-«сеттера». Но мне нравится все же включать в код такой метод, чтобы добыть новые сведения – как для себя, так и для клиентов этого патентованного API. Итак, что же произойдет, если удалить copy? 1 clang-warnings.m:13:1: warning: property attribute in continuation class does not match the primary class 2 @property (nonatomic, copy) NSString* name; 3 ^ 4 clang-warnings.m:5:43: note: property declared here 5 @property (nonatomic, readonly) NSString* name; 6 ^ 7 1 warning generated. По-видимому, это «баг» в Clang. Думаю, это простительно, поскольку -Weverything, как мне кажется, содержит и «полноценные» предупреждения, с которыми ассоциирован флаг -W, и те, которые такого флага не имеют – как рассмотренное выше. В данном конкретном случае рассмотренное предупреждение определенно является бессмысленным. Я нашел единственный выход для решения этой проблемы: попросту удалил семантику «сеттера». Это не лучший выход, поскольку семантика «сеттера» в комбинации с @synthesize критически важна для правильного поведения. Лично я обожаю находить «баги», которые возникают при отправке retain-сообщений стековым методам. Следующий код демонстрирует серьезную брешь в кланговском механизме проверки ошибок: 1 @property (nonatomic, retain) void (^myBlock)(void); Оказывается, здесь должно возникать предупреждение, но оно срабатывает, только если включен автоматический подсчет ссылок. 1 clang-warnings.m:7:1: warning: retain’ed block property does not copy the block – use copy attribute instead [-Wobjc-noncopy-retain-block-property] 2 @property (nonatomic, retain) void (^myBlock)(void); 3 ^ 4 1 warning generated. Данный случай не противоречит документации, но он относится к числу моих любимых багов. Мне показалось, что это хороший итог всему сказанному выше.

Резюме

Я отнюдь не являюсь поклонником точечного синтаксиса (хотите знать почему – посмотрите хотя бы здесь), но свойства, как ни крути, – классная вещь. Они не только помогают сократить шаблонный код, но и позволяют значительно повысить правильность кода благодаря синтезу. Конечно, нехорошо, что работа компилятора и документация не везде согласуются друг с другом. Но, как бы то ни было, при написании кода на Objective-C без свойств не обойтись. Просто нужно хорошо понимать, как именно они работают, поскольку на поверку многие обманчиво-простые детали оказываются довольно каверзными. Источник. Итак, краткое резюме для тех, кто «ниасилил»: свойства Objective-C гораздо сложнее, чем кажутся. Некоторые рекомендации:
  • везде использовать nonatomic;
  • необходимо соблюдать паттерн retain-autorelease (удержание-автовысвобождение) для методов-получателей;
  • никогда, ни при каких условиях не использовать макрос NS_NONATOMIC_IOSONLY
  • всегда указывать семантику методов-установщиков
  • переобъявлять свойства readonly (только для чтения) в расширениях классов для приватных (закрытых) установщиков
  • попробовать -Weverything – возможно, вам понравится.
Олег Хусаенов в DZ CLUB в эту субботу

Успеть купить билет

Читайте также
10 курсов по SQL для лучшего понимания работы с большими данными (май, 2023)
10 курсов по SQL для лучшего понимания работы с большими данными (май, 2023)
10 курсов по SQL для лучшего понимания работы с большими данными (май, 2023)
Собрали 10 платных и бесплатных онлайн-курсов для изучения SQL. Программы рассчитаны на слушателей, которые только начинают или продолжают знакомство с языком.
10 способов научиться программировать самостоятельно
10 способов научиться программировать самостоятельно
10 способов научиться программировать самостоятельно
Хотите научиться кодить и освоить алгоритмы? Собрали десять советов с чего начать изучение программирования для тех, кто только начинает своё путешествие в мир программирования и снабдили все это полезными ссылками на курсы для начинающих программистов.
Microsoft запустила обучающий сайт по Java
Microsoft запустила обучающий сайт по Java
Microsoft запустила обучающий сайт по Java
1 комментарий
Apple взялась за расшифровку голосовых сообщений в Telegram
Apple взялась за расшифровку голосовых сообщений в Telegram
Apple взялась за расшифровку голосовых сообщений в Telegram
1 комментарий

Хотите сообщить важную новость? Пишите в Telegram-бот

Главные события и полезные ссылки в нашем Telegram-канале

Обсуждение
Комментируйте без ограничений

Релоцировались? Теперь вы можете комментировать без верификации аккаунта.

Комментариев пока нет.