В Objective-C протокол — это группа методов, которые могут быть реализованы любым классом. Протоколы по сути такие же, как интерфейсы в C #, и у них обоих одинаковые цели. Они могут использоваться в качестве псевдоданных, что полезно для проверки того, что объект с динамической типизацией может отвечать на определенный набор сообщений. И, поскольку любой класс может «принять» протокол, их можно использовать для представления общего API между совершенно не связанными классами.
Официальная документация обсуждает как неформальный, так и формальный метод объявления протоколов, но неформальные протоколы на самом деле являются просто уникальным использованием категорий и не дают почти столько же преимуществ, сколько формальные протоколы. Учитывая это, данная глава посвящена исключительно формальным протоколам.
Создание протокола
Во-первых, давайте посмотрим, как объявить формальный протокол. Создайте новый файл в Xcode и выберите значок протокола Objective C под Mac OS X> Какао :
Как обычно, вам будет предложено ввести имя. Наш протокол будет содержать методы для вычисления координат объекта, поэтому назовем его CoordinateSupport :
Нажмите Далее и выберите местоположение по умолчанию для файла. Это создаст пустой протокол, который выглядит почти как интерфейс:
1
2
3
4
5
6
|
// CoordinateSupport.h
#import <Foundation/Foundation.h>
@protocol CoordinateSupport <NSObject>
@end
|
Конечно, вместо директивы @protocol
она использует @protocol
, за которым следует имя протокола. Синтаксис <NSObject>
позволяет нам включить другой протокол в CoordinateSupport
. В этом случае мы говорим, что CoordinateSupport
также включает в себя все методы, объявленные в протоколе NSObject
(не путать с классом NSObject
).
Далее давайте добавим несколько методов и свойств в протокол. Это работает так же, как объявление методов и свойств в интерфейсе:
01
02
03
04
05
06
07
08
09
10
11
12
|
#import <Foundation/Foundation.h>
@protocol CoordinateSupport <NSObject>
@property double x;
@property double y;
@property double z;
— (NSArray *)arrayFromPosition;
— (double)magnitude;
@end
|
Принятие протокола
Любой класс, который принимает этот протокол, гарантированно синтезирует свойства x
, y
и z
и реализует методы arrayFromPosition
и magnitude
. Хотя это не говорит о том, как они будут реализованы, оно дает вам возможность определить общий API для произвольного набора классов.
Например, если мы хотим, чтобы как Ship
и Person
могли отвечать на эти свойства и методы, мы можем сказать им, чтобы они приняли протокол, поместив его в угловые скобки после объявления суперкласса. Также обратите внимание, что, как и при использовании другого класса, вам необходимо импортировать файл протокола перед его использованием:
01
02
03
04
05
06
07
08
09
10
11
12
|
#import <Foundation/Foundation.h>
#import «CoordinateSupport.h»
@interface Person : NSObject <CoordinateSupport>
@property (copy) NSString *name;
@property (strong) NSMutableSet *friends;
— (void)sayHello;
— (void)sayGoodbye;
@end
|
Теперь, в дополнение к свойствам и методам, определенным в этом интерфейсе, класс Person
гарантированно отвечает API, определенному CoordinateSupport
. Xcode будет предупреждать вас о том, что реализация Person
является неполной, пока вы не синтезируете x
, y
и z
и не реализует arrayFromPosition
и magnitude
:
Аналогично, категория может принять протокол, добавив его после категории. Например, чтобы указать классу Person
принять протокол CoordinateSupport
в категории « Relations
», вы должны использовать следующую строку:
1
|
@interface Person(Relations) <CoordinateSupport>
|
И, если вашему классу нужно использовать более одного протокола, вы можете разделить их запятыми:
1
|
@interface Person : NSObject <CoordinateSupport, SomeOtherProtocol>
|
Преимущества протоколов
Без протоколов у нас было бы два варианта, чтобы и Ship
и Person
реализовали этот общий API:
- Повторно объявите одинаковые свойства и методы в обоих интерфейсах.
- Определите API в абстрактном суперклассе и определите
Ship
иPerson
как подклассы.
Ни один из этих вариантов не является особенно привлекательным: первый является избыточным и подвержен человеческим ошибкам, а второй строго ограничивает, особенно если они уже наследуются от разных родительских классов. Должно быть ясно, что протоколы гораздо более гибки и могут использоваться повторно, поскольку они защищают API от зависимости от какого-либо конкретного класса.
Тот факт, что любой класс может легко принять протокол, позволяет определять горизонтальные отношения поверх существующей иерархии классов:
Из-за гибкости протоколов, различные платформы iOS хорошо их используют. Например, элементы управления пользовательского интерфейса часто конфигурируются с использованием шаблона проектирования делегирования, в котором объект делегата отвечает за реакцию на действия пользователя. Вместо того, чтобы инкапсулировать обязанности делегата в абстрактном классе и заставлять делегатов создавать его подкласс, iOS определяет необходимый API для делегата в протоколе. Таким образом, любому объекту невероятно легко выступать в качестве объекта делегата. Мы рассмотрим это более подробно во второй половине этой серии iOS, лаконично .
Протоколы как псевдотипы
Протоколы могут использоваться в качестве псевдо-типов данных. Вместо того чтобы убедиться, что переменная является экземпляром класса, использование протокола в качестве инструмента проверки типа гарантирует, что переменная всегда соответствует произвольному API. Например, следующая переменная person
гарантирует реализацию API CoordinateSupport.
1
|
Person <CoordinateSupport> *person = [[Person alloc] init];
|
Тем не менее, принудительное применение протокола часто более полезно при использовании с типом данных id
. Это позволяет вам принимать определенные методы и свойства, полностью игнорируя класс объекта.
И, конечно, тот же синтаксис может быть использован с параметром метода. Следующий фрагмент добавляет новый метод getDistanceFromObject:
к API, параметр которого требуется для соответствия протоколу CoordinateSupport
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
// CoordinateSupport.h
#import <Foundation/Foundation.h>
@protocol CoordinateSupport <NSObject>
@property double x;
@property double y;
@property double z;
— (NSArray *)arrayFromPosition;
— (double)magnitude;
— (double)getDistanceFromObject:(id <CoordinateSupport>)theObject;
@end
|
Обратите внимание, что вполне возможно использовать протокол в том же файле, в котором он определен.
Динамическая проверка соответствия
В дополнение к статической проверке типов, описанной в предыдущем разделе, вы также можете использовать метод conformsToProtocol:
определенный протоколом NSObject
, для динамической проверки, соответствует ли объект протоколу или нет. Это полезно для предотвращения ошибок при работе с динамическими объектами (объектами, напечатанными как id
).
В следующем примере предполагается, что класс Person
принимает протокол CoordinateSupport
, а класс Ship
— нет. Он использует динамически типизированный объект mysteryObject
для хранения экземпляра Person
, а затем использует conformsToProtocol:
для проверки наличия поддержки координат. Если это так, то можно безопасно использовать свойства x
, y
и z
, а также другие методы, объявленные в протоколе CoordinateSupport
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// main.m
#import <Foundation/Foundation.h>
#import «Person.h»
#import «Ship.h»
int main(int argc, const char * argv[]) {
@autoreleasepool {
id mysteryObject = [[Person alloc] init];
[mysteryObject setX:10.0];
[mysteryObject setY:0.0];
[mysteryObject setZ:7.5];
// Uncomment next line to see the «else» portion of conditional.
//mysteryObject = [[Ship alloc] init];
if ([mysteryObject
conformsToProtocol:@protocol(CoordinateSupport)]) {
NSLog(@»Ok to assume coordinate support.»);
NSLog(@»The object is located at (%0.2f, %0.2f, %0.2f)»,
[mysteryObject x],
[mysteryObject y],
[mysteryObject z]);
} else {
NSLog(@»Error: Not safe to assume coordinate support.»);
NSLog(@»I have no idea where that object is…»);
}
}
return 0;
}
|
Если вы раскомментируете строку, которая переназначает mysteryObject
экземпляру Ship
, метод mysteryObject
conformsToProtocol:
вернет NO
, и вы не сможете безопасно использовать API, определенный в CoordinateSupport
. Если вы не уверены, какой объект будет содержать переменная, этот вид динамической проверки протокола важен для предотвращения сбоя вашей программы при попытке вызвать метод, который не существует.
Также обратите внимание на новую директиву @protocol()
. Это работает так же, как @selector()
, за исключением того, что вместо имени метода он принимает имя протокола. Он возвращает объект Protocol
, который может быть передан в conformsToProtocol:
среди других встроенных методов. Заголовочный файл протокола не нужно импортировать, чтобы @protocol()
работал.
Форвард-декларирующие протоколы
Если вы в конечном итоге будете работать с большим количеством протоколов, вы в конечном итоге столкнетесь с ситуацией, когда два протокола зависят друг от друга. Это циклическое отношение создает проблему для компилятора, так как он не может успешно импортировать один из них без другого. Например, допустим, мы пытались абстрагировать некоторые функции GPS в протокол GPSSupport
, но хотим иметь возможность преобразовывать «нормальные» координаты нашего существующего CoordinateSupport
в координаты, используемые GPSSupport
. Протокол GPSSupport
довольно прост:
1
2
3
4
5
6
7
8
|
#import <Foundation/Foundation.h>
#import «CoordinateSupport.h»
@protocol GPSSupport <NSObject>
— (void)copyCoordinatesFromObject:(id <CoordinateSupport>)theObject;
@end
|
Это не создает никаких проблем, пока мы не будем ссылаться на протокол GPSSupport
из CoordinateSupport.h
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
#import <Foundation/Foundation.h>
#import «GPSSupport.h»
@protocol CoordinateSupport <NSObject>
@property double x;
@property double y;
@property double z;
— (NSArray *)arrayFromPosition;
— (double)magnitude;
— (double)getDistanceFromObject:(id <CoordinateSupport>)theObject;
— (void)copyGPSCoordinatesFromObject:(id <GPSSupport>)theObject;
@end
|
Теперь для файла CoordinateSupport.h
необходим файл GPSSupport.h
для правильной компиляции, и наоборот. Это проблема типа «курица или яйцо», и компилятору это не очень понравится:
Разрешить рекурсивные отношения просто. Все, что вам нужно сделать, это заранее объявить один из протоколов, а не пытаться импортировать его напрямую:
1
2
3
4
5
6
7
8
9
|
#import <Foundation/Foundation.h>
@protocol CoordinateSupport;
@protocol GPSSupport <NSObject>
— (void)copyCoordinatesFromObject:(id <CoordinateSupport>)theObject;
@end
|
Все @protocol CoordinateSupport;
говорит, что CoordinateSupport
действительно протокол, и компилятор может предположить, что он существует, не импортируя его. Обратите внимание на точку с запятой в конце оператора. Это может быть сделано в любом из двух протоколов; Дело в том, чтобы удалить круговую ссылку. Компилятору все равно, как вы это делаете.
Резюме
Протоколы являются невероятно мощной функцией Objective-C. Они позволяют вам фиксировать отношения между произвольными классами, когда невозможно соединить их с общим родительским классом. Мы будем кратко использовать несколько встроенных протоколов в iOS , так как многие из основных функций приложения для iPhone или iPad определены как протоколы.
Следующая глава представляет исключения и ошибки, два очень важных инструмента для управления проблемами, которые неизбежно возникают при написании программ Objective-C.
Этот урок представляет собой главу из Objective-C, лаконично , бесплатную электронную книгу от команды Syncfusion .