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

Например, объекты классов Вее и Bird должны иметь некоторые общие свойства в силу того, что пчела и птица могут летать.

В то же время класс Вее может наследовать от класса Insect, хотя и не всякое насекомое способно летать. Так как же объекту типа Вее приобрести аспекты объекта типа Flier совершенно независимо от того, как их приобретает объект типа Bird? В некоторых объект-но-ориентированных языках программирования этот вопрос разрешается с помощью подмешанных классов. Например, в языке Ruby можно определить модуль Flier, наполнить его определениями методов и затем внедрить в оба класса, Вее и Bird. В языке Objective-C принят более простой, облегченный подход, называемый протоколом. В среде Cocoa протоколы нашли широкое применение.

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

Фактически реализация методов, объявленных в протоколе, возлагается на соответствующий ему класс. Протокольный метод может быть обязательным или необязательным. Если протокольный метод обязательный, а класс соответствует данному протоколу, то компилятор выдаст предупреждение, если класс не выполнит своих обязательств реализовать этот метод. С другой стороны, реализовывать необязательные методы совсем не обязательно. (Разумеется, это справедливо лишь с точки зрения компилятора. Так, если во время выполнения сообщение посылается объекту без реализации соответствующего метода, то в конечном итоге возникает аварийная ситуация, как пояснялось в главе 3.)

Рассмотрим на конкретном примере, каким образом протоколы применяются в среде Cocoa. Одни объекты можно копировать, а другие — нельзя, но это не имеет никакого отношения к наследованию классов этих объектов. Тем не менее хотелось бы иметь единообразный метод, на который мог бы реагировать любой копируемый объект. С этой целью в среде Cocoa определяется протокол NSCopying, в котором объявляется единственный (обязательный) метод copyWithZone:. Класс, явно соответствующий протоколу NSCopying, обязуется реализовать метод copyWithZone:. Ниже показано, каким образом протокол NSCopying определяется в файле NSObject. h, где его можно обнаружить в прикладном коде.

@protocol NSCopying

- (id)copyWithZone:(NSZone *)zone;

@end

Это все, что требуется для определения протокола. В этом определении используется директива компилятора @protocol, в которой устанавливается название протокола. Оно состоит полностью из определений методов и завершается директивой компилятора Send.

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

Объявления необязательных методов в определении протокола должны следовать после директивы @optional. В определении протокола может быть установлено, что в нем внедряются другие протоколы. Такие протоколы указываются списком через запятую и в угловых скобках после имени основного протокола, как показано в приведенном ниже примере кода из файла UIAlertView. h, принадлежащего компании Apple.

Sprotocol UIAlertViewDelegate <NSObject>

@optional

- (void)alertView:(UIAlertView *)alertview

clickedButtonAtlndex:(NSInteger)buttonlndex;

П ... объявления других необязательных методов ...

Send

Определение протокола NSCopying в файле NSOb j ect. h является всего лишь определением. Оно не устанавливает соответствие класса NSOb j ect протоколу NSCopying. На самом деле класс NSOb j ect не соответствует протоколу NSCopying! Для того чтобы убедиться в этом, попробуйте отправить метод copyWithZone: в качестве сообщения своему подклассу, производному от класса NSOb j ect, как показано ниже.

MyClass* шс = [MyClass new];

MyClass* mc2 = [me copyWithZone: nil];

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

Теперь посмотрим, что произойдет, если установить формальное соответствие класса протоколу NSCopying. С этой целью попробуйте внести следующие изменения в первую строку из раздела интерфейса заголовочного файла своего класса:

 

@interface MyClass : NSObject <NSCopying>

 

Теперь ваш код будет скомпилирован, но компилятор предупредит вас, что в классе отсутствует реализация метода copyWithZone:, что он обязывался сделать, поскольку метод copyWithZone : является обязательным для протокола NSCopying.

Имя протокола можно также использовать при указании типа объекта. Чаще всего объект обозначается как id, но при сопутствующем условии, что он соответствует протоколу, имя которого указывается в угловых скобках. Для того чтобы продемонстрировать это положение, рассмотрим еще один типичный пример применения протоколов в среде Cocoa, связанный непосредственно с табличным представлением (UITableView). У класса UITableView имеется свойство dataSource, объявленное следующим образом:

 

Sproperty (nonatomic, assign) id<UITableViewDataSource> dataSource

 

Это свойство представляет переменную экземпляра типа id <UITableViewDataSource>. Это означает, что источник данных должен соответствовать протоколу UITableView DataSource, какому бы классу он ни принадлежал. Такое соответствие накладывает обязательство на источник данных реализовать хотя бы обязательные методы экземпляра tab leView:numberOfRowsInSection: и tableView:cellForRowAtIndexPath:, которые будут вызываться из табличного представления, когда ему нужно будет знать, какие именно данные следует отображать.

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

MyClass* me = [MyClass new];

UITableView* tv = [UITableView new];

tv.dataSource = me; // компилятор выдаст предупреждение

При действующем механизме ARC это предупреждение выражено в довольно запутанных терминах следующим образом: “Assigning to ‘id<UITableViewDataSource>’ from in compatible

type ‘MyClass * strong’ (Присваивание ‘id<UITableViewDataSource>’ из несовместимого типа

‘MyClass * strong’).

Для того чтобы компилятор не выдавал подобные предупреждения, в объявлении класса MyClass должно быть установлено соответствие протоколу UITableViewDataSource. Как только это будет сделано, объект класса MyClass приобретет идентификатор id <UITableViewDataSource>, а при компиляции третьей строки приведенного выше фрагмента кода предупреждение уже не возникнет. Разумеется, в классе MyClass нужно также предоставить реализации методов tableView:numberOfRowsInSection: и tableView: cellForRowAtlndexPath:, чтобы избежать появления другого предупреждения о том, что в данном классе не реализован обязательный метод протокола, соответствовать которому он обязался.

В довольно большом числе случаев объект, который требуется присвоить и от которого ожидается соответствие протоколу, определяется как self. В подобных случаях такое соответствие класса протоколу можно объявить в файле реализации (с расширением . ш) как часть расширения класса, аналогично приведенному ниже примеру.

// MyClass.ш:

@interface MyClass () <UITableViewDataSource>

@end

(^implementation MyClass

- (void) someMethod {

UITableView* tv = [UITableView new]; tv.dataSource = self; )

Send

Я предпочитаю именно такой порядок, поскольку он означает, что объявление соответствия протоколу находится в том же файле, где применяется этот протокол. Преобладающая в среде Cocoa тенденция применять протоколы связана с объектами делегатов (и разумеется, в первую очередь с реализацией механизма делегирования, который вам, скорее всего, придется определять в своих протоколах). Подробнее о делегатах речь пойдет в главе 11, но и теперь вы можете заметить, что у многих классов имеется свойство delegate и что класс этого свойства зачастую обозначается идентификатором id <SomeProtocol>. Например, в шаблоне проекта Empty Window предоставляется класс AppDelegate, объявленный следующим образом:

 

@interface AppDelegate : UIResponder <UIApplicationDelegate>

 

Дело в том, что назначение класса AppDelegate — выполнять функции общего для приложения делегата. Общим для приложения является объект типа UIApplication, а его свойство delegate обозначается как id <UIApplicationDelegate>. Таким образом, класс AppDelegate объявляет свои функции установлением явного соответствия протоколу UIApplicationDelegate.

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

стороны, раздел с определением протокола не может быть первым потому, что в нем упоминается класс до его определения. Как правило, подобное затруднение разрешается следующим образом: первым следует раздел интерфейса класса, но ему предшествует предваряющее объявление протокола в одной строке, где указывается только имя протокола, который определяется далее, т.е. через три строки кода, как показано ниже.

@protocol MyProtocol;

@interface MyClass : NSObject

@property (nonatomic, weak)id<MyProtocol> delegate;

@end

@protocol MyProtocol - (void) doSomething: (MyClass*) m;

@end

Для вас как программиста применение протоколов в среде Cocoa имеет значение в двух отношениях.

Соблюдение соответствия

Если значение объекта, которое требуется присвоить в качестве аргумента, обозначается как id <SomeProtocol>, вы должны непременно обеспечить соответствие класса этого объекта протоколу SomeProtocol (и реализацию в нем обязательных методов данного протокола).

Пользование документацией

У протокола имеется своя страница в документации, где поясняется, что свойство delegate обозначается как id <UIApplicationDelegate>, но в то же время подразумевается, что если требуется выяснить, какие именно сообщения может получать делегат класса UIApplication, то для этого придется обратиться за справкой к документации на протокол UIApplicationDelegate.

Аналогично, если в документации на класс упоминается, что класс соответствует протоколу, не забудьте изучить документацию на протокол, поскольку она может содержать важные сведения о поведении класса. Для того чтобы выяснить, какие именно сообщения могут отправляться объекту, как упоминалось в главе 8, вам придется проследить вверх цепочку наследования его класса вплоть до суперкласса, а также найти любые протоколы, которым соответствует класс (или суперкласс) этого объекта.


 

 

 

Добавить комментарий