Working with Protocols
在现实世界中,人们在执行公务的时候往往需要遵循严格的程序在处理某些情况下。例如,执法人员要求“遵循协议”进行查询或收集证据。
在面向对象的编程世界里,当一个对象在给定的情况下能够定义一组行为是很重要的事情。例如,桌面视图希望能够与一个数据源对象交互以便自己知道需要显示的内容。这就意味着数据源必须能够响应桌面视图发送的一组消息。
数据源可以是任何类的实例,比如一个视图控件(NSViewController的子类在OS X或者UIViewController 在iOS上)或者一个也许继承自NSObject的专有的数据源类。为了桌面视图可以知道一个对象是否匹配对应的数据源,重要的是声明对象实现必要的方法。
OC允许你定义协议--protocol,声明一个用于特殊情况下的方法。本章描述了如何定义协议的语法,解释了如何标记一个类的接口遵守一个协议,这就意味着类必须实现所需的方法。
Protocols Define Messaging Contracts
一个类的接口声明这个类的方法和属性。相比之下,一个协议被用来声明方法和属性,独立于任何特定的类。
定义一个协议的基本语法如下:
@protocol ProtocolName |
// list of methods and properties |
@end |
协议可以包含实例方法和类方法的声明,也可以包含属性。
例如,考虑这个用于显示饼状图的自定义视图类,如图:
Figure 5-1 A Custom Pie Chart View
为了使得这个视图尽可能的被重用,所有关于信息的决定都被留给了另一个数据源对象处理。这就意味着许多同样视图类的实例可以显示不同的信息只是因为和不同的信息源交互。
饼状图需要的最小的信息单元包括段数,每个段的大小,每段的名称。所以,这个饼状图的数据源协议如下所示:
@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协议。
Note: Delegate and data source properties are usually marked as weak
for the object graph management reasons described earlier, in Avoid Strong Reference Cycles.
通过在属性上声明所需要的协议,如果你试图通过一个没遵守协议的对象设置属性值,编译器会给你一个警告,尽管基础的属性类类型是通用的。对象是UIViewController的实例或者是NSObject的实例都没关系。最重要的是遵守协议,这就意味着饼状图知道她需要的信息是什么。
Protocols Can Have Optional Methods
默认情况下,一个协议里所有的方法声明都需要相应的方法。这意味着任何遵守协议的类都需要实现那些方法。
在协议里也可以声明一个可选的方法。这些方法只有在被需要的时候才实现。
例如,你可以决定饼状图的名称是可选的。如果数据源对象没有实现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 |
This example defines a protocol with three required methods and two optional methods.
Check that Optional Methods Are Implemented at Runtime
如果一个协议里的方法被标记为可选的,你必须在试图调用方法之前检查一下对象是否实现了该方法。
例如,饼状图视图可能检查段的名称方法,如下所示:
NSString *thisSegmentTitle; |
if ([self.dataSource respondsToSelector:@selector(titleForSegmentAtIndex:)]) { |
thisSegmentTitle = [self.dataSource titleForSegmentAtIndex:index]; |
} |
respondsToSelector方法使用了一个选择器,在编译之后,选择器是一个方法的标识符。你可以通过使用@selector()来提供正确的标识符,声明一个方法的名字。
如果本例中的数据源实现了方法,标题会被使用,否则,标题还是nil。
如果你打算调用respondsToSelector:方法,像上面定义的那样是一个id类型的遵守一个协议的变量,你会得到一个编译器错误,因为没有已知的实例方法对应她。一旦你为一个协议使用id类型,那么就会坚持所以的静态类型,如果你尝试调用任何没有定义在特定协议里的方法,编译器就会报错。一个避免编译器报错的办法就是设置自定义的协议遵守或者说采用NSObject协议。
Protocols Inherit from Other Protocols
In the same way that an Objective-C class can inherit from a superclass, you can also specify that one protocol conforms to another.
一个OC类可以继承一个父类,那么以同样的方式,你可以指定一个协议复合另一个协议。
例如,定义你的协议符合NSObject协议(一些NSObject的行为从类中分离出来到一个分开的协议中,NSObject类采用来了NSObject协议)是个很好的惯例。
通过表明你自己的协议符合NSObject协议,你就表明了任何采用自定义协议的对象也会提供实现对于每个NSObject协议方法。因为你可能使用一些NSObject的子类,所以你不需要担心为这些NSObject方法提供你自己的实现。协议的采用是有帮助的,然而,只是在上面描述的情况下。
为了说明一个协议符合另一个,你需要在尖括号中提供协议的名称,像这样:
@protocol MyProtocol <NSObject> |
... |
@end |
In this example, any object that adopts MyProtocol
also effectively adopts all the methods declared in the NSObject
protocol.
Conforming to Protocols
The syntax to indicate that a class adopts a protocol again uses angle brackets, like this
表明一个类采用一个协议的语法如下:
@interface MyClass : NSObject <MyProtocol> |
... |
@end |
这就意味着任何MyClass类的实例都会不只响应声明在接口中的方法,还需要MyClass类提供在MyProtocol协议中需要实现的方法的实现。不需要在类的接口中重新声明协议里的方法--协议的采用就足够了。
如果你需要一个类采用多个协议,你可以使用逗号分隔,像这样:
@interface MyClass : NSObject <MyProtocol, AnotherProtocol, YetAnotherProtocol> |
... |
@end |
Tip: 如果你在一个类里采用了许多的协议,它可能是一个信号,表明你需要重构一个那些过于复杂的类通过必要的行为拆分到多个小类,每个都有明确的责任
对于OS X和iOS的新手来说使用一个应用程序委托类包含应用程序的大部分功能(管理底层数据结构,提供数据到多个用户界面元素,以及响应手势和其他用户交互)是一个普遍存在的陷阱。随着复杂性的增加,类会变得更难以维护。
一旦你表明了一个协议的一致性,那么类就必须为协议里每个需要实现的方法提供实现代码,还有那些你选择的可选方法。如果你的任何需要实现的方法编译失败了,编译器会有警告。
Cocoa and Cocoa Touch Define a Large Number of Protocols
Cocoa和Cocoa Touch对象会在各种各样的情况下使用协议。例如,桌面视图类(OS X里是NSTableView,iOS里是UITableView)都需要一个数据源对象为她们提供所需要的信息。她们都定义了自己的数据源协议,用法和上面提到的XYZPieChartViewDataSource协议一样。两个桌面视图类都允许你设置一个代理对象,对象也必须采用相关的NSTableViewDelegate或者UITableViewDelegate协议。代理负责处理用户交互,或者自定义显示某个条目。
一些协议被用来表明类之间没有层级的相似之处。与其与一些特定类的要求连接,一些协议相反会连接一些更普遍的Cocoa或者Cocoa Touch交互机制,这些机制可能采用了多个不相连接的类。
例如,许多框架模型对象(如集合类NSArray和NSDictionary)支持NSCoding协议,这意味着他们可以为档案或者分布解码或者编码为原始数据。假如每个图表中的对象都采用了协议,NSCoding使得把对象图形写入磁盘这件事变得相对容易。
一些OC语言级别的特性也依赖协议。例如,为了使用快速枚举器,集合必须采用NSFastEnumeration协议。参见Fast Enumeration Makes It Easy to Enumerate a Collection。此外,一些一些对象可以被复制,比如你使用一个属性参数copy的时候,参见Copy Properties Maintain Their Own Copies。任何你想要复制的对象都必须采用NSCopying协议,不然就会有一个运行时的异常。
Protocols Are Used for Anonymity
当一个类的对象不知道名字或者需要匿名的时候,协议也能派上用场。
例如,框架的开发人员可能选择不去发布一个框架类的接口。因为不知道类名,所以框架的使用者无法直接创建这个类的实例。相反,框架中的其他对象通常会指定返回一个现成的实例,像这样:
id utility = [frameworkObject anonymousUtility]; |
为了让这个anonymousUtility对象可用,框架的开发人员就可以发布一个协议,透露它的一些方法。尽管没有提供原始的类接口,这就意味着这些类是匿名的,而对象仍然可以在有限的方式下被使用,像这样:
id <XYZFrameworkUtility> utility = [frameworkObject anonymousUtility]; |
例如,你使用Core Data 框架写一个iOS的应用,你可能会遇到NSFetchedResultsController
类。这个类的作用是帮助一个数据源对象提供数据的存储在iOSUITableView上,使得像提供行数这样的信息变得容易一些。
如果你处理的桌面视图内容分为多个部分,你也可以为相关的层面信息请求一个获取结果控制器。与其返回一个包含特定层面信息的类,NSFetchedResultsController类会返回一个匿名的对象,这个对象采用了NSFetchedResultsSectionInfo协议。这意味着它仍然可以查询你需要的对象的信息,比如行数:
NSInteger sectionNumber = ... |
id <NSFetchedResultsSectionInfo> sectionInfo = |
[self.fetchedResultsController.sections objectAtIndex:sectionNumber]; |
NSInteger numberOfRowsInSection = [sectionInfo numberOfObjects]; |
即使你不知道sectionInfo对象的类,NSFetchedResultsSectionInfo协议规定,它可以响应numberOfObjects消息。