company_banner

10 малоизвестных возможностей Objective-C

  • Tutorial
Приветствую уважаемых хабражителей!

Objective-C — язык с богатым рантаймом, но в данной статье речь пойдёт не о содержимом хедера <objc/runtime.h>, а о некоторых возможностях самого языка, о которых многие разработчики и не догадываются. Да, на них натыкаешься, читая документацию, отмечаешь про себя «хм, интересно, надо как-нибудь копнуть», но они обычно быстро вылетают из головы. А начинающие разработчики часто вообще читают документацию наискосок.

В этой статье я собрал 10 удивительных на мой взгляд свойств языка Objective-C. Некоторые свойства самоочевидны, некоторые далеко не таковы. За использование некоторых в боевом коде надо бить по рукам, другие же способны помочь в оптимизации критических мест кода и в отладке. В конце статьи имеется ссылка на исходник, показывающий на примере все эти фичи.

Итак, начну с самого «вкусного» на мой взгляд: безымянные методы.

1. Безымянные методы



Имя метода задавать не обязательно, если у него имеются аргументы. Нередко встречаются методы типа - (void)setSize:(CGFloat)x :(CGFloat)y, но это можно довести и до абсолюта:

@interface TestObject : NSObject

+ (id):(int)value;
- (void):(int)a;
- (void):(int)a :(int)b;

@end

// ...

TestObject *obj = [TestObject :2];
[obj :4];
[obj :5 :7];


Забавно выгладят и селекторы для таких методов: @selector(:) и @selector(::).

Рекомендации по использованию: только в исследовательских целях.

2. Новый синтаксис применим к любым объектам


Квадратные скобки для доступа к элементам массива или словаря можно использовать и со своими объектами. Для этого надо объявить следующие методы.

Для доступа по индексу:
- (id)objectAtIndexedSubscript:(NSUInteger)index;
- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)index;


Для доступа по ключу:
- (id)objectForKeyedSubscript:(id)key;
- (void)setObject:(id)obj forKeyedSubscript:(id)key;


Используем:
id a = obj[1];
obj[@"key"] = a;


Рекомендации по использованию: иногда можно, но только если Ваш класс не является коллекцией. Если является, лучше наследоваться от имеющихся.

3. Неявные @property


Объявление @property в хедере на самом деле объявляет лишь геттер и мутатор для некоторого поля. Если их объявить напрямую, ничего не изменится:

- (int)value;
- (void)setValue:(int)newValue;

obj.value = 2;
int i = obj.value;


Так же неявные свойства — это любые функции, не принимающие аргументов, но возвращающие значение:

NSArray *a = @[@1, @2, @3];
NSInteger c = a.count;


А ещё — функции, имя которых начинается на «set», ничего не возвращающие, но принимающие один аргумент:

@interface TestObject : NSObject
- (void)setTitle:(NSString *)title;
@end;

//...

TestObject *obj = [TestObject new];
obj.title = @"simple object";


Рекомендации по использованию: объявление @property выглядит гораздо лучше, для этого и было введено. К свойствам лучше обращаться через ".", а вот обычные методы лучше вызывать через "[]". Иначе начинает сильно страдать читаемость кода.

4. Ручное выделение памяти под объект без alloc


Объекты можно создавать с помощью старой доброй сишной функции calloc, задав потом isa вручную. В принципе, это лишь замена alloc, а init можно отправить и потом.

// Выделяем память, заполненную нулями
void *newObject = calloc(1, class_getInstanceSize([TestObject class]));
// Задаём isa прямой записью в память
Class *c = (Class *)newObject;
c[0] = [TestObject class];
// Здесь __bridge_transfer-каст нужен для передачи объекта в ARC - иначе утечёт
obj = (__bridge_transfer TestObject *)newObject;
// Посылаем init - объект готов!
obj = [obj init];


Рекомендации по использованию: рекомендуется с превеликой осторожностью. Один из вариантов использования — выделение памяти разом под большой массив объектов (о чём рассказывал некогда AlexChernyy на CocoaHeads).

UPD: Благодаря пользователю Trahman было установлено, что данный подход не работает на iOS x64. С его же помощью было найдено портабельное решение:

object_setClass((__bridge id)newObject, [TestObject class]);


5. Распечатать текущий авторелиз-пул


У ObjC имеются «скрытые» функции, доступ к которым можно получить с помощью extern:

extern void _objc_autoreleasePoolPrint(void);


После вызова этой функции в консоль будет выведено содержимое текущего авторелиз-пула, примерно в таком виде:

objc[26573]: ##############
objc[26573]: AUTORELEASE POOLS for thread 0x7fff72fb0310
objc[26573]: 9 releases pending.
objc[26573]: [0x100804000]  ................  PAGE  (hot) (cold)
objc[26573]: [0x100804038]  ################  POOL 0x100804038
objc[26573]: [0x100804040]       0x100204500  TestObject
objc[26573]: [0x100804048]       0x100102fc0  __NSDictionaryM
objc[26573]: [0x100804050]       0x1007000b0  __NSArrayI
objc[26573]: [0x100804058]       0x1006000a0  __NSCFString
objc[26573]: [0x100804060]       0x100600250  NSMethodSignature
objc[26573]: [0x100804068]       0x100600290  NSInvocation
objc[26573]: [0x100804070]       0x100600530  __NSCFString
objc[26573]: [0x100804078]       0x100600650  __NSArrayI
objc[26573]: ##############


Это бывает очень полезно для дебага. Можно поискать и другие полезные штуки здесь: www.opensource.apple.com/source/objc4/objc4-551.1

Рекомендации по использованию: в дебаге — пожалуйста.

6. Прямой доступ к значениям синтезированных @property


Как уже говорилось выше, @property лишь генерирует сигнатуры геттера и мутатора. Но если свойство синтезированное (через @synthesize или по умолчанию), то кроме этого генерируется и ivar:

@property NSMutableDictionary *dict;

- (void)resetDict
{
	_dict = nil;
}


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

7. Доступ к публичным ivar-ам как в структурах


Несмотря на очевидность данной фичи, многие разработчики о ней почему-то не знают. Знание это может быть полезно для разрешения конфликта имён.

@interface TestObject : NSObject
{
@public
	int field;
}

@implementation TestObject

- (void)updateWithField:(int)field
{
	self->field = field;
}

@end

// ...

TestObject *obj = [TestObject new];
obj->field = 200;


Рекомендации по использованию: в ObjC лучше для таких целей использовать @property.

8. instancetype


В ObjC имеется замечательный тип id, который по сути является NSObject *, то есть, самым базовым типом для объектов, к которому можно привести любой другой объект. Удобно, но в некоторых случаях могут возникнуть проблемы. К примеру:

[[MyClass sharedInstance] count];


Если sharedInstance возвращает id, то код соберётся без предупреждений даже если в MyClass нет метода count. Если же sharedInstance будет возвращать instancetype, то ворнинг всё же появится, ведь компилятор явно понимает, что возвращается объект того класса, у которого вызван sharedInstance.

Рекомендации по использованию: уместно в методах типа init/new/copy и т.п.

9. Проксирование: forwardingTargetForSelector: и forwardInvocation:


Для ООП свойственно введение дополнительных уровней абстракции при некоторых проблемах. К примеру, иногда нужно из объекта сделать прокси для другого объекта. В этом нам всегда помогут следующие методы.

Если в нашем объекте не найдена имплементация для некоторого селектора, то ему будет послано следующее сообщение:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
	if ([_dict respondsToSelector:aSelector])
	{
		return _dict;
	}
	return [super forwardingTargetForSelector:aSelector];
}


Если объект, который мы проксируем, отвечает на селектор, то пусть он и реагирует на него. Иначе всё таки придётся кинуть эксепшн.

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

Сначала на запрос сигнатуры неизвестного нам метода мы должны вернуть сигнатуру существующего метода проксируемого объекта:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
	NSMethodSignature *sig = [super methodSignatureForSelector:aSelector];
	if ([NSStringFromSelector(aSelector) isEqualToString:@"allDamnKeys"])
	{
		sig = [_dict methodSignatureForSelector:@selector(allKeys)];
	}
	return sig;
}


Затем перенаправляем вызов и меняем имя селектора:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
	if ([NSStringFromSelector(anInvocation.selector) isEqualToString:@"allDamnKeys"])
	{
		anInvocation.selector = @selector(allKeys);
		[anInvocation invokeWithTarget:_dict];
	}
}


Рекомендации по использованию: очень удобный механизм для реализации прокси-объектов. Возможно, кто-то найдёт и другие варианты использования. Главное — не переусердствовать: код должен быть читаем и легко понимаем.

10. NSFastEnumeration


Что ж, в заключение ещё одна интересная фича ObjC: циклы for..in. Их поддерживают все дефолтные коллекции, но можем поддержать и мы. Для этого надо поддержать протокол NSFastEnumeration, а точнее — определить метод countByEnumeratingWithState:objects:count:, но не всё так просто! Вот сигнатура этого метода:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len


Этот метод будет вызван каждый раз, когда runtime захочет получить от нас новую порцию объектов. Их мы должны записать либо в предоставленный буфер (размер его len), либо выделить свой. Указатель на этот буфер надо поместить в поле state->itemsPtr, а количество объектов в нём вернуть из функции. Так же не забываем, что (в документации этого нет) поле state->mutationsPtr не должно быть пустым. Если этого не сделать, то мы получим неожиданный SEGFAULT. А вот в поле state->state можно записать что угодно, но лучше всего — записать количество уже отданных элементов. Если отдавать больше нечего, нужно вернуть ноль.

Вот мой пример реализации этой функции:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len
{
	if (state->state >= _value)
	{
		return 0;
	}
	NSUInteger itemsToGive = MIN(len, _value - state->state);
	for (NSUInteger i = 0; i < itemsToGive; ++i)
	{
		buffer[i] = @(_values[i + state->state]);
	}
	state->itemsPtr = buffer;
	state->mutationsPtr = &state->extra[0];
	state->state += itemsToGive;
	return itemsToGive;
}


Теперь можно использовать:

for (NSNumber *n in obj)
{
	NSLog(@"n = %@", n);
}


Рекомендации по использованию: может быть полезно для упрощения работы с кастомными структурами данных, к примеру, с деревьями и связанными списками.

Заключение


Полный исходный код проекта доступен на гитхабе. Если у Вас, уважаемый читатель, есть что дополнить — не стесняйтесь и пишите в комментариях.

Успехов всем в разработке и да пребудет с вами Clang Analyzer!
Mail.ru Group
1126.71
Строим Интернет
Share post

Comments 68

    0
    Method swizzling — еще одна из возможностей рунтайма, которой бывает полезно воспользоваться. Позволяет подменить любой метод своей реализацией. С сохранением возможности вызова оригинального метода. Т.е. для любого системного и своего (но в случае своих классов такие плюшки редко нужны) объекта, можно добавить свою прослойку в любой метод. Использовать с осторожностью. :)

    А еще associated objects. Если простыми словами, вы можете отдать во владение любому NSObject-у другой NSObject, он его будет retain-ить и отпустит, когда будет освобождать свою память.
      +1
      В рантайме много плюшек, но тут описаны как раз скрытые возможности без его использования. Так что это не «еще одна из»
        0
        В статье упоминаются class_getInstanceSize и _objc_autoreleasePoolPrint. Первый, относится к рантайму, а второй, к его скрытым возможностям.

        Какие еще плюшки рантайма вы используете в разработке? Я только те две, что указал в своем первом комментарии.
        +3
        В Method Swizzling очень много подводных камней.
        Чаще всего программист забывает о том, что подмену метода может осуществлять не только его код, но и другие библиотеки, причем, возможно, в то же самое время в другом потоке. Тема в целом сложная, и раскрыть её в одном комментарии не получится, но скажу, что когда в одном проекте возникла необходимость в Swizzling, я просмотрел все доступные библиотеки для этого, и во всех были ошибки. Затем стал смотреть, как это реализовано в крупных библиотеках (типа ReactiveCocoa от GitHub), и везде находил всё те же баги (пример).

        Короче, пришлось собрать список возможных подводных камней, и написать свою библиотеку RSSwizzle, где уж точно всё сделано как надо. :)

        В общем, Swizzling штука хоть местами и необходимая, но весьма опасная. В production применять стоит крайне осторожно, только если не осталось других способов решить проблему.
          +1
          Хм, а напишите статью об этом. Будет очень интересно почитать. Я когда-то делал свизлинг «в лоб», но всех проблем не учитывал.
            +1
            Если писать статью, то хорошо оформлять, и с картинками, на это нужно время. Но попробую.
              0
              да статья конечно же не помешала бы… Я тоже пришел к выводу что технику swizling не стоит использовать вообще а вместо нее лучше использовать class_replaceMethod нужно использовать, но вижу вы там еще более серьезно подошли к вопросу. Однако позвольте небольшое замечание к библиотеке. Имхо библиотека должна предотвращать возможность выстрелить себе в ногу, а поэтому идея swiizling методов суперкласса мне кажется не очень хорошей. В конце концов если программисту захочется swizl-ить суперклассовые методы он может вызвать функцию для суперкласса напрямую.
          +2
          associated_objects довольно тормозные, и зачастую используются в качестве костылей.
          В той же статье, по вашей ссылке, явно можно обойтись без них, но многие люди начинают это использовать именно так.
          Я бы вообще предложил не говорить о них вслух, дабы не популяризировать :)
            0
            Довольно тормозные по сравнению с чем?
            Вобще тема хаков и скрытых возможностей полна таких вещей, которые следует использовать с осторожностью. :) На то это и хаки и скрытые возможности, а не best practice.
              +1
              Тормозные относительно простого доступа/записи к свойству/ivar'у.
              0
              А если не затруднит, можно ссылку на сравнительные тесты производительности со стандартными коллекциями? (с учетом atomic/nonatomic опций).
              Сильно подозреваю, что по скорости будет сравнимо с обычными properties.
                +1
                Ссылку не дам, т.к. это был синтетический локальный тест в рамках этого проекта.

                Но суть была примерно такая:
                for (NSInteger i = 0; i < loop; i++) {
                    [[[Test alloc] init] associatedObject];
                }
                ///
                - (NSObject *)associatedObject {
                    static char const associatedObjectKey;
                    id associatedObject = objc_getAssociatedObject(self, &associatedObjectKey);
                    if (!associatedObject) {
                        associatedObject = [[NSObject alloc] init];
                        objc_setAssociatedObject(self, &associatedObjectKey, associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
                    }
                    return associatedObject;
                }
                
                ////////
                
                for (NSInteger i = 0; i < loop; i++) {
                    [[[Test alloc] init] ivarObject];
                }
                
                ////
                - (NSObject *)ivarObject {
                    if (!_ivarObject) {
                        self.ivarObject = [[NSObject alloc] init];
                    }
                    return _ivarObject;
                }
                
                



                Если у вас есть другая информация, то буду рад посмотреть/почитать.

                З.Ы. что-то я ниасилил спойлер…
                  +1
                  А нет, осилил, нужно было просто обновить страницу...
            +3
            Не дай бог встретить в проекте безымянные методы)
              +1
              Я встретил. Поэтому и написал про них ;)
                0
                «Нормальная практика», на гитхабе есть достаточно «либ» которые юзают этот «подход».
                  0
                  А можете привести пример?
                    0
                    хм, навскидку не вспомнил, поиски тоже не увенчались успехом.
                    Помню как минимум один контрол: popup или popover, у него немало звезд, и вот такие методы у него были.
                    Иногда встречаются вещи типа «foo:::», «bar::» и т.п.
                      +1
                      Тут используется для вызова reduce блока, с произвольным числом параметров.
                        0
                        Ну, здесь полуанонимные функции. Если бы там было
                        - (id):(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 :(id)obj6 :(id)obj7 :(id)obj8 :(id)obj9 :(id)obj10 :(id)obj11 :(id)obj12 :(id)obj13 :(id)obj14 :(id)obj15;
                        

                        вот тогда было бы в тему.
                        0
                        <--тут был коммент не туда-->
                  • UFO just landed and posted this here
                      0
                      На мой взгляд, пункты 2,6,10 — общеизвестные

                      Как показывает мой опыт, даже опытные разработчики могут не знать что-то из этих пунктов.

                      а остальные, согласно Вашим собственным ремаркам, мало применимы

                      «Мало применимы» не означает «бесполезны».
                      +2
                      7. Доступ к публичным ivar-ам как в структурах

                      Кстати, именно по этой причине советуют проверять self в конструкторах, вида:

                      if (self) {
                      _foo = @"Bar";
                      }
                      

                      В случае если self будет nil'ом, то получим:

                      self->_foo = @"Bar";
                      

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

                      А в случае с отправкой сообщений/доступу к свойствам эту проверку можно опустить

                      self.foo = @"Bar";
                      

                      здесь краша уже не будет в любом случае.
                        +1
                        self.foo = @"Bar";
                        
                        Это тоже плохая практика, по своему. Во время инициализации self находится в «подвешенном» состоянии, и пока инициализация не закончена, лучше не посылать ему сообщения. Считаю что первый вариант (с ivar и проверкой self) лучше.
                        Don’t Message self in Objective-C init (or dealloc)
                        qualitycoding.org/objective-c-init/
                          +1
                          Простите, но не соглашусь.
                          Что значит «подвешенное» состояние? После alloc'а у нас есть полноценный объект, который нужно заполнить актуальными данными.
                          Здесь нет какого-либо неопределенного поведения или сайд-эффектов.

                          P.S. а автор статьи по ссылке, имхо, просто льет воду не подкрепляя какими-то фактами. Я согласен что писать логику в конструкторе это не есть хорошая практика, но говорить что инициализация в инициализаторе это плохо — звучит очень бредово.

                          P.P.S. а может я просто не понял о чем написано в статье...
                            0
                            В конце статьи написано:
                            Having said “avoid sending messages to self in init and dealloc,” I now want to soften that statement. There are two places when it might be OK after all:

                            At the very end of init, and
                            At the very beginning of dealloc.


                            То есть, автор всё же не против, но «нужно думать головой» и не посылать сообщения ещё не созданному или уже удалённому объекту.
                              0
                              В простых случаях, таких как «чистые» геттеры в init, например, ничего плохого не может случится.

                              Но есть более сложные ситуации, и одна из них произошла у меня в реальном проекте. Я подробностей уже не помню, помню что в методе init я регистрировал свой класс-«одиночку» в KVO.

                              Вскоре обнаружилось, что в рантайме у меня создавались два инстанса синглтона. С тех пор я в init ничего лишнего не пишу.
                                0
                                Ну вот, т.е. все зависит от ситуации, а вы в прошлом комментарии описали догму «Don’t Message self in Objective-C init (or dealloc)».
                                Не знаю в чем у вас была проблема с KVO, но делать логику в конструкторе — плохо.
                                  0
                                  Мне кажется, что в данном случае проблема была в самой реализации синглтона. Если он сделан через dispatch_once и все нужные методы перегружены, то дважды быть созданным не может ну вообще никак.
                                    0
                                    Это было давно и неправда сделано через +initialize, когда не было еще ни GCD, ни блоков…
                                      0
                                      Тогда верю. Тем не менее, это лишь подтверждает сомнительность таких ограничений в современном мире.

                                      Согласен, вносить логику в к-тор и д-тор плохо, но обычный мутатор синтезированного свойства нельзя назвать логикой.
                                        0
                                        А если кто-то, допустим, переопределит (в категории или в унаследованном классе) ваш обычный мутатор вот таким образом?

                                        - (void) setProperty: (MyClass*) property
                                        {
                                            _property = property;
                                            _property.delegate = self;
                                        }
                                        
                                        

                                        Такого не может быть?
                                          0
                                          В принципе это возможно, согласен. Но!

                                          Что в этом реально плохого? Если установка делегата нам полюбому нужна, то её и в init написать можно. Если делегат не зануляется в dealloc, то программист сам создал грабли.
                                            0
                                            А что если делить на ноль и разыменовывать нулевой указатель?
                                            Не юзать деление и указатели?

                                            P.S. категории тоже сомнительная фича, имхо
                                              0
                                              А чем плохо переопределять сеттеры/геттеры? Или что в этом примере такого криминального? Мне правда интересно.
                                                0
                                                Ничего плохого, просто вы привели это как пример плохого кода, хотя с ним все в порядке, или я неправильно понял?
                                                  +3
                                                  С самим сеттером всё в порядке. Но если этот сеттер будет вызван в init'е, то property может начать посылать сообщения не до конца инициализированному объекту, чего мы явно не предполагали.

                                                  Рекомендация (кстати, официальная от Apple) не использовать в инициализаторах сеттеры как раз и связана с тем, что они (сеттеры) могут вызывать различные побочные эффекты, которые невозможно предугадать. Тот же, кто будет переопределять сеттер, тоже не должен мучиться вопросом «а что, если он используется в инициализаторе, не приведёт ли это к использованию не до конца проинициализированного объекта?» и т.п.
                                                  Надежнее использовать переменные экземпляра.
                                                    0
                                                    Спасибо!
                                                    Лучше бы я сам не написал.
                                                      0
                                                      С другой стороны встает вопрос:
                                                      «Должен ли я задать это свойство как ivar? Или я должен воспользоваться специально выделенным для этого инкапсулированным методом

                                                      Ну и зачем тогда вообще нужен сеттер? Сделать этот ivar публичным и разрешить всем в него писать, делов-то.
                                                      И не будет никаких сайд-эффектов, даже тех, которые необходимы.

                                                      И, как я уже писвл выше, на момент init'а объект уже является полноценным, нет никакого «подвешенного» состояния.

                                                      Если вы хотите меня переубедить в этом вопросе, то приведите, пожалуйста, реальные примеры из жизни, где посылка сообщений из init'а приводит к каким-то проблемам. А потом мы вместе подумаем где в этом примере ошибка.
                                                        +3
                                                        И, как я уже писвл выше, на момент init'а объект уже является полноценным, нет никакого «подвешенного» состояния.

                                                        Думаю, неправильно говорить, что на момент вызова init объект является полноценным, поскольку его поля не инициализированы и в этом кроется проблема. Скажем, если в некотором сеттере или другом вызываемом методе идет обращение к другому ivar или property, которое еще не инициализировано, это может привести к неожиданному поведению. (более сложный случай, когда есть переопределение сеттера в потомке с обращением к другому неинициализированному полю) Без примеров, поскольку у меня нет цели вас переубедить :)

                                                        Ссылка по теме на старую статью в блоге Майка Эша.
                                                          +3
                                                          Реальные примеры из жизни обычно в несколько тысяч строк, и на локализацию ошибки уходит не 2 минуты, так что просто приведу в коде то, о чем в ветке уже говорилось.

                                                          Программист пишет объект для работы с сетевым матчем мультиплеера, в инициализаторе использует сеттер:
                                                          @interface MatchSession : NSObject
                                                          @property (nonatomic, strong) GKMatch *match;
                                                          @end
                                                          
                                                          @implementation MatchSession
                                                          - (instancetype)initWithMatch:(GKMatch *)match
                                                          {
                                                              self = [super init];
                                                              if (self) {
                                                                  self.match = match; // 1
                                                              }
                                                              return self;
                                                          }
                                                          @end
                                                          


                                                          Далее, другой программист пишет наследника этого класса, в котором реализует приём и сохранение сообщений от матча.
                                                          @interface ExtendedMatchSession : MatchSession <GKMatchDelegate>
                                                          @property (nonatomic, readonly) NSMutableArray *messages;
                                                          @end
                                                          
                                                          @implementation ExtendedMatchSession
                                                          - (instancetype)initWithMatch:(GKMatch *)match
                                                          {
                                                              self = [super initWithMatch:match];
                                                              if (self) {
                                                                  _messages = [NSMutableArray new]; // 5
                                                                  match.delegate = self;
                                                              }
                                                              return self;
                                                          }
                                                          - (void)setMatch:(GKMatch *)match{
                                                              [super setMatch:match];
                                                              match.delegate = self; // 2
                                                          }
                                                          - (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID{
                                                              [_messages addObject:data]; // 4
                                                          }
                                                          @end
                                                          


                                                          И имеем программиста в Apple, который реализовал GKMatch примерно так:
                                                          - (void)setDelegate:(id<GKMatchDelegate>)delegate{
                                                              _delegate = delegate;
                                                              for (NSData *data in self.undeliveredData){
                                                                  [delegate match:self didReceiveData:data fromPlayer:@"..."]; // 3
                                                              }
                                                              // ...
                                                          }
                                                          


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

                                                          А если б в первом инициализаторе просто написать _match = match;, то никаких проблем бы не было.

                                                          (Если кто-то упустил, почему сообщения потеряются — следите за комментариями: "// 1" — номер указывает очередность выполнения участка кода).

                                                          P.S. Кстати, GKMatch реализован именно так — отсылает ранеее полученные сообщения в момент установки делегата.

                                                            +1
                                                            Пример получился немного корявый, но надеюсь суть про side эффекты ясна.
                                                              0
                                                              mish, Yan169, я понял о чем вы)
                                                              Насколько я понимаю прямой связи между посылкой сообщений из init'а и описанным багом нет, такого же эффекта можно добиться и другим способом.
                                                              В общем проблема не в init'е, а в самом коде.
                                                                0
                                                                Гм, вроде из моего примера очевидно, что использование сеттера вместо ivar в инициализаторе привело к неожиданному багу.
                                                                Но если вы считаете, что в коде MatchSession всё нормально, то как тогда по-вашему должен выглядеть код ExtendedMatchSession?
                                                                  0
                                                                  Если бы он был вызван _не_ из init'а, а сразу после него

                                                                  Foo *f = [Foo new];
                                                                  [f setMatch:m];
                                                                  


                                                                  то этого бы не произошло?
                                                                  Опять-таки, проблема не в init'е, а в корявом flow.
                                                                    0
                                                                    Конечно не произошло бы, т.к. инициализация объекта (включая переопределенный инициализатор) полностью бы завершилась. Посмотрите пример внимательнее.
                                                                  • UFO just landed and posted this here
                                                                      +2
                                                                      Это несерьезный костыль, нарушающий принцип подстановки Лисков. Потом первый программист будет гадать, почему в коде сессия инициализируется с матчем (через инъекцию, или фабрику, расширяя базовый класс), а в инициализатор приходит nil.
                                                                      Кроме того, нарушается инкапсуляция, т.к. код рассчитан на то, что исходник родительского класса известен и останется неизменным. Но это не так, там вполне обоснованно может появиться например NSParameterAssert(match).

                                                                      В конкретный момент времени создать в коде корректно работающую конструкцию конечно не проблема, но если цель — надежный и слабо связанный код без неожиданных сюрпризов из-за малейших изменений — стоит придерживаться официальных рекомендаций:
                                                                      Setter methods can have additional side-effects. They may trigger KVC notifications, or perform further tasks if you write your own custom methods.

                                                                      You should always access the instance variables directly from within an initialization method because at the time a property is set, the rest of the object may not yet be completely initialized. Even if you don’t provide custom accessor methods or know of any side effects from within your own class, a future subclass may very well override the behavior.
                                                                    0
                                                                    Повторюсь проблема в том, что мы имея до конца не созданный объект, вызываем некоторые методы, которые в общем случае (кроме случая, когда метод написан специально для инициализации) полагают, что объект уже полностью готов. Кроме того если в потомке такой метод будет переопределен, то сам потомок может быть не готов к тому, что его вызовут раньше (до того как сам потомок начнет инициализацию). Как раз последнее и хотел показать Yan169, если я правильно понял.
                                        0
                                        Ох, лучше уж ничего не писать о таких вещах. Знаю некоторых людей, которых хлебом не корми — дай ввернуть где-нибудь «новизну». Вчера смотрел WWDC, сегодня с горящими глазами тыкает в проект то-се, не разбирая где надо и где не очень.

                                        Потом везде оказываются ассоциативные объекты, связь с сервером реализована через Core Data, в половине мест вместо таблиц использованы Collevtion View и т.п.
                                          0
                                          Везде бывают крайности. Истина посередине ;)
                                            +2
                                            ёксель-пиксель, «связь с сервером реализована через Core Data» — это как?
                                              0
                                              вероятно имеется ввиду NSIncrementalStore
                                                +1
                                                вот так. К сожалению, это принципиально возможно, если написать свой Incremental Storage.
                                                +1
                                                Читал про NSIncrementalStore, но не пробовал на практике. По описанию показалась весьма рациональной реализацией клиент-серверного взаимодействия.

                                                Буду рад узнать какие у нее подводные камни, или может быть почему концепция в целом не рабочая.
                                                  0
                                                  Работать-то оно будет, но глючно. Ну к примеру есть у вас RESTful API, к процедурному ж вообще модель реляционную не привяжешь. Вы, скажем, удалили какую-то сущность, и кор дата пытается подчистить и обнулить все связи, которые указывали на эту сущность. Но это давно уже сделала серверная база данных! Получается, нужно писать хардкод — и код IS от таких хардкодов становится ужасно нечитаемым.

                                                  Хотите еще? Кор дата однопоточна, поэтому при запросе к серверу вам нужно создать синхронное NSUrlConnection-соединение. А если нужна авторизация NTLM и обработка Challenge-a? Создавайте семафоры, покрывайте код критическими секциями и т.п., но кровь из носа все должно отработать в одном потоке.

                                                  Во всех абсолютно случаях это избыточно и не нужно, любой самописный «фрэймворк» с сериализацией в объекты из JSON сработает куда лучше.
                                                +5
                                                // Выделяем память, заполненную нулями
                                                void *newObject = calloc(1, class_getInstanceSize([TestObject class]));
                                                // Задаём isa прямой записью в память
                                                Class *c = (Class *)newObject;
                                                c[0] = [TestObject class];
                                                // Здесь __bridge_transfer-каст нужен для передачи объекта в ARC - иначе утечёт
                                                obj = (__bridge_transfer TestObject *)newObject;
                                                // Посылаем init - объект готов!
                                                obj = [obj init];
                                                

                                                Неактуально.

                                                www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html
                                                On iOS for arm64, the isa field of Objective-C objects is no longer a pointer.
                                                Say what?

                                                On iOS for arm64, the isa field of Objective-C objects is no longer a pointer.
                                                If it's not a pointer anymore, what is it?

                                                Some of the bits still encode the pointer to the object's class. But neither OS X nor iOS actually uses all 64 bits of virtual address space. The Objective-C runtime may use these extra bits to store per-object data like its retain count or whether it has been weakly referenced.
                                                Why change it?

                                                Performance. Re-purposing these otherwise unused bits increases speed and decreases memory size. On iOS 7 the focus is on optimizing retain/release and alloc/dealloc.
                                                What does this mean for my code?

                                                Don't read obj->isa directly. The compiler will complain if you do. Trust the Compiler. The Compiler is your friend. Use [obj class] or object_getClass(obj) instead.

                                                Don't write obj->isa directly. Use object_setClass() instead.

                                                If you override +allocWithZone:, you may initialize your object's isa field to a «raw» isa pointer. If you do, no extra data will be stored in that isa field and you may suffer the slow path through code like retain/release. To enable these optimizations, instead set the isa field to zero (if it is not already) and then call object_setClass().

                                                If you override retain/release to implement a custom inline retain count, consider removing that code in favor of the runtime's implementation.

                                                The 64-bit iOS simulator currently does not use non-pointer isa. Test your code on a real arm64 device.
                                                  0
                                                  Вы правы. Если заменить прямую запись на
                                                  object_setClass((__bridge id)newObject, [TestObject class]);
                                                  

                                                  будет портабельно.
                                                    0
                                                    тоже об этом подумал, но уже после того как написал сообщение. :(
                                                      0
                                                      Добавил в пост.
                                                  0
                                                  К свойствам лучше обращаться через ".", а вот обычные методы лучше вызывать через "[]". Иначе начинает сильно страдать читаемость кода.


                                                  Спорно. Читаемость кода сильней страдает от [UIView class] (вместо UIView.class). Идемпотентные методы можно и нужно вызывать через точку.

                                                  instancetype для copy тоже надо применять осторожно, метод не всегда возвращает instancetype (-[NSMutableArray copy] вернет NSArray, например).
                                                    +4
                                                    > Читаемость кода сильней страдает от [UIView class] (вместо UIView.class)

                                                    Это субъективно.
                                                      0
                                                      Справедливо. Писать надо так, как написано в coding style проекта/команды, но последнее время мне близок coding style Гитхаба.
                                                        –2
                                                        мне уже иногда кажется, что я там работаю, половину их команды по osx/ios разработке знаю )
                                                      0
                                                      Насчет пункта 8. Как раз таки для init и new необязательно писать instancetype

                                                      Читать тут
                                                        +1
                                                        Необязательно, но для соблюдения единообразия на мой взгляд лучше всё-таки писать.
                                                          +1
                                                          Плюсую, автовывод типа для -init — по сути хак в компиляторе. Лучше писать instancetype явно.

                                                      Only users with full accounts can post comments. Log in, please.