Свойства в Objective C подробно и понятно
Несмотря на определенную простоту работы со свойствами в Objective C, то тут то там я постоянно натыкаюсь на вопросы, которые свидетельствуют о глубинном непонимании тех процессов, которые реально происходят при их определении. Именно поэтому, я взял на себя смелость провести небольшой экскурс по тому, как же на самом деле работают свойства в этом языке.
Для начала, представим себе простейший класс, в котором мы определим одно свойство.
@interface Example : NSObject
@property NSObject *property;
@end
@implementation Example
@synthesize property;
@end
Такой код скомпилируется и даже будет работать, породив предупреждение компилятора о необходимости определения семантики мутатора свойства. Предупреждение говорит нам о том, что код, который мы написали, аналогичен следующему:
@interface Example : NSObject
@property (assign) NSObject *property;
@end
@implementation Example
@synthesize property;
@end
Что, в свою очередь, раскрывается так:
@interface Example : NSObject
{
@private
NSObject *property;
}
@property (assign) NSObject *property;
@end
@implementation Example
-(NSObject *)property
{
return property;
}
-(void)setProperty:(NSObject *)value
{
property = value;
}
@end
Фактически, при определении свойства через synthesize создается backend-поле с именем, аналогичным имени свойства. Это не всегда хорошо, потому что создает определенные проблемы с перекрытием имен, поэтому я рекомендую определять имя backend-свойства явно.
@interface Example : NSObject
@property (assign) NSObject *property;
@end
@implementation Example
@synthesize property = _property;
@end
Порождаемый в этом случае код аналогичен следующему:
@interface Example : NSObject
{
@private
NSObject *_property;
}
@property (assign) NSObject *property;
@end
@implementation Example
-(NSObject *)property
{
return _property;
}
-(void)setProperty:(NSObject *)property
{
_property = value;
}
@end
Однако, вы можете заметить, что assign-семантика мутатора для reference-объектов со счетчиком ссылок (а таковыми являются все объекты, производные от NSObject) это что-то не совсем правильное, ведь переменная после присваивания может быть уничтожена через release. Специально для этого, существует еще две семантики, которые поддерживаются synthesize – это retain и copy.
Принцип работы retain очевиден — при присваивании полю экземпляра объекта через мутатор, счетчик ссылок увеличивается на единицу. Однако, несколько неочевидно то, что происходит при доступе к объекту через аксесор. Рассмотрим простой пример.
@interface Example : NSObject
@property (retain) NSObject *property;
@end
@implementation Example
@synthesize property = _property;
@end
Порождает код:
@interface Example : NSObject
{
@private
NSObject *_property;
}
@property (retain) NSObject *property;
@end
@implementation Example
-(NSObject *)property
{
return _property;
}
-(void)setProperty:(NSObject *)property
{
[property retain];
[_property release];
_property = property;
}
@end
Обращаю ваше внимание на два крайне важных момента.
Момент первый. Доступ через аксесор не увеличивает счетчик ссылок. То есть экземпляр класса, который вы получили через аксесор, может быть уничтожен в любой момент, а значит — вы должны явно управлять его временем жизни.
Таким образом, данный код является неправильным.
Example *instance = [[Example alloc] init];
NSObject *value = instance.property;
// Do something with “value”
[instance release];
Проблема его заключается в том, что если в том месте, где вы do something, произойдет присваивание полю instance.property какого-либо значения, ваш экземпляр объекта в переменной value будет уничтожен в силу того, что в мутаторе свойства произойдет уменьшение счетчика ссылок до потенциального нуля.
Чтобы защититься от этого, необходимо при работе всегда явно брать ссылку.
Example *instance = [[Example alloc] init];
NSObject *value = [instance.property retain];
// Do something with “value”
[value release];
[instance release];
Момент второй. Определение свойства через synthesize не определяет деаллокатора свойства. Таким образом, в случае простейшего объекта без предопределенного метода dealloc мы получим утечку.
Таким образом, если мы определили свойство через synthesize — мы обязаны переопределять деаллокатор и писать в нем код, явно освобождающий объект. Это можно сделать одним из двух способов.
Способ первый — через присваивание свойству значения nil.
-(void)dealloc
{
self.property = nil;
[super dealloc];
}
Способ второй — через ручное уменьшение ссылки у backend-поля.
-(void)dealloc
{
[_property release];
[super dealloc];
}
Лично я предпочитаю второй способ, но рекомендую первый — он гораздо понятнее и лучше читается.
Момент третий. Любое присваивание backend-полю через мутатор, определенный с retain, увеличивает счетчик ссылок. Однако, если вы присваиваете значение напрямую, например, в конструкторе — счетчик ссылок увеличен не будет.
Таким образом, код
-(id)initWithProperty:(NSObject *)property
{
self = [super init];
if(!self)
return nil;
self.property = property;
return self;
}
Является совершенно корректным. А код
-(id)initWithProperty:(NSObject *)property
{
self = [super init];
if(!self)
return nil;
_property = property;
return self;
}
Ужасен, так как число ссылок не увеличивается, а значит — вы неминуемо получите SIGABRT где-нибудь в процессе выполнения приложения. Правильно делать так:
-(id)initWithProperty:(NSObject *)property
{
self = [super init];
if(!self)
return nil;
_property = [property retain];
return self;
}
И упаси вас Б-г от такого кода:
-(id)initWithProperty:(NSObject *)property
{
self = [super init];
if(!self)
return nil;
self.property = [property retain];
return self;
}
Здесь ссылка на экземпляр объекта берется дважды, что при отсутствии двойного освобождения неминуемо ведет к утечкам. Я бы не обращал на это внимания, но подобная ошибка мне встречалась неоднократно в некоторых Open Source проектах.
Нередко, впрочем, она компенсируется двойным освобождением.
-(id)initWithProperty:(NSObject *)property
{
self = [super init];
if(!self)
return nil;
self.property = [property retain];
return self;
}
-(void)dealloc
{
[self.property release];
self.property = nil;
[super dealloc];
}
Так писать, конечно, можно, но не нужно.
Кроме того, существует возможность определить для synthesize семантику копирования в мутаторе. Выглядит и работает она практически так же, как семантика взятия ссылки, но в отличие от последней вызывает не retain, а copy, что требует от объекта определенных телодвижений для поддержки ее работы.
Во-первых, следует помнить, что объект должен поддерживать протокол NSCopying. Лучше делать это явно, чтобы скинуть проверку ошибок несоответствия на компилятор.
@interface Example : NSObject
@property (copy) NSObject<NSCopying> *property;
@end
Теперь при определении свойства через synthesize будет сгенерирован код:
@implementation Example
-(NSObject<NSCopying> *)property
{
return _property;
}
-(void)setProperty:(NSObject<NSCopying> *)property
{
NSObject<NSCopying> *pCopy = [property copy];
[_property release];
_property = pCopy;
}
@end
Осталось только определить деаллокатор, который бы освобождал ссылку на копию объекта — и дело сделано.
Однако, не стоит надеяться на то, что все свойства у любых объектов будут вести себя подобным образом. Если мы проинспектируем свойство view у объекта класса UIViewController, то с ужасом обнаружим, что аксесор данного свойства имеет под собой примерно следующий код:
-(UIView *)view
{
if(!_view)
[self loadView];
return [[_view retain] autorelease];
}
На данном примере можно осознать, что мутатор и аксесор свойства — это обычные методы, которые могут вызывать любые другие. Так, например, в данном случае аксесор проверяет, было ли загружено представление, и если нет — загружает его, после чего увеличивает счетчик ссылок на единицу и отдает объект в autorelease pool.
Чем хорош подход с пулом? Тем, что он позволяет застраховаться от ошибок, когда мы не сделали retain при получении значения свойства через аксессор. Объект в пуле освободится только при выходе из текущего user code стека вызова, а значит у нас есть определенная гарантия того, что объект не освободится прежде, чем мы закончим с ним работать.
Почему такую семантику не определяет synthesize? Дело в том, что она требует активного autorelease pool в текущем потоке, чего в общем случае гарантировать нельзя, а закладываться на необходимость его определения везде и всюду — это значит тратить лишние ресурсы на поддержание ненужной инфраструктуры.
Однако, следует помнить, что в общем случае при получении и присваивании свойства может выполняться произвольный код, а потому имеет смысл опираться только на публичный контракт свойства. А именно, при получении через аксесор свойства любого объекта ссылочного типа необходимо сделать retain. Ну и release в конце.
14 Responses to Свойства в Objective C подробно и понятно
Leave a Reply Cancel reply
-
Categories
-
Archives
- May 2017
- April 2017
- March 2017
- January 2017
- September 2016
- July 2016
- May 2016
- March 2016
- December 2015
- November 2015
- September 2015
- May 2015
- March 2015
- December 2014
- November 2014
- October 2014
- September 2014
- August 2014
- July 2014
- June 2014
- May 2014
- April 2014
- March 2014
- February 2014
- January 2014
- December 2013
- September 2013
- August 2013
- July 2013
- June 2013
- May 2013
- April 2013
- March 2013
- February 2013
- January 2013
- December 2012
- November 2012
- October 2012
- September 2012
- August 2012
- July 2012
- June 2012
- May 2012
- April 2012
- March 2012
- February 2012
- January 2012
- December 2011
- November 2011
- October 2011
-
Meta
Hi!
Thanks a lot! I’ve spent a lot of time to understand property in Objective C, your explanation is very clear.
Regards,
Andy
Спасибо за подробное объяснение – чуть ли не единственное в интернете.
Подскажите – описанные нетривиальные моменты про retain/assign действительны для ARC?
Да, там все то же самое, за исключением того факта, что ручные вызовы заменяются автоматическими. И ARC на всякий случай берет ссылку у любой переменной, полученной из аксессора во время ее работы, если не объявить ее как __weak.
Очень доходчиво. Спасибо за статью. Привет Вам из Германии ;)
@property (retain) NSObject *property;
если не указано явно nonatomic то
генерируется потокобезопасный сеттер,
в данной реализации это не учтено,
собственно это справедливо не только для retain.
можно посмотреть как это делается тут
http://www.cocoawithlove.com/2009/10/memory-and-thread-safe-custom-property.html
также retain getter надо писать вот так
- (MyClass *)someInstance
{
return [[someInstance retain] autorelease];
}
во избежании проблем с таким кодом
MyClass *myInstance= [anObject someInstance];
[anObject release];
[myInstance doSomething];
Приведенные “развертки” стандартного synthesize совершенно неверны. По-умолчанию все property имеют спецификатор “atomic” – из-за этого код, которой будет сгенерирован @synthesize для них, будет сильно отличаться от того, что преведено в статье. Подробнее об использовании atomic/nonatomic и последствиях лучше почитать одельно (легко найти например на stackoverflow). Отбросив особенности в плане атомарного доступа, оказывается, что для atomic property значение, возвращаемое из синтезированного геттера гарантированно будет находиться в autorelease pool. Таким образом, в приведенном “неправильном” примере все будет работать нормально и без каких-либо изменений:
Example *instance = [[Example alloc] init];
NSObject *value = instance.property;
// Do something with «value»
[instance release];
Кроме того, автор приводит несколько примеров, в которых используется прямое обращение через свойства в конструкторах и деструкторах объектов – и даже советует таким образом “освобождать” ссылки свойств в dealloc. Это еще одна распространенная ошибка – перегрузка аксессоров таких свойств в самом классе или его потомках приведет к выполнению кода у экземпляров объекта с невалидным состоянием. См. у Apple или более общий принцип “не использовать прямо или косвенно не-приватных методов класса в конструкторах и деструкторах”.
Этот пост – очередная демонстрация того, насколько противоречив и плох дизайн Objective C как языка для практической разработки. Автор, очевидно, приложил усилия, чтобы разобраться в предмете (использование Objective C properties), но тем не менее многие тонкие моменты остались не учтены. В результате статья, которая должна помочь разобраться начинающему пользователю, сама по себе содержит критические ошибки. Можно было бы списать все на некомпетентность автора, но я далеко не первый раз встречаюсь с подобным. Поэтому считаю, что в первую очередь виноваты плохой дизайн языка и низкое качество вводной и сопутствующей документации.
Хорошая статья, спасибо!
[…] свойств, которые относятся к системе подсчета ссылок: тык. Так что я снимаю с себя необходимость пытаться […]
Простите, не могу понять одну вещь. Вот в Стендфордских лекциях по Objective-C говорят, что одним из ключевых плюсов и необходимости использования @property является возможность реалиции lazy instantiation. Как я понял, lazy instantiation нужна, чтобы у нас не возникало таких ситуаций, когда мы хотим получить значение объекта, который еще не создан. Вот такой пример:
—————————————————————————————————-
.h
@property (nonatomic, strong) NSMutableArray* myArray;
.m
@synthesize myArray = _myArray;
– (NSMutableArray*) myArray
{
if (!_myArray){
_myArray = [[NSMutableArray alloc] initWithCapacity:2];
}
return _myArray;
}
—————————————————————————————————-
Он реализован с помощью @property. Могу ли я реализовать через обычные ivar и аксессоры?
.h
Class: NSObject{
@protected
NSMutableArray* _myArray;
}
.m
– (NSMutableArray*) myArray
{
if (!_myArray){
_myArray = [[NSMutableArray alloc] init];
}
return _myArray;
}
—————————————————————————————————-
Заранее простите, если вопрос некорректен, но прошу дать подробный и вразумительный ответ.
Если вкратце — да, можете, в 3 шага.
1. Определить в интерфейсе @property
2. Определить ivar нужного уровня доступа
4. Написать методы setVariable: и variable
Сейчас компилятор довольно умен, и если находит прописанный мутатор и аксессор, то ничего не делает.
Если интересны детали, то нужно задать больше наводящих вопросов.
Я просто не могу понять основных отличий и преимуществ @property перед ivar. Что делает @property?
Это разные вещи. @property это декларация интерфейса (пары методов — мутатора и аскессора), а ivar — это уже детали реализации. В общем случае они вообще никак не связаны.
А так, это синтаксический сахар. Можно определить в интерфейсе пару методов, а можно одну property. Тогда вместо [object setInteger:[object integer] + 3]; можно будет написать object.integer += 3;
1. А @property декларирует, помимо мутатора и аксессора, еще ivar? Ведь, мутатор и аксессор нуже для установки/получения значения ivar из “вне”. Мы как бы задекларировали методы доступа, а к чему именно не понятно.
2. @synthesize синтезирует реализацию методов доступа? И если мы напишем =_ivar, то еще и объявит _ivar?
3. Вот с Вашим примером немного не могу понять.
—–.h———————————————-
#import
@interface Class_3 : NSObject{
int ivar_a;
}
-(void)setInteger:(int) variable;
-(int)integer;
@end
—–.m———————————————-
#import “Class_1.h”
@implementation Class_1
-(void)setIvar_a:(NSNumber*) variable{
ivar_a = variable;
}
-(NSNumber*)Ivar_a{
return ivar_a;
}
-(void)setInteger:(int) variable{
ivar_a = [NSNumber numberWithInt:variable];
}
-(int)integer{
return [ivar_a intValue];
}
@end
——-main.m————————————————-
Class_1* tester = [[Class_1 alloc] init];
tester.integer = 3;
[tester setInteger:3+[tester integer]];
tester.integer+=3;
NSLog(@”%d”,tester.integer);
——————————————————————-
Вот тот код работает также, как и этот?Об этом вы говорили в своем примере?
—–.h———————————————-
#import
@interface Class_1 : NSObject
@property(nonatomic)int integer;
@end
—–.m———————————————-
#import “Class_1.h”
@implementation Class_1
@synthesize integer=ivar_a;
@end
——-main.m————————————————-
Class_1* tester = [[Class_1 alloc] init];
tester.integer = 3;
[tester setInteger:3+[tester integer]];
tester.integer+=3;
NSLog(@”%d”,tester.integer);
——————————————————————-
4. “[object setInteger:[object integer] + 3]; можно будет написать object.integer += 3;” Вот в моем примере, что с @property, что без него в main.m можно записать tester.integer+=3;
Ой простите, в первом случае не то в .m файле написал
#import “Class_1.h”
@implementation Class_1
-(void)setInteger:(int) variable{
ivar_a = variable;
}
-(int)integer{
return ivar_a;
}
@end