Objective-C для программистов C++

 

Objective-C для программистов C++

Перевод серии статей David Chisnall «Objective-C for C++ Programmers»: Часть первая, часть вторая, часть третья

Автор перевода: n0xi0uzz
Источник: netsago.org

Objective-C был спроектирован как минимальный набор расширений для C, предоставляющих семантику, схожую со Smalltalk. Он также включает в себя схожий со Smalltalk синтаксис, с помощью которого проще увидеть, какая часть кода является чистым C, а какая — содержит расширения Objective-C.

В отличие от C++, Objective-C — это расширенный набор C. Каждая программа, написанная на C, является корректной программой Objective-C. Все новые ключевые слова в Objective-C начинаются с символа @, который не является корректным идентификатором C.

Система рантайма

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

В Objective-C объектами являются простые структуры на C, в которых первый элемент — указатель на другую структуру на C, представляющую класс. Что именно содержит эта структура класса — немного различается по времени запуска, но как минимум она содержит информацию для поиска методов, оболочку для переменных экземпляра («полей класса», говоря терминами C++) и некоторые другие метаданные.

Так как Objective-C является расширенным набором расширений над чистым C, а все возможности библиотеки рантайма (включая структуру классов) раскрываются в C, — это значит, что Objective-C полностью открывает объектную модель программисту. Нет никакой магии в Objective-C. Когда вы вызываете метод в C++, компилятор делает некоторые соответствующие действия с виртуальными таблицами, чтобы найти корректный код для запуска. В Objective-C, он делает что-то одно из следующих вариантов:
 
  • Вызовет функцию C для поиска функции, реализующий метод (с рантайм GNU)
  • Вызовет функцию C, которая ищет метод и вызывает его в одну операцию (с рантайм Apple).

Это значит, что вы можете писать чистый код на C, который взаимодействует с объектами Objective-C. Это также позволяет вам обходить динамическое поведение тогда, когда оно не нужно.

Немного истории

Objective-C был создан в ранних 1980-х Брэдом Коксом (Brad Cox), как набор минимальных расширений к C для просто поддержки объектов в стиле Smalltalk. Он вырос из проекта Object Oriented Pre-Compiler (OOPC), который имел очень похожую семантику, но действительно ужасный синтаксис.

Первоначальная реализация и торговая марка Objective-C была куплена компанией NeXT, и язык использовался повсеместно в их новой операционной системе. В NeXT на Objective-C сделали GUI тулкит и построитель интерфейсов. Это была первая коммерческая утилита для быстрой разработки приложений (Rapid Application Development, RAD). У неё также был хороший побочный эффект, заключавшийся в том, что построитель интерфейса не хранил одно только описание интерфейса — он хранил сериализованные объекты.

В ранних 1990-х NeXT и Sun сотрудничали в области разработки портативной версии программной среды NeXT. В то время, как первоначальная система NeXT во многих местах использовала чистый C, новая версия определила два фреймворка: Foundation и AppKit. Foundation определил чистую функциональность, включая коллекцию классов, строк и другие вещи. AppKit содержал GUI-фреймворки. Оба были определены в спецификации OpenStep, которая была реализована Next, Sun, а позже — проектом GNU. После покупки NeXT, Apple переименовала реализацию OpenStep на «Cocoa». Так как OpenStep базировался на ранних разработках NeXT, название всех классов в OpenStep начинаются с «NS».

Достаточно интересна история поддержки Objective-C для GCC. Первоначальная реализация была написана в NeXT, но хранилась в отдельной библиотеке, которую пользователи могли подключить к их версии GCC. Не распространяя её вместе с GCC, NeXT надеялась обойти GPL. Этот трюк не сработал, и, в конечном счете, NeXT пришлось открыть код. Как бы то ни было, открытый код рантайм библиотеки не требовался, поэтому компилятор не работал на любой другой платформе. Ричард Столлман (Richard Stallman) написал новую версию, которая послужила заменой рантайму от NeXT. Она тоже, в свою очередь, была впоследствии заменена более новыми версиями, развитие которых пошло по разным путям.

Текущий код для Objective-C в GCC зависит от используемой версии. NeXT сохранила свою версию, которая представляет собой редко обновляемую кашу без иерархии, все находится в одном исходном файле и поддерживает только рантайм от NeXT. Основная ветка GCC включает совсем неподдерживаемый хлам, основанный на коде NeXT, но заполненный #ifdef-ами для рантайма GNU.

Как только GCC стал поддерживать Objective-C, начались попытки дублирования оболочки NeXT. Проект GNUstep в конечном счете выделился из них, реализуя большую часть OpenStep и часть дополнений Cocoa. Он также включает в себя замену построителя интерфейса от NeXT — GNU Opject Relational Modeler (GORM).

Немного синтаксиса

Синтаксические дополнения в Objective-C были спроектированы таким образом, чтобы выделить различную семантику объектно-ориентированного кода. В C++ вы можете сделать так:
doSomething();

Это может быть вызовом функции C, вызовом статической функции-члена текущего объекта в C++, переданной в качестве скрытого параметра или вызовом виртуальной функции, которая может быть реализована в дочернем классе, с текущим объектом в качестве скрытого параметра. С другой стороны, вы можете сделать так:

a.doSomething();

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

В противоположность этому, вещи, которые выглядят как C, в Objective-C всегда являются C. Аналог вызова метода в Objective-C пришел из синтаксиса Smalltalk:

[a doSomething];

Квадратные скобки используются для выделения синтаксиса отправки сообщений, похожего на Smalltalk. Я вернусь к различию между отправкой сообщений и вызовом функций немного позже. Когда у вас есть метод, принимающий некоторые аргументы, у всех аргументов в Objective-C есть имена:
[aDictionary setObject:@"a string" forKey:@"a key"];

Это может выглядеть странным для тех, кто более близок к синтаксису C, но это позволяет тратить гораздо меньше времени, размышляя над порядком параметров. Это также делает код более удобным для чтения, если вы перед этим не смотрели класс, экземпляром которого является aDictionary, — вы знаете, что этот метод устанавливает объект и ключ, и вы знаете, что есть что.

После обработки, это превращается в нечто вроде следующего:

SEL sel = sel_get_uid("setObject:forKey:");

IMP method = objc_msg_lookup(aDictionary->isa, sel);

method(aDictionary, sel, @"a string", @"a key");

Я остановлюсь позже на том, что такое sel. Не смотря на то, что это работает, не совсем понятно, что произойдет — значение sel будет установлено инициализирующей процедурой рантайм библиотеки, когда модуль будет загружен. Семейство рантаймов NeXT/Apple объединяет два последних шага в следующее:
objc_msgSend(aDictionary, sel, @"a string", @"a key");

Это работает немного быстрее, но делает оптимизацию, вроде последующего добавления кода в будущем гораздо сложнее. Так как это все функции C, в них нет ничего особенного — вы можете вызывать их из своего собственного кода, если захотите.

Объектная модель

Термин «объектно-ориентированный» был введен Аланом Кеем (Alan Kay) в 1960-х. Он продолжил разработку языка Smalltalk вместе с командой в Xerox PARC, как языка объединяющего идеи этой модели программирования.

Основной идеей объектно-ориентированного программирования явились простые модели компьютеров, которые связаны друг с другом посредством обмена сообщений. Это очень отличается от подхода в C++, где присутствует нечто мало похожее на объектную ориентированность, а основной идеей является ассоциирование функций со структурами данных.

В C++ объекты — это структуры с набором связанных функций, которые являются либо статическими, либо виртуальными. Статическая функция — семантический эквивалент функции в C со скрытым первым аргументом, содержащим объект. Это крайне неэффективное расширение над C, так как следующие вещи эквивалентны:

// C++

Object->function();

// C

function(Object);

Версия на C++ длиннее, но не дает каких-либо семантических отличий.

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

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

[object message];

Этот код отправляет сообщение с именем message объекту. Какой код будет вызван в качестве результата этого действия, полностью зависит от класса объекта. Вот одно очень важное отличие между этим и аналогом в C++: в C++ этот механизм до сих пор немного зависит от того, объектом какого класса вы считаете данный объект.

Предположим, что у вас есть иерархия из четырех классов. A — базовый класс, B и C — дочерние классы A, а D — дочерний класс B. Если каждый из них, кроме класса A, реализует виртуальную функцию doSomething() в C++, вы можете вызвать её, только используя шаблон. Рассмотрим следующую строку:

Object.doSomething();

Если вы предполагаете, что Object — это объект класса B или D, и он действительно является объектом D, будет вызвана реализация, определенная в D. Если вы предполагаете, что это объект класса C, он вызывает реализацию из C. Если вы полагаете, что это объект класса A, вам необходимо будет попробовать провести два явных динамических приведения и посмотреть, какой сработает, или использовать шаблон.

Если у вас есть такой же порядок классов в Objective-C, с классами B, C и D, реализующих метод doSomething, вы можете попробовать следующее:

[object doSomething];

Если вы предполагаете, что данный объект является типом B, а на самом деле он типа C, метод doSomething все равно будет вызван. Если вы думаете, что это экземпляр класса A, при компиляции вы получите предупреждение о том, что экземпляры класса A не отвечают на сообщение doSomething. Если это действительно экземпляр классов B, C или D, то это сработает в рантайме, а если же он действительно экземпляр класса A, вы получите исключение в рантайме.

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

Классы не являются особенными

В Smalltalk классы — это просто объекты с некоторыми специальными возможностями. То же самое справедливо и в Objective-C. Класс — это объект. Он отвечает на сообщения так же, как и объект. И Objective-C, и C++ разделяют выделение памяти для объекта и его инициализацию:

  • В C++ выделение памяти для объекта делается с помощью оператора new. В Objective-C это делается отправлением классу сообщения alloc, который, в свою очередь, вызывает malloc() или аналог.
  • Инициализация в C++ происходит с помощью вызова функции с именем, аналогичным имени класса. Objective-C не проводит различий между методами инициализации и другими методами, но по соглашению метод инициализации по умолчанию называется init.

Когда вы объявляете метод, на который отвечает объект, объявление начинается со знака «-», а «+» используется для методов класса. Обычно эти префиксы используются для сообщений в документации, так что вы должны писать +alloc и -init, чтобы указать, что alloc посылает сообщение классу, а init — экземпляру.

Классы в Objective-C, как и в других объектно-ориентированных языках, это «фабрики» объектов. Большинство классов сами не реализуют метод +alloc; вместо этого они наследуют его от супер-класса. В NSObject, базовом классе в большинстве программ на Objective-C, метод +alloc вызывает +allocWithZone. Он принимает NSZone в качестве аргумента, — C-структуру, содержащую некоторую линию поведения для выделения памяти под объекты. Возвращаясь в 1980-е, когда Objective-C использовался в NeXTSTEP для реализации драйверов устройств и большинства GUI на компьютерах с 8 МБ ОЗУ и ЦПУ с тактовой частотой 25 МГц, NSZone была очень важна для оптимизации. На данный момент, почти все программисты на Objective-C её игнорируют (у неё столько же шансов стать более полезной, как у архитектуры NUMA — более используемой).

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

Распространенный трюк реализации этой возможности называется «isa-swizzling». Как я уже сказал, объекты в Objective-C являются структурами C, где первый элемент — указатель на класс. К этому элементу можно получить доступ точно так же, как и к другим переменным экземпляра; вы можете изменить класс объекта в рантайме, просто присвоив новое значение. Конечно же, если вы установите класс объекта чему-то, что имеет другую структуру в памяти, это закончится ужасно. Тем не менее, у вас может быть суперкласс, которые определяет интерфейс, а затем набор подклассов, которые определяют поведение. Например, эта техника используется в стандартном строковом классе (NSString), в котором есть различные экземпляры для различных кодировок, для статических строк и т.д.

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

Типы и указатели

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

Три наиболее часто используемых новых типов в Objective-C — это id, Class и SEL. id — это указатель на объект Objective-C. Это эквивалентно void* в C, к которому вы можете привести любой тип указателя на объект, а также привести его к любому другому типу указателя на объект. Вы можете попробовать отправить какое-нибудь сообщение к id, но вы получите исключение в рантайме, если такая возможность не поддерживается.

Class — это указатель на класс Objective-C. Классы являются объектами, поэтому они также могут получать сообщения. Имя класса является типом, а не переменной. Идентификатор NSObject это тип экземпляра NSObject, но он также может быть использован в качестве получателя сообщений. У вас может быть такой класс:

[NSObject class];

Этот код посылает сообщения +class классу NSObject, который возвращает указатель к структуре Class, представляющую собой сам класс. Это полезно для интроспекции, как мы увидим далее.

Третий тип, SEL, представляет собой селектор — абстрактное представление имени метода. Вы можете создать его во время компиляции с помощью директивы @selector() или в рантайме путем вызова функции рантайм библиотеки со строкой C или используя функцию OpenStep NSSelectorFromString(), которая дает селектор для строки Objective-C. Эта техника позволяет вам вызывать методы по имени. Вы можете сделать это в C, используя что-то вроде dlsym(), но это гораздо сложнее в C++. В Objective-C вы можете сделать такое:

[object performSelector:@selector(doSomething)];

Что будет эквивалентно следующему:

[object doSomething];

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

В C++ нет эквивалента типу id, так как объекты всегда должны быть типизированы. В Objective-C у вас есть дополнительная система типов. Оба следующих варианта будут корректны:

id object = @"a string";

NSString *string = @"a string";

На самом деле, константная строка — это экземпляр класс NSConstantString, который является дочерним классом NSString. Присваивание его к NSString* включает проверку типов во время компиляции для сообщений и доступ к публичным переменным экземпляра (которые почти никогда не используются в Objective-C). Вы можете нарушить эту установку, сделав следующее:
NSArray *array = (NSArray*)string;

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

В то время, как делать такое может показаться странным (а это так и есть, так что не делайте так), это подчеркивает очень важное отличие между Objective-C и C++. У Objective-C определенная семантика типов, в то время как C++ имеет переменную семантику типов. В C++ тип зависит от типа переменной. Когда вы присваиваете указателю на объект в C++ переменную, определенную как указатель на суперкласс, два указателя могут не иметь одинаковое численное значение (это сделано для того, чтобы позволить множественное наследование, которое не поддерживает Objective-C).

Определение классов

Определение классов в Objective-C есть секции интерфейса и реализации. В C++ есть нечто похожее, но там это в какой-то степени смешано. Интерфейс в Objective-C только определяет части, которые точно должны быть общедоступны. В целях реализации, он включает в себя приватные переменные экземпляра, так как вы не можете сделать класс суперклассом, пока не знаете, насколько он велик. Более поздние реализации, как, например, 64-битный рантайм от Apple, не имеет этого ограничения.

Интерфейс объектов в Objective-C выглядит следующим образом:

@interface AnObject : NSObject  {

@private

    int integerivar

@public

    id anotherObject;

}

+ (id) aClassMethod;

- (id) anInstanceMethod:(NSString*)aString with:(id)anObject

@end,>

Первая строка содержит три части: идентификатор AnObject — имя нового класса. Имя после двоеточия — это объект NSObject (это необязательно, но почти каждый объект в Objective-C расширяет NSObject). Имена в угловых скобках это протоколы, — это схоже с интерфейсами в Java — которые реализуются этим классом.

Как и в C++, переменные экземпляра («поля» в C++) могут иметь спецификаторы доступа. В отличии от C++, перед этими спецификаторами ставится знак «@», чтобы избежать совпадений с корректными идентификаторами C.

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

struct _AnObject

{

    @defs(AnObject);

};

Директива @defs означает: «Поместить все поля указанного объекта в эту структуру», поэтому структура _AnObject имеет точно такой же формат в памяти, как и экземпляр класса AnObject. Вы можете использовать это правило, например, для доступа к переменным экземпляра напрямую. Часто это используют, чтобы позволить функции C управлять объектом Objective-C напрямую, в целях повышения производительности.

Также с помощью этой возможности, вы можете создавать объекты в стеке, на что я и намекал ранее. Так как у структуры и объекта одинаковый формат размещения в памяти, вы просто создаете структуру, устанавливаете её указатель на корректный класс, а затем приводите указатель к указателю на объект. Затем вы можете использовать его как объект, хотя вы должны быть очень осторожны: ничто не должно хранить указатели к нему за пределами области видимости структуры (я никогда не мог найти реальное применение этому трюку; это исключительно академическое использование).

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

Интерфейс схож с ранним объявлением в C. Ему так же нужна реализация, которая использует @implementation, что не удивительно:

@implementation AnObject

+ (id) aClassMethod

{

 ...

}

- (id) anInstanceMethod:(NSString*)aString with:(id)anObject

{

 ...

}

@end

Обратите внимание, как указаны типы параметров, в скобках. Это повторное использование синтаксиса приведения в C, чтобы показать, что значения приводятся к этому типу. На самом деле, они могут и не быть такого типа. Точно такие же правила применимы здесь по время приведения. Это обозначает, что приведение между несовместимыми типами указателей вызывают предупреждение (а не ошибку).

Управление памятью

Традиционно Objective-C не предоставляет каких-либо возможностей по управлению памятью. В ранних версиях корневой класс Object реализовывал метод +new, который вызывал malloc() для создания нового объекта. Когда вы заканчивали работу с объектом, вы посылали сообщение -free. OpenStep добавил подсчет ссылок. Каждый объект, наследуемый от NSObject, отвечает на сообщения -retain и -release. Когда вы хотите хранить указатель на объект, вы отправляете сообщение -retain. Когда вы заканчиваете работу, отправляете сообщение -release.

Эта модель имеет одну небольшую проблему. Часто вы не хотите хранить указатель на объект, но и не хотите пока что его освобождать. Типичный пример — возвращение к объекту. Вызывающему объекту может потребоваться хранить указатель на объект, а вам нет.

Решением этой проблемы стал класс NSAutoreleasePool. В дополнение к -retain и -release, NSObject также отвечает на сообщение -autorelease. Когда вы отправляете какое-либо из них, оно регистрирует себя с текущим пулом автоматического высвобождения. Когда объект пула уничтожен, отсылается сообщение -release к каждому объекту, который ранее получил сообщение -autorelease. В приложениях OpenStep экземпляр NSAutoreleasePool создается в начале каждого цикла и уничтожается в конце. Также вы можете создавать собственные экземпляры, чтобы освободить автоматически высвобождаемые объекты быстрее.

Этот механизм ликвидирует много копирования, в котором нуждается C++. Также следует отметить здесь то, что в Objective-C переменчивость — атрибут объекта, а не ссылки. В C++ у вас константные указатели и не константные указатели. Вам не позволяется применять неконстантные методы к константному объекту. Это не гарантирует то, что объект не изменится, — просто вы его не измените.

В Objective-C общий шаблон определяет неизменяемого класса, а затем изменяемого подкласса. NSString является типичным примером, — у него есть изменяемый подкласс NSMutableString. Если вы получаете NSString и хотите сохранить его, вы можете послать сообщение -retain и сохранить указатель без операции копирования. В качестве альтернативы вы можете послать NSString сообщение +stringWithString:. Он проверит, изменяемый ли аргумент и если да, вернет оригинальный указатель.

Objective-C 2.0 как с рантайм Apple, так и GNU, поддерживает не перемещающий сборщик мусора, который избавляет от необходимости пользоваться сообщениями -retain и -release. Это дополнение к языку это не всегда хорошо поддерживается существующими фреймворками и должно использоваться с осторожностью.

Интроспекция

В C++ есть некоторая поддержка интроспекции с помощью Runtime Type Information (RTTI). Обычно её плохо поддерживают компиляторы, а там, где такая поддержка есть, она редко используется «в целях производительности». В противоположность этому, интроспекция всегда доступна в Objective-C.

Обычно структура класса содержит один связанный список или массив метаданных о методах и другой — для переменных экземпляра. Для каждой переменной экземпляра он включает в себя смещение с начала объекта, тип и имя. Вы можете использовать это для создания довольно интересных вещей. Например, я написал фреймворк для ?toil?, который проверяет эту информацию и использует её для автоматического размножения объектов (в C++ это возможно только для тех объектов, исходный код для которых у вас есть). Для методов там есть имя, тип и указатель на функцию, реализующую метод. Это называется Instance Method Pointer (IMP) и определено в заголовочном файле следующим образом:

typedef id (*IMP)(id, SEL, ...);

Он использует два других типа Objective-C. Тип id — указатель на объект некоторого вида. Все, что вы знаете про id это то, что он будет отвечать на сообщения (не смотря на то, что вы не знаете, какие сообщения, пока не спросите). Другой — SEL — тип селектора.

Вы можете сделать следующее:

IMP method = [object methodForSelector:@selector(doSomething)];

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

Так как вы можете строить селекторы в рантайме, вы сможете использовать этот подход для некоторого очень динамичного окружения. В парсере XML в ?toil? я использовал это когда добавлял дочерние элементы в объект, представляющий собой элемент XML. Корневой класс реализует метод -addChild:forKey:, содержащий следующий код:

NSString * childSelectorName =\

     [NSString stringWithFormat:@"add%@:", aKey];

SEL childSelector = NSSelectorFromString(childSelectorName);

if([self respondsToSelector:childSelector])

{

  [self performSelector:childSelector withObject:aChild];

}

Он создает селектор из ключевого имени, а затем вызывает его с объектом. Этот подход использован во многих местах в реализации XMPP. Например, тег <name> может быть пропарсен классом, который просто собирает символьные данные и превращает их в строку. Когда он достигает </name>, он делает примерно следующее:
[parent addChild:string forKey:@"name"];

Затем родитель вызывает этот метод, который производит вызов его метода -addname: со строкой в качестве аргумента. Более сложная форма находится в Cocoa в форме KVC, где проводится интроспекция и метода и метаданных переменой экземпляра. Когда вы вызываете -setValue:forKey:, производится вызов установщика, устанавливающего переменную экземпляра напрямую или вызов -setValue:forUndefinedKey:. Использование KVC медленнее, чем установка переменных экземпляра напрямую или даже прямой вызов методов set/get, но он полностью изолирует реализацию от интерфейса.

В Objective-C 2.0 Apple предоставила свойства, доступ к которым происходит следующим образом:

object.property = 12;

Это очень тонкий слой синтаксического сахара. Внутри они переводятся в сообщения set и get, в зависимости от того, какое свойство было использовано. Лучшей возможностью свойств является то, что они позволяют этим методам set и get генерироваться автоматически для любой переменной экземпляра с семантикой удержания в памяти, присваивания или копирования.

Протоколы

В Objective-C протоколами являются наборы сообщений, реализующих класс. Вы можете указать, что указатель должен указывать на класс, реализующий данный интерфейс:
id<AnInterface> object;

NSString<AnInterface> *string;

Первый пример эквивалентен объявлению переменной, как тип интерфейса в Java. В C++ ближайшим эквивалентом является использование абстрактных классов вместо интерфейсов и указание абстрактного класса в качестве типа.

Более интересен второй пример. Строковой переменной дозволено быть любым подклассом NSString, который реализует AnInterface, который позволяет вам ограничивать подмножество подклассов, реализующий отдельный интерфейс. Общий пример:

NSObject<MyDelegateProtocol> *delegate;

Это позволяет ему быть любым подклассом NSObject (и поэтому методы, которые вы ожидаете от любого объекта, будут работать) и дополнительно требуется реализовать определенный протокол. А вот альтернативный вариант:
id <NSObject, MyDelegateProtocol> delegate;

Это работает для NSObject, так как NSObject является и классом, и протоколом, с классом, перенимающим протокол. Это было сделано так, чтобы NSProxy и другие корневые классы могли быть использованы поочередно с подклассами NSObject.

Так как Objective-C произошел от Smalltalk с принципом «всё является объектом», неудивительно, что протоколы, как и классы, тоже объекты. Вы можете посылать им сообщения для проведения интроспекции. Обычно вы не делаете это сами, вместо чего полагаясь на сообщения, посланные к подклассам NSObject:

if ([object conformsToProtocol:@protocol(MyDelegateProtocol)])

{

  // Действия с делегированным протоколом

}

Вещью, которая немного смущает в протоколах, является тот факт, что они проверяются на равенство простым сравнением имен. Если у вас есть два протокола с одинаковыми именами, то у вас нет способа указать в рантайме, который из них реализует объект. Причиной этому послужило разрешение проверки для согласования с помощью директивы @protocol(), как продемонстрировано выше, в исходных файлах, у которых нет доступа к описанию протокола; но это может вызвать неразбериху.

Расширенные классы

Одной из вещей, прямых аналогов которой нет в C++, является идея категории — коллекции методов, которые добавляются к существующему классу. Будучи загруженной, категория не имеет отличий от других методов. Категории, как и классы, объявляются с интерфейсом и реализацией.

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

@interface AnObject (Private)

- (void) privateMethod;

@end

Если вы поместите это в начало файла реализации, вы не получите предупреждения во время компиляции, когда отправите сообщение -privateMethod экземплярам AnObject. Имя в скобках (Private) — просто имя категории. Оно может быть любым. Обратите внимание на то, что без раздела @inmplementation, это просто станет ранним объявлением C функции. Если нет соответствующей реализации в C, вы получите ошибку линковщика. Если нет соответствующей реализации метода в Objective-C, вы получите исключение в рантайме.

Вы можете предоставить дополнительную реализацию метода, используя категорию точно таким же путем, как и когда вы предоставляли «нормальные» методы:
@implementation AnObject (NewMethods)

- (void) newMethod

{

  ...

}

@end

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

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

Неформальные протоколы

Очень частым шаблоном в Objective-C является идея неформального протокола, — коллекции методов, которые класс может реализовывать, а может не реализовывать. Её частое использование — для делегирования объектов. В Java очень часто для делегатов ожидается реализация интерфейса. У такого подхода часто есть набор методов, некоторые из которых реализованы как методы без тела.

В Objective-C есть два пути для определения неформальных протоколов. Первый — определение категории на базовом классе (обычно на NSObject), которая предоставляет нулевые реализации каждого метода. Это означает, что каждый класс будет отвечать на сообщения в интерфейсе, но что-то действительно делать будут только реализованные методы. Вы должны поместить в файл исходного кода, который использует неформальный протокол, нечто следующее:

@implementation NSObject (MyInformalProtocol)

- (void) handleSomethingFrom:(id) sender {}

@end

Затем вы просто отправляете сообщение handleSomethingFrom: делегированному объекту. Обратите внимание, что вам не нужен раздел @interface для создания разделения между интерфейсом и реализацией для категорий; интерфейс приватен, а вам не нужно, чтобы что-то ещё вызывало этот метод, кроме вашего класса, поэтому не открывайте интерфейс. Хотя этот метод прост, он не идеален во многих случаях, так как приводит к заполнению координирующей таблицы корневого объекта в основном не используемым хламом (который может привести к понижению производительности в рантайм Apple из-за того, как там реализовано кеширование).

Другой вариант — производить тестирование в рантайме. Если вы пошлете объекту сообщение respondsToSelector:, вы можете выяснить, реализует ли он названный метод. Для делегатов, вы можете затем кешировать IMP для метода, и вызвать его напрямую позже. В вашем методе setDelegate: будет нечто такое:

handleMethod = NULL;

if([delegate respondsToSelector:@selector(handleSomethingFrom:))

{

  handleMethod = [delegate methodForSelector:\

	 @selector(handleSomethingFrom:)];

}

Потом при использовании, вы делаете следующее:

// Эквивалентно [delegate handleSomethingFrom:self];

if (NULL != handleMethod

{

  handleMethod(delegate, @selector(handleSomethingFrom:), self);

}

Objective-C 2.0 дает третий вариант, который предназначен для использования директивы @optional в объявлении протокола. Это большая трата памяти, так как вам необходимо производить тесты в рантайме для определения, реализует ли объект, сопоставленный протоколу, опциональный метод из этого протокола.

Повторная отправка

В C++ вы не посылаете сообщения, а вызываете функции-члены. Это важное отличие, так как оно обозначает, что вызываемые методы семантически схожи с вызовом функций. Objective-C вносит другой слой абстракции. Если вы посылаете сообщение объекту Objective-C, который его не понимает, возникнет исключение. Тем не менее, это не делается самим языком.

Библиотека рантайма имеет механизм устранения неисправностей, когда нет метода для селектора. Она вызывает метод, который интроспектирует получателя для некоторой информации о типе, помещает вызов в объект NSInvocation, а затем передает его методу -forwardInvocation: данного объекта.

Объект NSInvocation инкапсулирует получателя, селектор и аргументы. Вы можете использовать эту идею для отправки сообщений высокого порядка. Рассмотрим следующий пример:

[[anArray map] toUppercase];

Метод -map, примененный к массиву, возвращает объект прокси с помощью метода forwardInvocation:, реализованного подобным образом:
- (void) forwardInvocation:(NSInvocation*)anInvocation

{

 SEL selector = [anInvocation selector];

 NSMutableArray * mappedArray = [NSMutableArray array];

 FOREACHI(array, object)

 {

  if([object respondsToSelector:selector])

  {

   [anInvocation invokeWithTarget:object];

   id mapped;

   [anInvocation getReturnValue:&mapped];

   [mappedArray addObject:mapped];

  }

 }

 [anInvocation setReturnValue:mappedArray];

}

FOREACHI — макрос из ?toil?, который просто производит кеширование IMP на NSEnumerator. Когда вы посылаете сообщение -toUppercase к мэп прокси, он производит итерацию по каждому объекту массива; проверяет, отвечает ли он селектору и, если да, вызывает метод с аргументами. Возвращаемое значение добавляется к новому массиву.

Это почти невозможно сделать в C++. Вы можете использовать шаблон команды, чтобы сделать нечто похожее, но только реализовав сначала ваш собственный механизм диспетчеризации.
 

Заключение


Objective-C очень небольшой язык и мы рассмотрели практически все в этой серии статей, включая некоторые продвинутые возможности. Тем не менее, как и в Smalltalk, он несколько вводит в заблуждение. В то время как ядро языка очень просто, большинство из окружения программ на Objective-C приходит из библиотеки. Обычно это реализация спецификации OpenStep, такие как GNUstep или Cocoa от Apple, а знакомство с библиотекой может занять много времени.