11.2 协议

协议是多个类共享的一个方法列表。协议中列出的方法没有相应的实现;由其他人来实现(比如你!)。协议提供一种方式来使用指定的名称定义一组多少有点相关的方法。这些方法通常有文档说明,所以你知道它们将如何执行,因此,如果需要,可在自己的类定义中实现它们。

如果决定实现特定协议的所有方法,也就意味着要遵守(confirm to)或者采用(adopt)这项协议。

定义一个协议很简单:只要使用@protocol指令,之后是你给出的协议名称。然后,和处理接口部分一样,声明一些方法。@end指令之前的所有方法声明都是协议的一部分。

如果选择使用Foundation框架,你将发现一些已定义的协议。其中一个名为NSCopying,而且它声明了一个方法,如果你的类要支持使用copy(或者copyWithZone:)方法来复制对象,则必须实现这个方法。我们将在第18章“复制对象”中讲述复制对象的主题。

下面是在标准的Foundation头文件NSObject.h中定义NSCopying协议的方式:


@protocol NSCopying

-(id)copyWithZone:(NSZone*)zone;

@end


如果你的类采用NSCopying协议,则必须实现名为copyWithZone:的方法。通过在@interface行的一对尖括号(<……>)内列出协议名称,可以告知编译器你正在采用一个协议。这项协议的名称放在类名和它的父类名称之后,如下所示:


@interface AddressBook:NSObject<NSCopying>


这说明,AddressBook是父类为NSObject的对象,并且它遵守NSCopying协议。因为系统已经知道以前为这个协议定义的方法(在这个例子中,它是从头文件NSObject.h中得知的),所以不必在接口部分声明这些方法。但是,要在实现部分定义它们。

因此,在这个例子中,在AddressBook的实现部分,编译器期望找到定义的copyWithZone:方法。

如果你的类采用多项协议,只需把它们都列在尖括号中,用逗号分开,如下所示:


@interface AddressBook:NSObject<NSCopying, NSCoding>


以上代码告知编译器AddressBook类采用NSCopying和NSCoding协议。这次,编译器将期望在AddressBook的实现部分看到为这些协议列出的所有方法的实现。

如果定义了自己的协议,则不必由自己实际实现它。但是,这就告诉其他程序员:如果要采用这项协议,则必须实现这些方法。这些方法可以从超类继承。这样,如果一个类遵守NSCopying协议,则它的子类也遵守NSCopying协议(不过这并不意味着对于该子类,这些方法得到了正确的实现)。

如果希望继承你的类的用户实现一些方法,则可以使用协议定义这些方法。可以为你的GraphicObject类定义一个Drawing协议,并且可以在其中定义paint、erase和outline方法,如下代码所示:


@protocol Drawing

-(void)paint;

-(void)erase;

@optional

-(void)outline;

@end


作为GraphicObject类的创建者,你不必实现这些绘制方法。但是,需要指定一些方法,要求从GraphicObject创建子类的人实现这些方法,以便符合他要创建的绘图对象的标准。

注意注意这里使用了@protocol指令。下面列出的所有方法的指令都是可选的。也就是说,采用Drawing方法不一定要实现outline方法来遵守该协议(之后可以通过在协议定义内使用@required指令来列出需要的方法)。

因此,如果创建名为Rectangle的GraphicObject的子类,并宣传说(也就是,文档说明)这个Rectangle类遵守Drawing协议,那么这个类的用户就知道他们可以向这个类的实例发送paint、erase和outline(可能有)消息。

注意无论如何,这是理论。编译器允许你声明遵守一项协议,并且只有当你没有实现这些方法时,才发出警告消息。

注意协议不引用任何类,它是无类的(classless)。任何类都可以遵守Drawing协议,不仅仅是GraphicObject的子类。

可以使用conformsToProtocol:方法检查一个对象是否遵循某项协议。例如,如果有一个名为currentObject的对象,并且想要查看它是否遵循Drawing协议,可以向它发送绘图消息,可以如下编写:


id currentObject;

……

if([currentObject conformsToProtocol:@protocol(Drawing)]==YES)

{

//Send currentObject paint, erase and/or outline msgs

……

}


这里使用的专用@protocol指令用于获取一个协议名称,并产生一个Protocol对象,conformsToProtocol:方法期望这个对象作为它的参数。

通过在类型名称之后的尖括号中添加协议名称,可以借助编译器的帮助来检查变量的一致性,如下所示:


id<Drawing>currentObject;


这告知编译器currentObject将包含遵守Drawing协议的对象。如果向currentObject指派静态类型的对象,这个对象不遵守Drawing协议(假定有一个Square对象,它不遵守Drawing协议),编译器将发出一条警告消息,如下所示:


warning:class‘Square’does not implement the‘Drawing’protocol


这里存在一项编译器校验,所以向currentObject指派一个id变量不会产生这条信息,因为编译器无法知道存储在id变量中的对象是否遵守Drawing协议。

如果这个变量保存的对象遵守多项协议,则可以列出多项协议,如以下代码所示:


id<NSCopying, NSCoding>myDocument;


定义一项协议时,可以扩展现有协议的定义。所以,以下协议定义


@protocol Drawing3D<Drawing>


说明Drawing3D协议也采用了Drawing协议。因此,任何采用Drawing3D协议的类都必须实现此协议列出的方法,以及Drawing协议的方法。

最后,分类也可以采用一项协议,如下所示:


@interface Fraction(Stuff)<NSCopying, NSCoding>


此处,Fraction拥有一个分类Stuff(当然,并非最佳的名称),这个分类采用了NSCopying和NSCoding协议。

和类名一样,协议名必须是唯一的。

非正式协议

你可能在读物中遇到过非正式(informal)协议的概念。它实际上是一个分类,列出了一组方法但并没有实现它们。每个人(或者几乎每个人)都继承相同的根对象,因此非正式分类通常是为根类定义的。有时,非正式协议也称作抽象(abstract)协议。

如果查看头文件<NSScriptWhoseTests.h>,可能会发现如下所示的一些方法声明:


@interface NSObject(NSComparisonMethods)

-(BOOL)isEqualTo:(id)object;

-(BOOL)isLessThanOrEqualTo:(id)object;

-(BOOL)isLessThan:(id)object;

-(BOOL)isGreaterThanOrEqualTo:(id)object;

-(BOOL)isGreaterThan:(id)object;

-(BOOL)isNotEqualTo:(id)object;

-(BOOL)doesContain:(id)object;

-(BOOL)isLike:(NSString*)object;

-(BOOL)isCaseInsensitiveLike:(NSString*)object;

@end


这些代码为NSObject类定义了一个名为NSComparisonMethods的分类。这项非正式协议列出了一组方法(这里列出了9个),可以将它们实现为协议的一部分。非正式协议实际上仅仅是一个名称之下的一组方法。这在文档说明和模块化方法时,可能有所帮助。

声明非正式协议的类自己并不实现这些方法,并且选择实现这些方法的子类需要在它的接口部分重新声明这些方法,同时还要实现这些方法中的一个或多个。和正式协议不同,编译器不提供有关非正式协议的帮助;这里没有遵守协议或者由编译器测试这样的概念。

如果一个对象采用正式协议,则它必须遵守协议中的所有信息。这可以在运行时以及编译时强制执行。如果一个对象采用非正式协议,则它可能不需要采用此协议的所有方法,具体取决于这项协议。可以在运行时强制要求遵守一项非正式协议(借助respondsToSelector:),但是在编译时不可以。

注意前面描述的@optional指令添加到了Objective-C 2.0语言中,用于取代非正式协议的使用。你可以看到几个UIKit类(UIKit是Cocoa Touch框架的一部分)使用了这一点。