Objective-C 协议 (Protocol)

1,793 阅读8分钟

在现实生活中, 人们在交易过程中可能产生各种交易"协议", 在具体交易时,交易者需要"遵循"交易"协议".

在面向对象语言中, 在某些情况下,让一个对象"遵循"一套给定的行为是很重要的. 比如, TableView对象会期望和数据源(DataSource)对象进行交互, 数据源需要"遵循"TableView需要的数据给TableView提供数据.

Objective-C 允许我们定义协议(Protocols), 在协议中声明特定情况下我们需要的方法. 这篇文章主要讲述定义一个正式(Formal)协议的语法, 讲解如何让一个类接口遵循一个协议.

使用协议

类接口中声明了和类相关的方法和属性, 而我们可以通过协议不依赖于任何特定的类来声明方法和属性.

定义一个协议的基本语法如下:

@protocol ProtocolName
// 属性和方法列表
@end

协议中可以包含类方法声明, 实例方法声明以及属性.

举例说明, 如果我们需要实现一个饼状图:

Image

为了让这个视图尽量可以复用, 图中所有的信息应当留给另外一个数据源对象去管理. 这样的话, 同一个对象实例就可以通过不同的数据源来展示不同的信息.

饼状图视图需要的最少信息应当包含: 分区数量, 每个分区的相对大小, 每个分区的标题. 饼状图的数据源协议:

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

这个饼状图类接口需要一个属性来管理数据源对象. 这个对象可以是任何类, 所以属性类型为id. 我们可以唯一确定的事情就是这个对象要遵循XYZPieChartViewDataSource协议:

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

Objective-C 使用尖括号< > 来表示遵循协议. 这个例子声明了一个指向遵循了XYZPieChartViewDataSource协议的泛型对象的weak属性.

Note: 代理和数据源属性经常使用weak关键字修饰, 以防止循环引用. 此部分在本系列Objective-C 属性的使用中提到过.

在指定属性遵循的协议后, 如果我们尝试为此属性设置一个并没有遵循该协议的对象时, 编译器会发出警告, 即使这个属性的类型为id. 这个对象是否是UIViewController的实例, 或者NSObject的实例, 这些都无关紧要, 重要的是这个对象需要遵循指定的协议.

声明可选方法

在协议中声明的方法默认都是必须实现的方法(required), 也可以在协议中指定可选方法(optional).

比如我们可以指定饼状图中的标题为可选. 如果数据源没有实现titleForSegmentAtIndex:方法, 饼状图将不显示标题. 我们可以通过@optional修饰符标记协议方法为可选:

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

在这个例子中, 只有titleForSegmentAtIndex:方法被标记为可选. 之前的方法没有被修饰符修饰, 默认为required(必须实现的).

@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 (详见Selector文档),selector代表着编译后的方法标识符. 我们可以通过@selector()修饰符, 并向其中传入方法名称获取selector.

在这个例子中, 如果datasource实现了这个方法, 就会使用这个title, 否则titlenil.

Note: 局部变量对象在初始化时自动置为nil

如果我们还像之前协议中定义的那样使用一个遵循了XYZPieChartViewDataSourceid 指针变量调用 respondsToSelector:方法, 编译器会警告未知的实例方法. 当我们为id限制了protocol类型的时候, 所有的静态类型检查就都又生效了, 当我们尝试调用在protocol没有定义的方法时, 编译器就会报错. 这时可以通过让我们自定义的协议遵循NSObject协议, 来避免这种编译器报错.

协议继承(inherit)

正如类继承那样, 协议也可以继承.

比如, 我们在定义协议时,最好让其继承NSObject协议.

当我们的自定义类继承了NSObject协议时, 就意味着遵循自定义类的对象也会提供NSObject协议的方法. 由于我们很可能会使用NSObject的子类, 我们就不需要再提供NSObject协议的方法实现了.

协议继承的语法如下:

@protocol MyProtocol <NSObject>
...
@end

在这个例子中, 任何遵循了MyProtocol协议的类也要遵循NSObject的协议.

遵循协议

一个类遵循协议的语法如下:

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

这个例子中, MyClass的实例不仅会响应类接口中声明的方法, 还会响应MyClass类实现了的 MyProtocol中的方法. 不需要在类接口中再次声明协议方法 - 遵循协议就足够了.

Note: 编译器不会自动合成遵循的协议中声明的属性.

如果我们需要在一个类中遵循多个协议, 我们可以通过如下的语法实现:

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

Tip: 如果我们需要在一个类中遵循大量的协议, 那标志着我们需要重构这个过于复杂的类. 我们可以将这个大类按照小的功能分成多个小类, 每个小类明确自己的职责, 来重构此类.

对于OS X和iOS开发者来说, 使用AppDelegate类包含大量的应用功能(管理数据结构,服务器数据,响应手势和其它用户交互等)是一个相当常见的陷阱.当应用功能越来越复杂时, 类的维护会变得越来越困难.

一旦我们遵循了一个协议之后, 那么这个类必须至少为所有的必选方法提供实现,也可以选择实现可选方法.当没有实现所有的必选方法时,编译器会报错.

Note: 协议中的方法声明和其它声明类似. 实现中的方法名称和参数类型必须和协议中的一致.

使用Cocoa和Cocoa Touch中定义的协议

Cocoa和Cocoa Touch类对象提供的大量的可用协议. 比如, TableView需要使用遵循了其数据源协议的数据源对象. TableView也允许我们为其设置遵循了UITableViewDelegate的代理, 这个代理提供了负责管理用户交互的方法.

另外一些协议用来表示类之间的非层级关系的相似点. 

比如:很多框架中的模型对象(比如NSArrayNSDictionary)支持NSCoding协议,这就意味着它们拥有编解码它们的属性的能力. NSCoding协议使得把整个对象的图状关系记录到磁盘上是非常容易的,只要其中的给每个对象都遵循了NSCoding协议.

一部分Objective-C语言级别的特性也依赖于协议. 比如: 为了支持快速遍历,一个集合必须遵循NSFastEnumeration协议,详见Fast Enumeration Makes It Easy to Enumerate a Collection. 除此之外,一些对象可以被复制(copy),比如前文提到过的使用copy attribute 修饰的属性中,任何需要copy的属性类型,必须支持NSCopying协议, 否则在运行时就会产生异常.

通过协议实现类的匿名化

协议在某些对象的类是未知或者需要隐藏的情况下是很有用的.

例如, framework的开发人员可能决定对于framework中的某些类接口不对外开放. 由于类名是未知的, 所以对于framework的使用者来说, 没有办法直接创建这些类的实例. framework中其它的对象会返回一个预制的实例:

id utility = [frameworkObject anonymousUtility];

为了使anonymousUtility对象产生作用,framework的开发者可以发布一个protocol对外公布其一部分的方法. 虽然没有提供原来的类接口, 这个类还是可以通过一种用协议限制的方式使用:

id <XYZFrameworkUtility> utility = [frameworkObject anonymousUtility];

如果我们正在编写使用Core Data框架的iOS App, 我们可能会遇到NSFetchedResultsController类. 这个类用来为UITableView的数据源对象提供存储的数据.

如果我们使用多个section的TableView,我们可以从NSFetchedResultsController 类获取相关的section数据, NSFetchedResultsController遵循了NSFetchedResultsSectionInfo协议, 提供了获取section中的row的数量的功能:

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

虽然我们不知道sectionInfo对象的类, 但是遵循了NSFetchedResultsSectionInfo协议表明它可以响应numberOfObjects消息.

相关资料: Working with Protocols