Работа с протоколами

В реальном мире люди на официальном бизнесе часто обязаны выполнять строгие процедуры при справлении с определенными ситуациями. Сотрудники правоохранительных органов, например, обязаны “следовать протоколу” при проведении расследований или собирании доказательств.

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

Источник данных мог быть экземпляром любого класса, такого как контроллер представления (подкласс NSViewController на OS X или UIViewController на iOS) или специализированный класс источника данных, возможно, просто наследовавшийся от NSObject. Для табличного представления, чтобы знать, подходит ли объект как источник данных, важно быть в состоянии объявить, что объект реализует необходимые методы.

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

Протоколы определяют обменивающиеся сообщениями договоры

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

Базовый синтаксис для определения протокола похож на это:

@protocol ProtocolName
// list of methods and properties
@end

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

Как пример, рассмотрите пользовательский класс представления, использующийся для отображения круговой диаграммы, как показано на рисунке 5-1.

Рисунок 5-1  пользовательское представление круговой диаграммы

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

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

@protocol XYZPieChartViewDataSource
- (NSUInteger)numberOfSegments;
- (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex;
- (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex;
@end

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

Синтаксис для объявления свойства источника данных для представления был бы похож на это:

@interface XYZPieChartView : UIView
@property (weak) id <XYZPieChartViewDataSource> dataSource;
...
@end

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

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

Протоколы могут иметь дополнительные методы

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

Также возможно указать дополнительные методы в протоколе. Это методы, которые может реализовать класс, только если он должен.

Как пример, Вы могли бы решить, что заголовки на круговой диаграмме должны быть дополнительными. Если объект источника данных не реализует titleForSegmentAtIndex:, никакие заголовки не должны быть показаны в представлении.

Можно отметить методы протокола как дополнительное использование @optional директива, как это:

@protocol XYZPieChartViewDataSource
- (NSUInteger)numberOfSegments;
- (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex;
@optional
- (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex;
@end

В этом случае, только titleForSegmentAtIndex: метод отмечен дополнительный. Предыдущие методы не имеют никакой директивы, так, как предполагается, требуются.

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

@protocol XYZPieChartViewDataSource
- (NSUInteger)numberOfSegments;
- (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex;
@optional
- (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex;
- (BOOL)shouldExplodeSegmentAtIndex:(NSUInteger)segmentIndex;
@required
- (UIColor *)colorForSegmentAtIndex:(NSUInteger)segmentIndex;
@end

Этот пример определяет протокол с помощью трех требуемых методов и двух дополнительных методов.

Проверьте, что Дополнительные Методы Реализованы во Время выполнения

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

Как пример, представление круговой диаграммы могло бы протестировать на метод заголовка сегмента как это:

    NSString *thisSegmentTitle;
    if ([self.dataSource respondsToSelector:@selector(titleForSegmentAtIndex:)]) {
        thisSegmentTitle = [self.dataSource titleForSegmentAtIndex:index];
    }

respondsToSelector: метод использует селектор, относящийся к идентификатору для метода после компиляции. Можно обеспечить корректный идентификатор при помощи @selector() директива и указание имени метода.

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

При попытке вызвать respondsToSelector: метод на id приспосабливание протоколу, поскольку это определяется выше, Вы получите ошибку компилятора, что нет никакого известного метода экземпляра для него. Как только Вы квалифицируете id с протоколом возвращается вся статическая проверка типа; Вы получите ошибку, при попытке вызвать какой-либо метод, не определяющийся в указанном протоколе. Один способ избежать ошибки компилятора состоит в том, чтобы установить пользовательский протокол для принятия NSObject протокол.

Протоколы наследовались из других протоколов

Таким же образом то, что класс Objective C может наследоваться от суперкласса, можно также указать, что один протокол соответствует другому.

Как пример, это - наиболее успешная практика для определения протоколов для приспосабливания NSObject протокол (часть из NSObject поведение разделяется от его интерфейса класса в отдельный протокол; NSObject класс принимает NSObject протокол).

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

Чтобы указать, что один протокол соответствует другому, Вы обеспечиваете имя другого протокола в угловых скобках, как это:

@protocol MyProtocol <NSObject>
...
@end

В этом примере, любой принимающий объект MyProtocol также эффективно принимает все методы, объявленные в NSObject протокол.

Приспосабливание протоколам

Синтаксис, чтобы указать, что класс принимает протокол снова, использует угловые скобки, как это

@interface MyClass : NSObject <MyProtocol>
...
@end

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

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

@interface MyClass : NSObject <MyProtocol, AnotherProtocol, YetAnotherProtocol>
...
@end

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

Какао и касание какао определяют большое количество протоколов

Протоколы используются Сенсорными объектами Какао и Какао для множества различных ситуаций. Например, классы табличного представления (NSTableView для OS X и UITableView для iOS), оба используют объект источника данных предоставить их необходимую информацию. Оба определяют их собственный протокол источника данных, использующийся почти таким же способом в качестве XYZPieChartViewDataSource пример протокола выше. Оба класса табличного представления также позволяют Вам устанавливать объект делегата, который снова должен соответствовать соответствующему NSTableViewDelegate или UITableViewDelegate протокол. Делегат ответственен за контакт со взаимодействием с пользователем или настройкой дисплея определенных записей.

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

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

Несколько функций уровня языка Objective C также полагаются на протоколы. Для использования быстрого перечисления, например, набор должен принять NSFastEnumeration протокол, как описано в Быстром Перечислении Упрощает Перечислять Набор. Кроме того, некоторые объекты могут быть скопированы, такой как тогда, когда с помощью свойства с a copy атрибут, как описано в Свойствах Копии Поддерживает Их Собственные Копии. Любой объект, который Вы пытаетесь скопировать, должен принять NSCopying протокол, иначе Вы получите исключение на этапе выполнения.

Протоколы используются для анонимности

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

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

    id utility = [frameworkObject anonymousUtility];

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

    id <XYZFrameworkUtility> utility = [frameworkObject anonymousUtility];

Если Вы запишете приложение для iOS, использующее Базовую платформу Данных, например, то Вы, вероятно, столкнетесь NSFetchedResultsController класс. Этот класс разработан, чтобы помочь объекту источника данных снабдить сохраненными данными к iOS UITableView, упрощение предоставить информацию как число строк.

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

    NSInteger sectionNumber = ...
    id <NSFetchedResultsSectionInfo> sectionInfo =
            [self.fetchedResultsController.sections objectAtIndex:sectionNumber];
    NSInteger numberOfRowsInSection = [sectionInfo numberOfObjects];

Даже при том, что Вы не знаете класса sectionInfo объект, NSFetchedResultsSectionInfo протокол диктует, что может ответить на numberOfObjects сообщение.