Статьи

Объективно-С кратко: протоколы

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

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


Во-первых, давайте посмотрим, как объявить формальный протокол. Создайте новый файл в Xcode и выберите значок протокола Objective C под Mac OS X> Какао :

Рисунок 29 Значок Xcode для файлов протокола

Значок Xcode для файлов протокола

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

Рисунок 30 Наименование протокола

Наименование протокола

Нажмите Далее и выберите местоположение по умолчанию для файла. Это создаст пустой протокол, который выглядит почти как интерфейс:

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 :

Рис. 31. Предупреждение о неполной реализации для Person CoordinateSupport

Предупреждение о неполной реализации для Person <CoordinateSupport>

Аналогично, категория может принять протокол, добавив его после категории. Например, чтобы указать классу Person принять протокол CoordinateSupport в категории « Relations », вы должны использовать следующую строку:

1
@interface Person(Relations) <CoordinateSupport>

И, если вашему классу нужно использовать более одного протокола, вы можете разделить их запятыми:

1
@interface Person : NSObject <CoordinateSupport, SomeOtherProtocol>

Без протоколов у нас было бы два варианта, чтобы и Ship и Person реализовали этот общий API:

  1. Повторно объявите одинаковые свойства и методы в обоих интерфейсах.
  2. Определите API в абстрактном суперклассе и определите Ship и Person как подклассы.

Ни один из этих вариантов не является особенно привлекательным: первый является избыточным и подвержен человеческим ошибкам, а второй строго ограничивает, особенно если они уже наследуются от разных родительских классов. Должно быть ясно, что протоколы гораздо более гибки и могут использоваться повторно, поскольку они защищают API от зависимости от какого-либо конкретного класса.

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

Рисунок 32 Связывание несвязанных классов с использованием протокола

Связывание несвязанных классов с использованием протокола

Из-за гибкости протоколов, различные платформы 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 для правильной компиляции, и наоборот. Это проблема типа «курица или яйцо», и компилятору это не очень понравится:

Рисунок 33 Ошибка компилятора, вызванная циклическими ссылками на протокол

Ошибка компилятора, вызванная циклическими ссылками на протокол

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

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 .