Несмотря на определенную простоту работы со свойствами в 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 в конце.

Share →

14 Responses to Свойства в Objective C подробно и понятно

  1. Andy says:

    Hi!
    Thanks a lot! I’ve spent a lot of time to understand property in Objective C, your explanation is very clear.
    Regards,
    Andy

  2. Artem says:

    Спасибо за подробное объяснение – чуть ли не единственное в интернете.

    Подскажите – описанные нетривиальные моменты про retain/assign действительны для ARC?

    • bober_maniac says:

      Да, там все то же самое, за исключением того факта, что ручные вызовы заменяются автоматическими. И ARC на всякий случай берет ссылку у любой переменной, полученной из аксессора во время ее работы, если не объявить ее как __weak.

  3. Евгений says:

    Очень доходчиво. Спасибо за статью. Привет Вам из Германии ;)

  4. Павел says:

    @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];

  5. Maksim says:

    Приведенные “развертки” стандартного 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), но тем не менее многие тонкие моменты остались не учтены. В результате статья, которая должна помочь разобраться начинающему пользователю, сама по себе содержит критические ошибки. Можно было бы списать все на некомпетентность автора, но я далеко не первый раз встречаюсь с подобным. Поэтому считаю, что в первую очередь виноваты плохой дизайн языка и низкое качество вводной и сопутствующей документации.

  6. Дмитрий says:

    Хорошая статья, спасибо!

  7. […] свойств, которые относятся к системе подсчета ссылок: тык. Так что я снимаю с себя необходимость пытаться […]

  8. Антон says:

    Простите, не могу понять одну вещь. Вот в Стендфордских лекциях по 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;
    }
    —————————————————————————————————-
    Заранее простите, если вопрос некорректен, но прошу дать подробный и вразумительный ответ.

    • bober_maniac says:

      Если вкратце — да, можете, в 3 шага.

      1. Определить в интерфейсе @property
      2. Определить ivar нужного уровня доступа
      4. Написать методы setVariable: и variable

      Сейчас компилятор довольно умен, и если находит прописанный мутатор и аксессор, то ничего не делает.

      Если интересны детали, то нужно задать больше наводящих вопросов.

      • Антон says:

        Я просто не могу понять основных отличий и преимуществ @property перед ivar. Что делает @property?

        • bober_maniac says:

          Это разные вещи. @property это декларация интерфейса (пары методов — мутатора и аскессора), а ivar — это уже детали реализации. В общем случае они вообще никак не связаны.

          А так, это синтаксический сахар. Можно определить в интерфейсе пару методов, а можно одну property. Тогда вместо [object setInteger:[object integer] + 3]; можно будет написать object.integer += 3;

          • Антон says:

            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;

            • Антон says:

              Ой простите, в первом случае не то в .m файле написал
              #import “Class_1.h”
              @implementation Class_1
              -(void)setInteger:(int) variable{
              ivar_a = variable;
              }
              -(int)integer{
              return ivar_a;
              }
              @end

Leave a Reply

Войти с помощью: 

Your email address will not be published. Required fields are marked *

PageLines