协议

此文章翻译自苹果官方文档原文地址:http://developer.apple.com/TP30001163-CH12-SW1

协议

协议负责声明那些可以被任意类所实现的方法。协议应用于至少在以下三种情况:

  • 声明那些对象需要实现的方法。
  • 为一个对象声明接口来隐藏它的类。

抽出那些没有继承关系的类之间的相似之处。

 

声明接口

类和接口声明了那些由某一个类联系起来的一些方法——类大部分情况下要实现的一些方法。另一方面,正式和非正式的协议所声明的方法独立于任何一个特定的类,但是任何一个类也许是很多的类都有可能实现。

协议只不过是一个方法声明的列表,并不附属于某一个类的定义。例如:这些负责收集用户鼠标动作的方法就可以被放在一个协议中:

- (void)mouseDown:(NSEvent *)theEvent;
- (void)mouseDragged:(NSEvent *)theEvent;
- (void)mouseUp:(NSEvent *)theEvent;

如果某一个类需要相应鼠标事件就可以采用这个协议并实现他的方法。

协议使方法的声明可以不受类继承关系的影响,所以他们可以用在一些类和分类所不能及的地方。协议列出那些可能在某些地方被实现的方法,但是并不关心具体是哪个类实现的,它所关心的只是是否有一个类遵守了这个协议—-实现了这个协议所声明的方法。这样,对象不仅可以按照继承关系来归类同时还可以根据他们所遵守的协议来分类。不在同一继承关系中的类也可能属于同一类型的因为他们可能遵守相同的协议。

协议在面向对象设计中可以发挥很大的作用,特别是当一个工程要把许多实现的方法区分开或者他包含了一些其他工程中开发的对象。Cocoa大量的使用协议来实现通过Objective-C消息进行进程间的通信。

然而一个objective-c程序也不见得一定要使用协议。同定义类和消息表达式不同,他们是可选的。一些cocoa框架需要他们,一些则不需要。一切都以实际情况为准。

 

预先定义方法

如果你知道一个对象所属的类,你就可以查看它的接口声明(同时还包含继承到的接口声明)来知道它所响应的消息。这些声明中说明了它所能接受的消息而协议则提供了一个途径说明了它所发送的消息。

对象通过发送和接收消息进行通信。例如,一个对象可能会将一个操作委托给另一个对象去做,或者仅仅是向另一个对象请求一些信息。在一些情况下,一个对象可能希望把它的动作通知给其他对象以使他们进行必要的额外的处理。

如果你为同一个工程开发发送类和接受类(又或者某人已经提供了接受类和它的接口文件),这种通信是容易协调的。只需要发送类引用接收类的接口文件,这个接口文件声明了发送类发送的消息中所使用的方法选择器。

然而,如果你要开发一个对象负责发送消息而接收对象还未定义也就是说是留给以后实现的当然你也就不可能得到接收者的接口文件了。这样你就需要有一个途径声明要在消息中使用的方法但是并不实现它。协议正好可以满足这个愿望,它通知编译器类所使用的方法同时还通知其他实现这些方法的类它们需要定义这些方法来同你所定义的类协同工作。

设想,你要开发一个对象它通过向另一个对象发送helpOut:和其他消息来请求帮助。你提供了一个assistant实例变量来保存这些消息的输出对象同时定义一个方法(存取方法)来为实例变量赋值。这些方法可以让其他的对象将它们自己注册为潜在接收者。

- setAssistant:anObject
{
    assistant = anObject;
}

这样当一个消息发送给assistant时,可以对接收者进行校验来判断它是否实现了响应方法:

- (BOOL)doWork
{
    ...
    if ( [assistant respondsToSelector:@selector(helpOut:)] ) {
        [assistant helpOut:self];
        return YES;
    }
    return NO;
}

因为你写代码的时候你并不知道什么类型的对象会将他们自己注册为assistant,这样你就不能引用这个类的接口文件,你只能为helpOut:方法声明一个协议。

为匿名对象声明接口

协议可以用于为匿名对象(不知道类型的对象)声明方法。匿名对象可以代表一种服务或是特定的一组功能,特别是当只有一种类型的对象是需要的。(对象在定义一个应用的结构中占有重要地位并且那种必须先初始化才能使用的对象不适合作为匿名对象)

对象对于他们的开发者来说不是匿名的,但是当他们被提供给其他人使用时就不同了。例如,考虑下面的情况:

  • 某个人提供了一个框架或一组对象供其他人使用,其中可能包含一些对象并没有标明类型是哪个类或哪个接口。由于缺少类名和接口名用户就不能创建某一个类的实例。取而代之的,这些类的提供者必须提供一个已经初始化并分配好的实例。典型的方法就是有另外一个类提供一个方法来返回一个可用的实例对象:
    id formatter = [receiver formattingService];

    这个方法返回的是一个没有指明类型的对象,至少是一个提供者不想指明的。为了让这个对象可以使用,提供者至少必须要指明它可以响应的消息有哪些。这些消息就由与它相关的对象使用一个协议通过声明一个方法列表来标明。

  • 你可以发送一个消息到一个远程对象(在其他应用中的对象)。
    每个应用都有自己的结构,类,和内部逻辑。但是你并不需要了解其他应用是怎么工作的或它的组件之间是如何通信的。作为一个外部调用者,你只需要知道你能发送什么样的消息(协议)和发送给谁(接收者)。如果一个应用将它的一个对象声明作为远程消息的潜在接收者那么同时必须声明一个协议来声明这个对象用于响应那些消息的方法。它并不需要对这个对象的细节进行说明。作为发送者的应用不需要知道这个对象的类型或在自己的设计中使用这个类,需要的仅仅是协议。

协议使匿名对象成为可能。没有协议,就没有办法声明一个对象的接口而不指明它的类。

注意:虽然匿名对象的提供者没有标明它的的类型,但是对象在运行时会暴露它的类型。类消息会返回匿名对象的类。然而,发掘这些额外的信息是没什么意义的,协议中的信息已经足够了。

 

 非层级关系的相似

如果有几个类实现了一组方法,这些类通常由一个声明了共通方法的抽象类来分组。这其中的每一个类都可以按照自己的需要重新实现方法,但是由于继承的关系抽象类还是决定了这些类之间有着必然的相似之处。

然而,有时用抽象类来为共通方法分组是不可行的。有时是一些不相关的类却要实现一些相似的方法。而这有限的相似之处又不足以建立一种继承的关系。例如,你想通过创建一个XML来控制并初始化你的应用的外观显示:

- (NSXMLElement *)XMLRepresentation;
- initFromXMLRepresentation:(NSXMLElement *)xmlString;

这些方法可以被放到一个协议中,实现他们的类仅仅因他们都遵守这个协议而相似。

对象可以由他们所执行的协议来分类,而不仅仅是他们的类。例如,一个NSMatrix实例必须同它所显示的单元格对象进行通信。 这个矩阵可以要求这些单元格对象的类型都为NSCell(一个类),实际上是需要这些对象都继承 NSCell类以实现响应NSMatrix消息的方法。另外,NSMatrix对象也可以要求这些显示单元对象实现一组特定的方法来响应消息(遵守一个协议)。如果这样的话,NSMatrix对象不需要关心单元格对象属于什么类型,仅仅需要他们都实现了需要的方法就可以了。 

 

一个正式的协议

Objective-C 提供了一个正式的方法将一组方法(包括声明的属性)声明为一个协议。正式协议由语言本身和运行时支持。例如,编译器可以基于协议进行类型校验,并且对象可以在运行时进行自查他们是否遵守了一个协议。

声明一个协议

使用 @protocol指令声明一个标准的协议:

@protocol ProtocolName
method declarations
@end

例如,你可以声明一个XML显示协议:

@protocol MyXMLSupport
- initFromXMLRepresentation:(NSXMLElement *)XMLElement;
- (NSXMLElement *)XMLRepresentation;
@end

同类名不同,协议名没有全局可见这个属性。他们只能存在于他们自己的命名空间。

 

可选的协议方法

协议方法可以用@optional关键字标记为可选。同 @optional类似,还有一个 @required关键字用来标记方法为必须。这样就可以用@optional@required把协议分成你需要的两部分。如果你没有使用这些关键字,那么默认为@required。

@protocol MyProtocol
 
- (void)requiredMethod;
 
@optional
- (void)anOptionalMethod;
- (void)anotherOptionalMethod;
 
@required
- (void)anotherRequiredMethod;
 
@end
注意:Mac OS X v10.5中协议不能声明可选的属性。这个限制在Mac OS X v10.6及以后版本取消了。

 

非正式协议

作为正事协议的补充,你也可以通过把方法分到一个分类中来定义一个非正式协议,如下声明:

@interface NSObject ( MyXMLSupport )
- initFromXMLRepresentation:(NSXMLElement *)XMLElement;
- (NSXMLElement *)XMLRepresentation;
@end

非正式协议通常声明为 NSObject类类别, 因为这样定义的方法可以被那些继承自NSObject的类更广泛的使用。因为所有的类都继承自根类(NSObject),这样协议中的方法不会受继承关系中任何一部分的限制(方法在任何类中都能用)。(当然要声明一个其他类类型的非正式协议也是可以的,但是通常没有必要。)

通常当我们声明一个协议时,一个分类接口没有相关的实现。相反,实现协议的类在他们自己的接口文件里会重新声明这些方法并且在实现文件内同其他的一些方法一起定义。

非正式协议打破了分类的规则声明一组方法但是并不把他们同任何一个特定的类或实现关联起来。

作为非正式协议,在分类中声明并不被大多语言所支持。在编译时不会进行类型校验并且在运行时也不会去校验对象遵守哪个协议。如果希望使用这些功能就必须使用正式协议。非正式协议在所有的方法都是可选时会比较有用,例如使用代理,但是通常更推荐使用正式协议(特别是Mac OS X v10.5及以后版本)。

协议对象

正如在运行时类是由类对象表现、方法是由选择代码表现一样,正式协议也是由一种特殊的数据类型——协议类的实例所表现的。如果一个代码中使用协议(除非是用于标明类型)必须要提交到相对应的协议对象。

在很多方面,协议定义同类定义相似。他们都声明方法,并且在运行时他们都由对象表现,类由类的实例表现而协议由协议的实例负责。和类对象一样,协议对象也是由系统在运行时使用代码中定义的方法和声明的变量来创建的。他们都不在程序的源代码中分配内存和初始化。

源代码可以使用@protocol()指令引用一个协议对象,这个指令同时也是声明协议的指令,不同的是后面有一对括号。括号中为协议名字:

Protocol *myXMLSupportProtocol = @protocol(MyXMLSupport);

这是通过代码获得一个协议对象的唯一途径。同类名不同,一个协议名并不单指一个对象——除非是在@protocol()里面。

编译器为每个协议声明创建一个协议对象,但是仅当:

  • 被一个类采用,或者
  • 在代码中被引用(使用 @protocol()

那些被声明但是没有使用的协议(除非是下面提到的类型校验)是不会被协议对象在运行时表现的。

采用一个协议

采用一个协议同声明一个超类类似。都是为一个类分配方法。超类声明是分配继承它的方法,协议分配的是协议中声明的方法。如果一个类的声明中在超类名后面用尖括号包着一个协议名,就说明这个类采用了一个正式协议:

@interface ClassName : ItsSuperclass < protocol list >

分类使用同样的方法采用协议:

@interface ClassName ( CategoryName ) < protocol list >

一个类可以采用多个协议,协议名用逗号分开。

@interface Formatter : NSObject < Formatting, Prettifying >

一个类或者分类若要采用一个协议必须实现这个协议所声明的所有方法,否则编译器会报错。上面提到的Formatter类就必须要定义它所采用的两个协议中定义的所有方法,除了那些它本身要声明的自己的方法。

一个类或者分类若要采用一个协议就必须要引用声明协议的头文件。在被采用的协议中声明的方法不能在类或类接口再声明。

一个类可以仅仅采用一个协议而不声明其他方法。例如,下面的这个类声明采用了Formatting和 Prettifying协议,但是并没有声明它自己的变量或方法。

@interface Formatter : NSObject < Formatting, Prettifying >
@end

服从一个协议

如果一个类采用了一个协议或者它继承的类采用了一个协议那么我们就说这个类服从这个协议。如果一个类服从一组协议那么它的实例也服从于这些协议。

因为一个类若要采用一个协议必须实现所有协议中声明的方法,所以说一个类服从一个协议就等同于说它实现了这个协议声明的所有方法。

可以通过向一个对象发送conformsToProtocol:消息来检查它是否服从于某个协议。

if ( ! [receiver conformsToProtocol:@protocol(MyXMLSupport)]  ) {
    // Object does not conform to MyXMLSupport protocol
    // If you are expecting receiver to implement methods declared in the
    //  MyXMLSupport protocol, this is probably an error
}

(注意:有一个类方法也是用conformsToProtocol:这个名字)

conformsToProtocol:同校验单个方法的respondsToSelector:相似,不同的是conformsToProtocol:校验的是对象是否采用了一个协议(也就是说它实现了所有协议中声明的方法),而不是校验某一个特定的方法是否实现。所以conformsToProtocol:比respondsToSelector:要更有效率。

conformsToProtocol:同时和isKindOfClass:也很相似,所不同的是它的校验是基于协议而不是基于继承关系。

类型校验

对于一个对象的类型声明可以被延伸到引用一个正式协议。 协议提供了一种由编译器进行的另一种类型校验,因为他不会同某一个特定的实现绑定所以他可以更抽象化。

在一个类型声明中,协议名写在一对见括号中放在类型名的后面:

- (id )formattingService;
id <MyXMLSupport> anObject;

同一个静态名可以允许编译器基于类的继承关系进行类型校验一样,这个语法允许编译器基于所服从的协议进行类型校验。

例如,如果Formatter是一个抽象类,如下面这个声明

Formatter *anObject;

所有继承Formatter的对象都被划分为同一类型的一组并且允许编译器针对类型进行校验。

与此相似如下的声明:

id <Formatting> anObject;

所有遵守Formatting协议的对象被划分为同一类型的一组,并且无论他们处在类继承关系中的什么位置。编译器可以确保所有服从这个协议的对象都分配为这个类型。

在这两种情况中,都是将相似的对象分为一个类型——因为他们有相同的继承关系,或者因为他们有共同的一组方法。

这两种类型可以合并到一条声明中:

Formatter <Formatting> *anObject;

协议不能用于为类对象分配类型。只有实例才可以被静态的指定为协议类型,就和只有实例才能被静态的指定为一个类类型一样。(但是,在运行时,所有的类和实例都会响应conformsToProtocol:消息)

 

协议里面的协议

一个协议可以包含另一个协议,使用的语法和类采用协议的语法相同:

@protocol ProtocolName < protocol list >

所有在尖括号中提到的协议会被作为ProtocolName协议的一部分。例如,

@protocol Paging < Formatting >

这样那些遵守Paging协议的对象同时也遵守Formatting协议。如下进行类型声明:

id < Paging > someObject;

同时如下的 conformsToProtocol:消息

if ( [anotherObject conformsToProtocol:@protocol(Paging)] )
    ...

这里需要提到的是仅仅校验Paging协议同时也就校验了是否遵守Formatting协议。

正如上面提到的当一个类采用了一个协议它必须要实现协议声明的所有方法。另外,它也必须要服从那些采用的协议所包含的一些协议。如果所包含的协议仍然包含其他的协议,那么类同时必须服从他们。一个类可以通过以下两个技术来服从于一个被包含的协议。

  • 实现协议声明的方法
  • 继承一个遵守某一协议的类并实现方法

例如,有一个Pager类采用了Paging协议。如果Pager是一个NSObject的子类:

@interface Pager : NSObject < Paging >

它必须要实现Paging中所有的方法,包括Formatting协议中声明的方法。它在采用Paging协议的同时也采用了Formatting协议。

另一种情况,如果Pager是Formatter(采用了Formatting协议的类 )的子类:

@interface Pager : Formatter < Paging >

它必须实现Paging协议中声明的所有方法, 但是Formatting协议中声明的那些就不用了。Pager会从Formatter中继承Formatting协议中声明的方法。

注意:一个类可以采用非正式的方法采用一个协议,仅仅需要实现协议中声明的方法。

引用其他协议

当你在做一个复杂的应用时,你有时需要像如下来写代码:

#import "B.h"
 
@protocol A
- foo:(id <B>) anObject;
 @end

B协议声明如下:

#import "A.h"
 
@protocol B
- bar:(id <A>)anObject;
@end

在这种情况下,因为循环引用,这两个文件都不会正确的编译。为了打破这种循环引用,在定义协议时必须要使用@protocol指令去引用需要的协议而不是直接引用接口文件:

@protocol B;
 
@protocol A
- foo:(id <B>)anObject;
 @end

 注意:这样使用@protocol指令仅仅是通知编译器B是稍后要定义的一个协议。它不会引用定义B协议的接口文件。

 

 

欢迎交流讨论。转载请注明出处。本文地址: http://www.sjslibrary.com/?p=246

posted @ 2012-07-05 09:32  酱酱爱  阅读(453)  评论(0编辑  收藏  举报