上一章Objective-C编程快速入门教程请查看:自定义现有的类:类别和扩展
使用协议Protocol在面向对象编程的世界中,能够定义在给定情况下期望对象的一组行为是很重要的。例如,表视图希望能够与数据源对象通信,以便找出需要显示什么。这意味着数据源必须响应表视图可能发送的一组特定消息。
数据源可以是任何类的实例,比如一个视图控制器(OS X上的NSViewController的子类或iOS上的UIViewController),或者一个专用的数据源类,它可能只是继承自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
注意:此协议对无符号整数标量值使用NSUInteger值。下一章将更详细地讨论这种类型。
统计图视图类接口需要一个属性来跟踪数据源对象。这个对象可以是任何类,所以基本的属性类型是id。关于这个对象,我们只知道它符合相关的协议。
声明视图数据源属性的语法如下:
@interface XYZPieChartView : UIView
@property (weak) id <
XYZPieChartViewDataSource> dataSource;
...
@end
Objective-C使用尖括号来表示协议的一致性。这个例子声明了一个通用对象指针的弱属性,它符合XYZPieChartViewDataSource协议。
注意:由于前面描述的对象图管理原因,为了避免强引用周期,代理/委托和数据源属性通常被标记为weak。
通过在属性上指定所需的协议一致性,如果试图将属性设置为不符合协议的对象,即使基本属性类类型是泛型的,也会得到编译器警告。对象是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。
如果试图在符合上面定义的协议的id上调用respondsToSelector:方法,则会得到一个编译器错误,即它没有已知的实例方法。一旦你用协议限定了一个id,所有的静态类型检查都会返回; 如果试图调用未在指定协议中定义的任何方法,则会出现错误。避免编译器错误的一种方法是设置自定义协议以采用NSObject协议。
协议:从其他协议继承
就像Objective-C类可以继承超类一样,你也可以指定一个协议符合另一个协议。
例如,最好的实践是定义你的协议以符合NSObject协议(NSObject的一些行为从其类接口拆分为单独的协议; NSObject类采用NSObject协议)。
通过指示你自己的协议符合NSObject协议,你可以指示采用自定义协议的任何对象也将为每个NSObject协议方法提供实现。因为你可能正在使用NSObject的某个子类,所以不需要为这些NSObject方法提供自己的实现。然而,对于上面描述的情况,采用协议是有用的。
要指定一个协议符合另一个协议,需要在尖括号中提供另一个协议的名称,如下所示:
@protocol MyProtocol <
NSObject>
...
@end
在本例中,任何采用MyProtocol的对象也有效地采用了NSObject协议中声明的所有方法。
遵循/符合协议表示类采用协议的语法同样使用尖括号,如下所示
@interface MyClass : NSObject <
MyProtocol>
...
@end
这意味着MyClass的任何实例不仅响应接口中声明的方法,而且还为MyProtocol中所需的方法提供实现。不需要在类接口中重新声明协议方法—只要采用协议就足够了。
注意:编译器不会自动合成所采用协议中声明的属性。
如果你需要一个类来采用多个协议,你可以将它们指定为逗号分隔的列表,如下所示:
@interface MyClass : NSObject <
MyProtocol, AnotherProtocol, YetAnotherProtocol>
...
@end
提示:如果你发现自己在一个类中采用了大量的协议,这可能意味着你需要重构一个过于复杂的类,方法是将必要的行为分割到多个更小的类中,每个类都有明确定义的职责。
对于新的OS X和iOS开发人员来说,一个比较常见的陷阱是使用一个应用程序委托类来包含应用程序的大部分功能(管理底层数据结构,将数据提供给多个用户界面元素,以及响应手势和其他用户交互)。随着复杂性的增加,类变得更加难于维护。
一旦你指出了协议的一致性,该类至少必须为每个必需的协议方法以及你选择的任何可选方法提供方法实现。如果你没有实现任何需要的方法,编译器会警告你。
注意:协议中的方法声明与其他声明一样。实现中的方法名和参数类型必须与协议中的声明匹配。
Cocoa和Cocoa Touch定义了大量的协议
协议由Cocoa和Cocoa Touch对象用于各种不同的情况。例如,表视图类(用于OS X的NSTableView和用于iOS的UITableView)都使用一个数据源对象来为它们提供必要的信息。两者都定义了自己的数据源协议,其使用方式与上面的XYZPieChartViewDataSource协议示例非常相似。这两个表视图类也允许你设置委托对象,它必须符合相关的NSTableViewDelegate或UITableViewDelegate协议。委托负责处理用户交互,或自定义某些条目的显示。
一些协议用于指示类之间的非层次相似性。一些协议并没有链接到特定的类需求,而是与更一般的Cocoa或Cocoa Touch通信机制相关,这些机制可能被多个不相关的类采用。
例如,许多框架模型对象(比如像NSArray和NSDictionary这样的集合类)支持NSCoding协议,这意味着它们可以对它们的属性进行编码和解码,以便作为原始数据存档或分发。NSCoding使得将整个对象图写入磁盘变得相对容易,只要图中的每个对象都采用该协议。
一些Objective-C语言级别的特性也依赖于协议。例如,为了使用快速枚举,集合必须采用NSFastEnumeration协议,如快速枚举中所述,这使得枚举集合变得很容易。另外,一些对象可以被复制,例如当使用具有copy属性的属性时(如copy Properties中所述),可以维护它们自己的副本。你尝试复制的任何对象都必须采用NSCopying协议,否则你将得到一个运行时异常。
匿名协议协议在对象的类未知或需要隐藏的情况下也很有用。
例如,框架的开发人员可能选择不发布框架内某个类的接口。因为类名是未知的,所以框架的用户不可能直接创建该类的实例。相反,框架中的其他对象通常被指定为返回一个现成的实例,如下所示:
id utility = [frameworkObject anonymousUtility];
为了让这个anonymousUtility对象变得有用,框架的开发人员可以发布一个协议来揭示它的一些方法。即使原始的类接口没有提供,这意味着类是匿名的,对象仍然可以以有限的方式使用:
id <
XYZFrameworkUtility> utility = [frameworkObject anonymousUtility];
例如,如果你正在编写一个使用Core Data框架的iOS应用程序,你可能会遇到NSFetchedResultsController类。这个类被设计用来帮助一个数据源对象向一个iOS UITableView提供存储的数据,使得提供像行数这样的信息变得容易。
如果你正在处理一个内容被分成多个部分的表视图,你还可以向fetchedresultscontroller询问相关的部分信息。NSFetchedResultsController类没有返回包含该节信息的特定类,而是返回一个匿名对象,这符合NSFetchedResultsSectionInfo协议。这意味着你仍然可以通过查询对象来获得你需要的信息,比如一个section中的行数:
NSInteger sectionNumber = ...
id <
NSFetchedResultsSectionInfo> sectionInfo =
[self.fetchedResultsController.sections objectAtIndex:sectionNumber];
NSInteger numberOfRowsInSection = [sectionInfo numberOfObjects];
【协议的定义和使用 – Objective-C编程快速入门教程】即使你不知道sectionInfo对象的类,NSFetchedResultsSectionInfo协议规定它可以响应numberOfObjects消息。
推荐阅读
- 自定义现有的类(类别和扩展 – Objective-C编程快速入门教程)
- 封装数据 – Objective-C编程快速入门教程
- 使用对象 – Objective-C编程快速入门教程
- 定义类 – Objective-C编程快速入门教程
- 入门编程介绍 – Objective-C编程快速入门教程
- Core Foundation编程概念全解
- Objective-C线程技术(线程同步和线程安全)
- Objective-C运行时Runtime完全解读
- iOS内存管理(引用计数、Runloop、AutoreleasePool和引用循环)