用Objective-C进行面向对象的编程

从Cocoa事件驱动架构的机制和采用的范式可以看出它广泛地使用了面向对象的方法。Objective-C是Cocoa的主要开发语言,也是完全面向对象的语言,尽管它的基础是ANSI C。它为消息的分发提供运行环境支持,也为定义新类指定了语法规则。Objective-C支持绝大多数其它面向对象编程语言(比如C++和Java)具有的抽象和机制,包括继承、封装、重用性、和多态。

但是,Objective-C在一些重要的方面又和其它面向对象的语言不同。举例来说,Objective-C和C++不同,不支持操作符重载、模板、或多重继承。Objective-C也不象Java那样,具有“垃圾收集”机制,可以自动释放不再需要的对象(虽然它有机制和规则可以完成同样的任务)。

虽然Objective-C没有这些特性,但是它作为一种面向对象编程语言的能力可以进行补偿和超越。本文接下来的部分将探讨Objective-C的特殊能力,同时概要介绍Java版本的Cocoa。

进一步阅读:本部分的很多内容是Objective-C权威指南—Objective-C编程语言—一书上的概括。有关Objective-C的详细描述,请查阅该文档。

Objective-C的优点

如果您是一个面向过程的编程人员,对面向对象的概念不熟悉,则可以首先将对象的本质考虑为一个结构体加上关联的函数,这可能有助于理解本文的内容。这个概念和现实相差不太远,特别是在运行环境的实现方面。

每个Objective-C对象都隐藏着一个数据结构,它的第一个成员变量—或者说是实例变量—是“isa指针”(大多数剩下的成员变量由对象的类或超类来定义)。顾名思义,isa指针指向的是对象的类,这个类也是一个对象,有自己的权限(参见图2-1),是根据类的定义编译而来的。类对象负责维护一个方法调度表,该表本质上是由指向类方法的指针组成的;类对象中还保留一个超类的指针,该指针又有自己的方法调度表和超类(还有所有通过继承得到的公共和保护的实例变量)。isa指针对消息分发机制和Cocoa对象的动态能力很关键。

下图为对象的isa指针

用Objective-C进行面向对象的编程

对隐藏在对象表面下的工作机制的惊鸿一瞥只是使您简单了解Objective-C运行环境如何支持消息分发、继承、和一般对象行为的其它方面。但是它对于理解Objective-C的主要能力—动态能力是很必要的。

动态能力

Objective-C是一种非常动态的语言。这种动态能力使程序可以突破编译和连接时的约束,将更多符号辨识的工作转移到处于受控状态的运行环境上。Objective-C比其它编程语言具有更强的动态能力,这种能力来源于如下三个方面:

动态类—在运行时确定对象的类

动态绑定—在运行时确定要调用的方法

动态装载—在运行时为程序增加新的模块

Objective-C为动态类型引入了一个称为id的数据类型,用于表示任意的Cocoa对象。列表2-2中的代码实例显示了这种基本对象类型的典型用法:

id word;
while (word = [enm nextObject]) {
// etc....

id数据类型使我们有可能在运行时进行任意的对象替换。您因此可以在代码中用运行时的因子指定希望使用的对象类型。动态类型使对象中的关联可以在运行时确定,而不需要在静态设计时强制指定对象类型。编译时的类型检查可以确保更加严格的数据完整性,但是作为交换,动态类型则给您的程序更大的灵活性。而且,通过对象的内省(比如询问动态类型转换后的匿名对象所属的类),您仍然可以在运行时确认对象的类型,并验证它是否可以进行特定的操作(当然,您总是可以在需要的时候进行静态类型检查)。

动态类型为Objective-C的第二种动态能力—动态绑定—提供了物质基础。正如动态类型将对象类的确定推迟到运行时一样,动态绑定将调用方法的确定也推迟到运行时。在编译时,方法的调用并不和代码绑定在一起,只有在消息确实发送出来之后,才确定被调用的代码。通过动态类型和动态绑定技术,您的代码每次执行都可以得到不同的结果。运行时因子负责确定消息的接收者和被调用的方法。

运行时的消息分发机制为动态绑定提供支持。当您向一个动态类型确定了的对象发送消息时,运行环境系统会通过接收者的isa指针定位对象的类,并以此为起点确定被调用的方法,方法和消息是动态绑定的。而且,您不必在Objective-C代码中做任何工作,就可以自动获取动态绑定的好处。您在每次发送消息时,特别是当消息的接收者是动态类型已经确定的对象时,动态绑定就会例行而透明地发生。

动态装载是最后一种动态能力。它是Cocoa的一个特性,依赖于Objective-C的运行环境支持。通过动态装载,Cocoa程序可以在需要的时候才装载执行代码和资源,而不是在启动的时候装载所有的程序组件。可执行代码(在装载之前就连接好了)通常包含一些新的、会被集成到应用程序运行时映像的类。代码和本地化资源(包括nib文件)被包装在程序包中,可以通过Foundation框架中的NSBundle类中定义的方法来显式装载。

这种程序代码和资源的“迟缓装载(lazy-loading)”机制降低了对系统内存的要求,从而提升了程序的整体性能。更重要的是,动态装载使应用程序变得可扩展。您可以考虑在应用程序中采用插件架构,使自己和其它开发者可以通过附加的模块进行定制,应用程序可以在发布数月或数年后动态装载附加的模块。如果设计是正确的,这些类就不会和已经存在的类发生冲突,因为每个类都封装了自己的实现并拥有自己的名字空间。

语言扩展

Objective-C在基本语言上做了两个扩展:范畴(categories)和协议(protocols),它们是强大的软件开发工具。这两个扩展引入了声明方法并将它们关联到某个类的技术。

范畴

范畴提供一种为某个类添加方法而又不必制作子类的途径。范畴中的方法会变成类的一部分(在您的应用程序的作用域内),并为该类的所有子类所继承。在运行时,原始方法和通过范畴添加的方法之间没有差别,您可以向类(或者它的子类)实例发送消息,以调用范畴中定义的方法。

范畴不仅是一种为类添加行为的便利方法,还可以对方法进行分组,将相关的方法放在不同的范畴中。范畴对于组织规模大的类特别方便,例如当几个开发者同时在一个类上工作时,您甚至可以将不同的范畴放在不同的源文件中。

范畴的声明和实现很象子类。在语法上,唯一的区别是范畴的名称需要跟在@interface或@implementation导向符之后,且放在园括号中。举例来说,假定您希望为NSArray类增加一个方法,以便用更加结构化的方式打印集合的描述。那么您可以在范畴的头文件中书写如下的声明代码:

#import <Foundation/NSArray.h> // if Foundation not already imported
            
@interface NSArray (PrettyPrintElements)
- (NSString *)prettyPrintDescription;
@end

然后在实现文件中书写如下代码:

#import "PrettyPrintCategory.h"
            
@implementation NSArray (PrettyPrintElements)
- (NSString *)prettyPrintDescription {
// implementation code here...
}
@end

范畴有一些限制。您不能通过范畴为类添加新的实例变量。虽然范畴方法可以覆盖现有的方法,但这并不是推荐的做法,特别是当您希望对现有行为进行增强的时候。一个原因是范畴方法是类接口的一部分,因此无法通过向super发送消息来获取类中已经定义的行为。如果您需要改变一个类的现有方法的行为,更好的方法是生成一个该类的子类。

您可以通过范畴来为根类—NSObject—添加方法。通过这种方式添加的方法可以用于与该代码相连接的所有实例和类对象。非正式的协议—Cocoa委托机制的基础—在NSObject类中声明为范畴。然而,这种在使用上的广泛适用也有它的风险。您通过范畴向NSObject添加的行为可能会有意料不到的结果,可能导致崩溃,数据损坏,甚至更坏的结果。

协议

Objective-C的另一个扩展称为协议,它非常象Java中的接口。两者都是通过一个简单的方法声明列表发布一个接口,任何类都可以选择实现。协议中的方法通过其它类实例发送的消息来进行调用。

协议的主要价值和范畴一样,在于它可以作为子类化的又一个选择。它们带来了C++多重继承的一些优点,使接口(如果不是实现的话)可以得到共享。协议是一个类在声明接口的同时隐藏自身的一种方式。接口可以暴露一个类提供的所有(通常是这种情况)或部分服务。类层次中的其它类都可以通过实现协议中的方法来访问协议发布的服务,不一定和协议类有继承关系(甚至不一定具有相同的根类)。通过协议,一个类即使对另一个类的身份(也就是类的类型)一无所知,也可以和它进行由协议定义的特定目的的交流。

有两种类型的协议:正式和非正式协议。非正式协议在"范畴"部分中已经简单介绍了,它们是NSObject类中定义的范畴。因此每个以NSObject为根类的对象(和类对象)都隐式采纳了范畴中发布的接口。和正式协议不同,一个类不必实现非正式协议中的每个方法,而是只实现它感兴趣的方法就可以了。为了使非正式协议正确工作,声明非正式协议的类在向某个目标对象发送协议消息之前,必须首先向它发送respondsToSelector: 消息并得到肯定的回答(如果目标对象没有实现相应的方法,则产生一个运行时例外)。

Cocoa中的“协议”通常指的是正式协议。它使一个类可以正式地声明一个方法列表,作为向外提供服务的接口。Objective-C语言和运行系统支持正式协议;编译器可以根据协议进行类型检查,对象可以在运行时进行内省,以确认是否遵循某个协议。正式协议有自己的专用术语和语法。术语方面,提供者和客户的意义有所不同:

提供者(通常是一个类)声明正式的协议。

客户类采纳正式协议,表示客户类同意实现协议中所有的方法。

如果一个类采纳某协议或者是从采纳该协议的类派生出来的(协议可以被子类继承),则可以说该类遵循该协议。

在Objective-C中,声明和采纳协议都有自己的语法。协议的声明必须使用编译导向符@protocol。下面的例子显示了NSCoding协议(在Foundation框架的NSObject.h头文件中)的声明方式:

@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (id)initWithCoder:(NSCoder *)aDecoder;
@end

协议的声明类不需要实现这些方法,但应该对遵循该协议的对象方法进行调用。

如果一个类要采纳某个协议,需要在在@interface导向符后、紧接着超类的位置上指定协议的名称,并包含在尖括号中。一个类可以采纳多个协议,不同的协议之间用逗号分隔。下面是Foundation框架中的NSData类采纳三个协议的方式:

@interface NSData : NSObject <NSCopying, NSMutableCopying, NSCoding>

通过采纳这些协议,NSData许诺自己要实现协议中声明的所有方法。范畴也可以采纳协议,对协议的采纳将成为类定义的一部分。

Objective-C通过类遵循的协议和类继承的超类来定义类的类型。您可以通过发送conformsToProtocol:消息来检查一个类是否遵循特定的协议:

if ([anObject conformsToProtocol:@protocol(NSCoding)]) {
// do something appropriate
}

在类型声明—方法、实例变量、或函数中,您可以将遵循的协议作为类型的一部分来指定。这样您就可以通过编译器来得到另一个级别的类型检查,这种检查比较抽象,因为它不和特定的实现相关联。您可以使用与协议采纳相同的语法规则,即把协议的名称放在尖括号中,通过这种语法可以在类型中指定遵循的协议。您常常会看到在这些声明中使用了动态对象类型id,例如:

- (void)draggingEnded:(id <NSDraggingInfo>)sender;

这里,参数中引用的对象可以是任意类型的类,但是必须遵循NSDraggingInfo协议。

除了目前为止已经提到的协议之外,Cocoa还提供了几个协议的例子。一个有趣的例子就是NSObject协议。可以想象得到的是,NSObject类采纳了这个协议,还有一个根类—NSProxy—也采纳了这个协议。通过这个协议,NSProxy类可以和Objective-C运行环境的一部分进行交互,包括引用计数、内省、和对象行为的其它基础部分。

正式协议有其自己的限制。如果协议声明的方法列表随着时间而增长,协议的采纳者就会不再遵循该协议。因此,Cocoa中的正式协议被用于稳定的方法集合,比如NSCopying和NSCoding。如果您预期协议方法会增多,则可以声明为非正式协议,而不是正式协议。

使用Objective-C

在面向对象的程序中,完成工作的方式是通过消息,即一个对象向另一个对象发送消息。通过消息,发送对象可以向接收对象(接收者)发出请求,请求接收者执行某些动作,返回某些对象或值,或者同时执行两者。

Objective-C在消息传递方法采用了独特的语法形式。列表2-2的语句来自SimpleCocoaTool工程的代码:

NSEnumerator *enm = [sorted_args objectEnumerator];

消息表达式位于赋值符号的右边,包含在方括号中。消息表达式中最左边的部分是接收者。它是一个变量,代表送出消息的对象。在这个例子中,接收者是sorted_args,即NSArray类的一个实例。紧接着接收者的是消息体,在这个例子中就是objectEnumerator(这里我们要专注的是消息语法,而不是深入探讨这个SimpleCocoaTool中的消息或其它消息实际上做些什么)。objectEnumerator消息调用sorted_args对象中名为objectEnumerator的方法,该方法会返回一个对象的引用,并由赋值符号左边的enm变量来保存。enm变量的类型被静态地定义为NSEnumerator类的一个实例。您可以将这个语句图解为:

用Objective-C进行面向对象的编程

消息通常有参变量,或者称为参数。仅带一个参数的消息在消息名称后面附加一个冒号,并将参数直接放在冒号后:

用Objective-C进行面向对象的编程

和函数的参变量一样,参数的类型必须和方法声明中指定的类型相匹配。作为例子,请看如下SimpleCocoaTool工程中的表达式:

NSCountedSet *cset = [[NSCountedSet alloc] initWithArray:args];

这里args也是NSArray类的一个实例,它是initWithArray:消息的参数。

如果消息有多个参数,则消息名称就有多个部分,每个部分都以冒号结束,冒号后面是新的参数:

用Objective-C进行面向对象的编程

上面引用的initWithArray:例子很有意思,它说明了嵌套的使用。在Objective-C中,您可以将一个消息嵌套到另一个消息内部,将一个消息表达式返回的对象用作将它包围在内的另一个消息表达式的接收者。因此,为了解释嵌套的消息表达式,可以从最里面的表达式开始,然后向外延伸。下面的语句可以解释为:

将alloc消息发送给NSCountedSet类,以创建(通过为其分配内存)一个未初始化的类实例。

请注意:Objective-C类自身也是对象,因此您也可以象它们的实例一样,向它们发送消息。在消息表达式中,类消息的接收者总是一个类对象。

将initWithArray:消息发送给未初始化的类实例,以根据args数组对对象本身进行初始化,并返回一个自身的引用。

接下来考虑SimpleCocoaTool工程中main例程中的如下语句:

NSArray *sorted_args = [[cset allObjects] sortedArrayUsingSelector:@selector(compare:)];

这个消息表达式中值得注意的是sortedArrayUsingSelector:消息的参数。该参数要求使用编译器导向符@selector来创建一个选择器。选择器是一个名称,在Objective-C运行环境中用于唯一标识一个接收者的方法,这个名称包含消息名的所有部分,包括冒号,但是不包括其它部分,比如返回类型或参数类型。

让我们暂停一下,回顾一下消息和方法的专用术语。方法本质上就是类定义和实现的函数,消息接收者是该类的实例。消息是一个与参数结合在一起的选择器,消息发送给接收者后导致对方法的调用。消息表达式同时包含接收者和消息。图2-2对这些关系进行描述:

下图为消息的专用术语

用Objective-C进行面向对象的编程

Objective-C使用了很多在ANSI C中找不到的类型和常量(literal)。在某些情况下,这些类型和常量会代替ANSI C的对应部分。表2-1描述一些重要的类型,包括每个类型允许使用的常量。

下表Objective-C定义的重要类型和常量

类型 描述和文字
id 动态对象类型,否定常量为nil。
Class 动态类的类型,否定常量为Nil。
SEL 选择器的数据类型(typedef)。和ANSI C一样,这种类型的否定常量为NULL。
BOOL 布尔类型。允许的值为YES和NO。

在程序的控制流程语句中,您可以通过测试正确的否定常量来确定处理流程。举例来说,下面的while语句来自SimpleCocoaTool工程的代码,它隐式测试了word对象,以判断返回对象是否存在(或者从另一个角度看,测试是否不等于nil):

while (word = [enm nextObject]) {
printf("%s\n", [word UTF8String]);
}

在Objective-C中,您可能经常向nil发送消息而没有副作用。运行环境保证发给nil的消息的返回值和其它类型的返回值对象一样是可以工作的。

SimpleCocoaTool代码中最后需要注意的是一些Objective-C的初学者不容易注意到的东西。请对比下面的语句:

NSEnumerator *enm = [sorted_args objectEnumerator];

和:

相关推荐